From 4fbaf89e19c7993a595d90c3044becdfb3774134 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 23 May 2019 17:18:03 +0200 Subject: [PATCH 01/10] migrate users from v1 to v2 & migrate adminFlag to security.xml --- .../src/main/java/sonia/scm/user/User.java | 18 +++ .../scm/user/update/XmlUserV1UpdateStep.java | 145 ++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java diff --git a/scm-core/src/main/java/sonia/scm/user/User.java b/scm-core/src/main/java/sonia/scm/user/User.java index bd4b975561..d974c13a0d 100644 --- a/scm-core/src/main/java/sonia/scm/user/User.java +++ b/scm-core/src/main/java/sonia/scm/user/User.java @@ -105,6 +105,24 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject this.mail = mail; } + /** + * Constructs ... + * + * + * @param name + * @param displayName + * @param mail + */ + public User(String name, String displayName, String mail, String password, String type, boolean active) + { + this.name = name; + this.displayName = displayName; + this.mail = mail; + this.password = password; + this.type = type; + this.active = active; + } + //~--- methods -------------------------------------------------------------- /** diff --git a/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java new file mode 100644 index 0000000000..8a56a9ae09 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java @@ -0,0 +1,145 @@ +package sonia.scm.user.update; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContextProvider; +import sonia.scm.migration.UpdateStep; +import sonia.scm.plugin.Extension; +import sonia.scm.security.AssignedPermission; +import sonia.scm.store.ConfigurationEntryStore; +import sonia.scm.store.ConfigurationEntryStoreFactory; +import sonia.scm.store.StoreConstants; +import sonia.scm.user.User; +import sonia.scm.user.xml.XmlUserDAO; +import sonia.scm.version.Version; + +import javax.inject.Inject; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import java.io.File; +import java.util.List; +import java.util.Map; + +import static sonia.scm.version.Version.parse; + +@Extension +public class XmlUserV1UpdateStep implements UpdateStep { + + private static Logger LOG = LoggerFactory.getLogger(XmlUserV1UpdateStep.class); + + private final SCMContextProvider contextProvider; + private final XmlUserDAO userDAO; + private final ConfigurationEntryStoreFactory configurationEntryStoreFactory; + + @Inject + public XmlUserV1UpdateStep(SCMContextProvider contextProvider, XmlUserDAO userDAO, ConfigurationEntryStoreFactory configurationEntryStoreFactory) { + this.contextProvider = contextProvider; + this.userDAO = userDAO; + this.configurationEntryStoreFactory = configurationEntryStoreFactory; + } + + @Override + public void doUpdate() throws JAXBException { + if (!determineV1File().exists()) { + return; + } + JAXBContext jaxbContext = JAXBContext.newInstance(XmlUserV1UpdateStep.V1UserDatabase.class); + XmlUserV1UpdateStep.V1UserDatabase v1Database = readV1Database(jaxbContext); + ConfigurationEntryStore securityStore = createSecurityStore(); + v1Database.userList.users.forEach(user -> update(user, securityStore)); + } + + @Override + public Version getTargetVersion() { + return parse("0.0.1"); + } + + @Override + public String getAffectedDataType() { + return "sonia.scm.user.xml"; + } + + private void update(XmlUserV1UpdateStep.V1User v1User, ConfigurationEntryStore securityStore) { + User user = new User( + v1User.name, + v1User.displayName, + v1User.mail, + v1User.password, + v1User.type, + v1User.active); + userDAO.add(user); + + if (v1User.admin) { + securityStore.put(new AssignedPermission(v1User.name, "*")); + } + } + + private XmlUserV1UpdateStep.V1UserDatabase readV1Database(JAXBContext jaxbContext) throws JAXBException { + return (XmlUserV1UpdateStep.V1UserDatabase) jaxbContext.createUnmarshaller().unmarshal(determineV1File()); + } + + private ConfigurationEntryStore createSecurityStore() { + return configurationEntryStoreFactory.withType(AssignedPermission.class).withName("security").build(); + } + + private File determineV1File() { + File configDirectory = new File(contextProvider.getBaseDirectory(), StoreConstants.CONFIG_DIRECTORY_NAME); + for (File file : configDirectory.listFiles()) { + if (file.getName().equals("users" + StoreConstants.FILE_EXTENSION)) { + file.renameTo(new File(configDirectory + "/usersV1" + StoreConstants.FILE_EXTENSION)); + } + } + return new File(configDirectory, "usersV1" + StoreConstants.FILE_EXTENSION); + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlRootElement(name = "user") + private static class V1User { + private Map properties; + private boolean admin; + private long creationDate; + private String displayName; + private Long lastModified; + private String mail; + private String name; + private String password; + private String type; + private boolean active; + + @Override + public String toString() { + return "V1User{" + + "properties=" + properties + + ", admin='" + admin + '\'' + + ", creationDate=" + creationDate + '\'' + + ", displayName=" + displayName + '\'' + + ", lastModified=" + lastModified + '\'' + + ", mail='" + mail + '\'' + + ", name='" + name + '\'' + + ", password=" + password + '\'' + + ", type='" + type + '\'' + + ", active='" + active + '\'' + + '}'; + } + } + + private static class UserList { + @XmlElement(name = "user") + private List users; + } + + @XmlRootElement(name = "user-db") + @XmlAccessorType(XmlAccessType.FIELD) + private static class V1UserDatabase { + private long creationTime; + private Long lastModified; + @XmlElement(name = "users") + private XmlUserV1UpdateStep.UserList userList; + } + +} From 9a18facbf520906315acaced4cefbc5e284e30d8 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 23 May 2019 17:19:34 +0200 Subject: [PATCH 02/10] Tests for user migration V1 to V2 --- .../user/update/XmlUserV1UpdateStepTest.java | 103 ++++++++++++++++++ .../resources/sonia/scm/user/update/users.xml | 37 +++++++ 2 files changed, 140 insertions(+) create mode 100644 scm-webapp/src/test/java/sonia/scm/user/update/XmlUserV1UpdateStepTest.java create mode 100644 scm-webapp/src/test/resources/sonia/scm/user/update/users.xml diff --git a/scm-webapp/src/test/java/sonia/scm/user/update/XmlUserV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/user/update/XmlUserV1UpdateStepTest.java new file mode 100644 index 0000000000..03bbbebf9e --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/user/update/XmlUserV1UpdateStepTest.java @@ -0,0 +1,103 @@ +package sonia.scm.user.update; + +import com.google.common.io.Resources; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.SCMContextProvider; +import sonia.scm.security.AssignedPermission; +import sonia.scm.store.ConfigurationEntryStore; +import sonia.scm.store.ConfigurationEntryStoreFactory; +import sonia.scm.store.InMemoryConfigurationEntryStore; +import sonia.scm.store.InMemoryConfigurationEntryStoreFactory; +import sonia.scm.user.User; +import sonia.scm.user.xml.XmlUserDAO; + +import javax.xml.bind.JAXBException; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +import static org.mockito.Mockito.doNothing; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +@ExtendWith(TempDirectory.class) +class XmlUserV1UpdateStepTest { + + @Mock + SCMContextProvider contextProvider; + @Mock + XmlUserDAO userDAO; + + @Captor + ArgumentCaptor userCaptor; + + XmlUserV1UpdateStep updateStep; + ConfigurationEntryStore assignedPermissionStore; + + @BeforeEach + void mockScmHome(@TempDirectory.TempDir Path tempDir) { + when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile()); + assignedPermissionStore = new InMemoryConfigurationEntryStore<>(); + ConfigurationEntryStoreFactory inMemoryConfigurationEntryStoreFactory = new InMemoryConfigurationEntryStoreFactory(assignedPermissionStore); + updateStep = new XmlUserV1UpdateStep(contextProvider,userDAO, inMemoryConfigurationEntryStoreFactory); + } + + @Nested + class WithExistingDatabase { + + @BeforeEach + void captureStoredRepositories() { + doNothing().when(userDAO).add(userCaptor.capture()); + } + + @BeforeEach + void createUserV1XML(@TempDirectory.TempDir Path tempDir) throws IOException { + URL url = Resources.getResource("sonia/scm/user/update/users.xml"); + Path configDir = tempDir.resolve("config"); + Files.createDirectories(configDir); + Files.copy(url.openStream(), configDir.resolve("users.xml")); + } + + @Test + void shouldCreateNewPermissionsForV1AdminUser() throws JAXBException { + updateStep.doUpdate(); + Optional assignedPermission = assignedPermissionStore.getAll().values().stream().filter(a -> a.getName().equals("scmadmin")).findFirst(); + assertThat(assignedPermission.get().getPermission().getValue()).contains("*"); + assertThat(assignedPermission.get().isGroupPermission()).isFalse(); + } + + @Test + void shouldCreateNewUserFromUsersV1Xml() throws JAXBException { + updateStep.doUpdate(); + verify(userDAO, times(3)).add(any()); + } + + @Test + void shouldMapAttributesFromUsersV1Xml() throws JAXBException { + updateStep.doUpdate(); + Optional user = userCaptor.getAllValues().stream().filter(u -> u.getName().equals("scmadmin")).findFirst(); + assertThat(user) + .get() + .hasFieldOrPropertyWithValue("name","scmadmin") + .hasFieldOrPropertyWithValue("mail", "scm-admin@scm-manager.com") + .hasFieldOrPropertyWithValue("displayName", "SCM Administrator") + .hasFieldOrPropertyWithValue("active", false) + .hasFieldOrPropertyWithValue("password", "ff8f5c593a01f9fcd3ed48b09a4b013e8d8f3be7") + .hasFieldOrPropertyWithValue("type", "xml"); + } + } +} diff --git a/scm-webapp/src/test/resources/sonia/scm/user/update/users.xml b/scm-webapp/src/test/resources/sonia/scm/user/update/users.xml new file mode 100644 index 0000000000..1c579389e8 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/user/update/users.xml @@ -0,0 +1,37 @@ + + + 1558597074729 + 1558597185919 + + + + true + 1558597074732 + SCM Administrator + scm-admin@scm-manager.com + scmadmin + ff8f5c593a01f9fcd3ed48b09a4b013e8d8f3be7 + xml + + + + false + 1558597074734 + SCM Anonymous + scm-anonymous@scm-manager.com + anonymous + xml + + + + true + 1558597107621 + edii + 1558597185919 + edi@edi.de + edi + 30f0d7632401710a20719ec21d21bc4ec232aa31 + xml + + + From a91177a2d7e2b5c03416699805f5bcacee154efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 31 May 2019 15:57:30 +0200 Subject: [PATCH 03/10] Migrate creationDate and lastModified --- .../scm/user/update/XmlUserV1UpdateStep.java | 41 ++++++++++++++----- .../user/update/XmlUserV1UpdateStepTest.java | 9 +++- .../resources/sonia/scm/user/update/users.xml | 1 + 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java index 8a56a9ae09..404993a2bf 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java @@ -3,6 +3,7 @@ package sonia.scm.user.update; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.SCMContextProvider; +import sonia.scm.migration.UpdateException; import sonia.scm.migration.UpdateStep; import sonia.scm.plugin.Extension; import sonia.scm.security.AssignedPermission; @@ -22,9 +23,15 @@ import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import java.util.Map; +import java.util.Optional; +import static java.util.Optional.empty; +import static java.util.Optional.of; import static sonia.scm.version.Version.parse; @Extension @@ -45,11 +52,12 @@ public class XmlUserV1UpdateStep implements UpdateStep { @Override public void doUpdate() throws JAXBException { - if (!determineV1File().exists()) { + Optional v1UsersFile = determineV1File(); + if (!v1UsersFile.isPresent()) { + LOG.info("no v1 file for users found"); return; } - JAXBContext jaxbContext = JAXBContext.newInstance(XmlUserV1UpdateStep.V1UserDatabase.class); - XmlUserV1UpdateStep.V1UserDatabase v1Database = readV1Database(jaxbContext); + XmlUserV1UpdateStep.V1UserDatabase v1Database = readV1Database(v1UsersFile.get()); ConfigurationEntryStore securityStore = createSecurityStore(); v1Database.userList.users.forEach(user -> update(user, securityStore)); } @@ -65,6 +73,7 @@ public class XmlUserV1UpdateStep implements UpdateStep { } private void update(XmlUserV1UpdateStep.V1User v1User, ConfigurationEntryStore securityStore) { + LOG.debug("updating user {}", v1User.name); User user = new User( v1User.name, v1User.displayName, @@ -72,29 +81,39 @@ public class XmlUserV1UpdateStep implements UpdateStep { v1User.password, v1User.type, v1User.active); + user.setCreationDate(v1User.creationDate); + user.setLastModified(v1User.lastModified); userDAO.add(user); if (v1User.admin) { + LOG.debug("setting admin permissions for user {}", v1User.name); securityStore.put(new AssignedPermission(v1User.name, "*")); } } - private XmlUserV1UpdateStep.V1UserDatabase readV1Database(JAXBContext jaxbContext) throws JAXBException { - return (XmlUserV1UpdateStep.V1UserDatabase) jaxbContext.createUnmarshaller().unmarshal(determineV1File()); + private XmlUserV1UpdateStep.V1UserDatabase readV1Database(Path v1UsersFile) throws JAXBException { + JAXBContext jaxbContext = JAXBContext.newInstance(XmlUserV1UpdateStep.V1UserDatabase.class); + return (XmlUserV1UpdateStep.V1UserDatabase) jaxbContext.createUnmarshaller().unmarshal(v1UsersFile.toFile()); } private ConfigurationEntryStore createSecurityStore() { return configurationEntryStoreFactory.withType(AssignedPermission.class).withName("security").build(); } - private File determineV1File() { - File configDirectory = new File(contextProvider.getBaseDirectory(), StoreConstants.CONFIG_DIRECTORY_NAME); - for (File file : configDirectory.listFiles()) { - if (file.getName().equals("users" + StoreConstants.FILE_EXTENSION)) { - file.renameTo(new File(configDirectory + "/usersV1" + StoreConstants.FILE_EXTENSION)); + private Optional determineV1File() { + Path configDirectory = new File(contextProvider.getBaseDirectory(), StoreConstants.CONFIG_DIRECTORY_NAME).toPath(); + Path existingUsersFile = configDirectory.resolve("users" + StoreConstants.FILE_EXTENSION); + Path usersV1File = configDirectory.resolve("usersV1" + StoreConstants.FILE_EXTENSION); + if (existingUsersFile.toFile().exists()) { + try { + Files.move(existingUsersFile, usersV1File); + } catch (IOException e) { + throw new UpdateException("could not move old users file to " + usersV1File.toAbsolutePath()); } + LOG.info("moved old users file to {}", usersV1File.toAbsolutePath()); + return of(usersV1File); } - return new File(configDirectory, "usersV1" + StoreConstants.FILE_EXTENSION); + return empty(); } @XmlAccessorType(XmlAccessType.FIELD) diff --git a/scm-webapp/src/test/java/sonia/scm/user/update/XmlUserV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/user/update/XmlUserV1UpdateStepTest.java index 03bbbebf9e..4e7784edda 100644 --- a/scm-webapp/src/test/java/sonia/scm/user/update/XmlUserV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/user/update/XmlUserV1UpdateStepTest.java @@ -97,7 +97,14 @@ class XmlUserV1UpdateStepTest { .hasFieldOrPropertyWithValue("displayName", "SCM Administrator") .hasFieldOrPropertyWithValue("active", false) .hasFieldOrPropertyWithValue("password", "ff8f5c593a01f9fcd3ed48b09a4b013e8d8f3be7") - .hasFieldOrPropertyWithValue("type", "xml"); + .hasFieldOrPropertyWithValue("type", "xml") + .hasFieldOrPropertyWithValue("lastModified", 1558597367492L) + .hasFieldOrPropertyWithValue("creationDate", 1558597074732L); } } + + @Test + void shouldNotFailForMissingConfigDir() throws JAXBException { + updateStep.doUpdate(); + } } diff --git a/scm-webapp/src/test/resources/sonia/scm/user/update/users.xml b/scm-webapp/src/test/resources/sonia/scm/user/update/users.xml index 1c579389e8..a8e8738157 100644 --- a/scm-webapp/src/test/resources/sonia/scm/user/update/users.xml +++ b/scm-webapp/src/test/resources/sonia/scm/user/update/users.xml @@ -7,6 +7,7 @@ true 1558597074732 + 1558597367492 SCM Administrator scm-admin@scm-manager.com scmadmin From d9b9a07591001fea74ce5bc078ac92656ed87d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 31 May 2019 16:32:19 +0200 Subject: [PATCH 04/10] Read admin users from configuration --- .../scm/user/update/XmlUserV1UpdateStep.java | 45 ++++++++++++++++--- .../user/update/XmlUserV1UpdateStepTest.java | 27 ++++++++--- .../sonia/scm/user/update/config.xml | 20 +++++++++ .../resources/sonia/scm/user/update/users.xml | 28 ++++++++++-- 4 files changed, 105 insertions(+), 15 deletions(-) create mode 100644 scm-webapp/src/test/resources/sonia/scm/user/update/config.xml diff --git a/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java index 404993a2bf..dc645c64d7 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java @@ -21,17 +21,20 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; - import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; +import static java.util.Collections.emptyList; import static java.util.Optional.empty; import static java.util.Optional.of; +import static java.util.Optional.ofNullable; import static sonia.scm.version.Version.parse; @Extension @@ -57,9 +60,30 @@ public class XmlUserV1UpdateStep implements UpdateStep { LOG.info("no v1 file for users found"); return; } + Collection adminUsers = determineAdminUsers(); + LOG.debug("found the following admin users from global config: {}", adminUsers); XmlUserV1UpdateStep.V1UserDatabase v1Database = readV1Database(v1UsersFile.get()); ConfigurationEntryStore securityStore = createSecurityStore(); - v1Database.userList.users.forEach(user -> update(user, securityStore)); + v1Database.userList.users.forEach(user -> update(user, adminUsers, securityStore)); + } + + private Collection determineAdminUsers() throws JAXBException { + Path configDirectory = determineConfigDirectory(); + Path existingConfigFile = configDirectory.resolve("config" + StoreConstants.FILE_EXTENSION); + if (existingConfigFile.toFile().exists()) { + return extractAdminUsersFromConfigFile(existingConfigFile); + } else { + return emptyList(); + } + } + + private Collection extractAdminUsersFromConfigFile(Path existingConfigFile) throws JAXBException { + JAXBContext jaxbContext = JAXBContext.newInstance(XmlUserV1UpdateStep.V1Configuration.class); + V1Configuration v1Configuration = (V1Configuration) jaxbContext.createUnmarshaller().unmarshal(existingConfigFile.toFile()); + return ofNullable(v1Configuration.adminUsers) + .map(userList -> userList.split(",")) + .map(Arrays::asList) + .orElse(emptyList()); } @Override @@ -72,7 +96,7 @@ public class XmlUserV1UpdateStep implements UpdateStep { return "sonia.scm.user.xml"; } - private void update(XmlUserV1UpdateStep.V1User v1User, ConfigurationEntryStore securityStore) { + private void update(V1User v1User, Collection adminUsers, ConfigurationEntryStore securityStore) { LOG.debug("updating user {}", v1User.name); User user = new User( v1User.name, @@ -85,7 +109,7 @@ public class XmlUserV1UpdateStep implements UpdateStep { user.setLastModified(v1User.lastModified); userDAO.add(user); - if (v1User.admin) { + if (v1User.admin || adminUsers.contains(v1User.name)) { LOG.debug("setting admin permissions for user {}", v1User.name); securityStore.put(new AssignedPermission(v1User.name, "*")); } @@ -101,7 +125,7 @@ public class XmlUserV1UpdateStep implements UpdateStep { } private Optional determineV1File() { - Path configDirectory = new File(contextProvider.getBaseDirectory(), StoreConstants.CONFIG_DIRECTORY_NAME).toPath(); + Path configDirectory = determineConfigDirectory(); Path existingUsersFile = configDirectory.resolve("users" + StoreConstants.FILE_EXTENSION); Path usersV1File = configDirectory.resolve("usersV1" + StoreConstants.FILE_EXTENSION); if (existingUsersFile.toFile().exists()) { @@ -116,6 +140,10 @@ public class XmlUserV1UpdateStep implements UpdateStep { return empty(); } + private Path determineConfigDirectory() { + return new File(contextProvider.getBaseDirectory(), StoreConstants.CONFIG_DIRECTORY_NAME).toPath(); + } + @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "user") private static class V1User { @@ -147,6 +175,13 @@ public class XmlUserV1UpdateStep implements UpdateStep { } } + @XmlAccessorType(XmlAccessType.FIELD) + @XmlRootElement(name = "scm-config") + private static class V1Configuration { + @XmlElement(name = "admin-users") + private String adminUsers; + } + private static class UserList { @XmlElement(name = "user") private List users; diff --git a/scm-webapp/src/test/java/sonia/scm/user/update/XmlUserV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/user/update/XmlUserV1UpdateStepTest.java index 4e7784edda..d5d3ec93e3 100644 --- a/scm-webapp/src/test/java/sonia/scm/user/update/XmlUserV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/user/update/XmlUserV1UpdateStepTest.java @@ -26,12 +26,12 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; -import static org.mockito.Mockito.doNothing; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(MockitoExtension.class) @ExtendWith(TempDirectory.class) @@ -53,7 +53,7 @@ class XmlUserV1UpdateStepTest { when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile()); assignedPermissionStore = new InMemoryConfigurationEntryStore<>(); ConfigurationEntryStoreFactory inMemoryConfigurationEntryStoreFactory = new InMemoryConfigurationEntryStoreFactory(assignedPermissionStore); - updateStep = new XmlUserV1UpdateStep(contextProvider,userDAO, inMemoryConfigurationEntryStoreFactory); + updateStep = new XmlUserV1UpdateStep(contextProvider, userDAO, inMemoryConfigurationEntryStoreFactory); } @Nested @@ -66,10 +66,10 @@ class XmlUserV1UpdateStepTest { @BeforeEach void createUserV1XML(@TempDirectory.TempDir Path tempDir) throws IOException { - URL url = Resources.getResource("sonia/scm/user/update/users.xml"); Path configDir = tempDir.resolve("config"); Files.createDirectories(configDir); - Files.copy(url.openStream(), configDir.resolve("users.xml")); + copyTestDatabaseFile(configDir, "users.xml"); + copyTestDatabaseFile(configDir, "config.xml"); } @Test @@ -83,7 +83,7 @@ class XmlUserV1UpdateStepTest { @Test void shouldCreateNewUserFromUsersV1Xml() throws JAXBException { updateStep.doUpdate(); - verify(userDAO, times(3)).add(any()); + verify(userDAO, times(5)).add(any()); } @Test @@ -92,7 +92,7 @@ class XmlUserV1UpdateStepTest { Optional user = userCaptor.getAllValues().stream().filter(u -> u.getName().equals("scmadmin")).findFirst(); assertThat(user) .get() - .hasFieldOrPropertyWithValue("name","scmadmin") + .hasFieldOrPropertyWithValue("name", "scmadmin") .hasFieldOrPropertyWithValue("mail", "scm-admin@scm-manager.com") .hasFieldOrPropertyWithValue("displayName", "SCM Administrator") .hasFieldOrPropertyWithValue("active", false) @@ -101,6 +101,19 @@ class XmlUserV1UpdateStepTest { .hasFieldOrPropertyWithValue("lastModified", 1558597367492L) .hasFieldOrPropertyWithValue("creationDate", 1558597074732L); } + + @Test + void shouldCreatePermissionForUsersConfiguredAsAdminInConfig() throws JAXBException { + updateStep.doUpdate(); + Optional assignedPermission = assignedPermissionStore.getAll().values().stream().filter(a -> a.getName().equals("dent")).findFirst(); + assertThat(assignedPermission.get().getPermission().getValue()).contains("*"); + assertThat(assignedPermission.get().isGroupPermission()).isFalse(); + } + } + + private void copyTestDatabaseFile(Path configDir, String usersFileName) throws IOException { + URL url = Resources.getResource("sonia/scm/user/update/" + usersFileName); + Files.copy(url.openStream(), configDir.resolve(usersFileName)); } @Test diff --git a/scm-webapp/src/test/resources/sonia/scm/user/update/config.xml b/scm-webapp/src/test/resources/sonia/scm/user/update/config.xml new file mode 100644 index 0000000000..43a93f7d3a --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/user/update/config.xml @@ -0,0 +1,20 @@ + + + admins,vogons + arthur,dent + http://localhost:8081/scm + false + false + 80 + http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false + 8080 + proxy.mydomain.com + localhost + false + false + 8181 + false + false + Y-m-d H:i:s + false + diff --git a/scm-webapp/src/test/resources/sonia/scm/user/update/users.xml b/scm-webapp/src/test/resources/sonia/scm/user/update/users.xml index a8e8738157..69556105db 100644 --- a/scm-webapp/src/test/resources/sonia/scm/user/update/users.xml +++ b/scm-webapp/src/test/resources/sonia/scm/user/update/users.xml @@ -25,12 +25,34 @@ - true + false 1558597107621 - edii + Arthur Dent 1558597185919 edi@edi.de - edi + dent + 30f0d7632401710a20719ec21d21bc4ec232aa31 + xml + + + + false + 1558597107621 + Jeltz + 1558597185919 + edi@edi.de + jeltz + 30f0d7632401710a20719ec21d21bc4ec232aa31 + xml + + + + false + 1558597107621 + Marvin + 1558597185919 + edi@edi.de + marvin 30f0d7632401710a20719ec21d21bc4ec232aa31 xml From 0372c63e532630d6f9ab3a9b21fd5d743cff01af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 31 May 2019 16:37:26 +0200 Subject: [PATCH 05/10] Set version number --- .../main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java index dc645c64d7..aef94a21a3 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java @@ -88,7 +88,7 @@ public class XmlUserV1UpdateStep implements UpdateStep { @Override public Version getTargetVersion() { - return parse("0.0.1"); + return parse("2.0.0"); } @Override From f6bffb3903bf42b553d09b8f54e9f1effa6033e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 31 May 2019 16:38:16 +0200 Subject: [PATCH 06/10] Fix logger --- .../main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java index aef94a21a3..f0f68d8d59 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java @@ -40,7 +40,7 @@ import static sonia.scm.version.Version.parse; @Extension public class XmlUserV1UpdateStep implements UpdateStep { - private static Logger LOG = LoggerFactory.getLogger(XmlUserV1UpdateStep.class); + private static final Logger LOG = LoggerFactory.getLogger(XmlUserV1UpdateStep.class); private final SCMContextProvider contextProvider; private final XmlUserDAO userDAO; From cd80ff77eb128197a99e687de67bc505cf69bb07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 3 Jun 2019 09:58:56 +0200 Subject: [PATCH 07/10] Remove password hash from log output --- .../src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java | 1 - 1 file changed, 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java index f0f68d8d59..4a607dbac1 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java @@ -168,7 +168,6 @@ public class XmlUserV1UpdateStep implements UpdateStep { ", lastModified=" + lastModified + '\'' + ", mail='" + mail + '\'' + ", name='" + name + '\'' + - ", password=" + password + '\'' + ", type='" + type + '\'' + ", active='" + active + '\'' + '}'; From 218937be19d3a90293b5aea7e4d11b0e351863a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 3 Jun 2019 10:39:25 +0200 Subject: [PATCH 08/10] Add group migration --- .../group/update/XmlGroupV1UpdateStep.java | 191 ++++++++++++++++++ .../update/XmlGroupV1UpdateStepTest.java | 114 +++++++++++ .../sonia/scm/group/update/config.xml | 20 ++ .../sonia/scm/group/update/groups.xml | 25 +++ .../resources/sonia/scm/user/update/users.xml | 2 +- 5 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/group/update/XmlGroupV1UpdateStep.java create mode 100644 scm-webapp/src/test/java/sonia/scm/group/update/XmlGroupV1UpdateStepTest.java create mode 100644 scm-webapp/src/test/resources/sonia/scm/group/update/config.xml create mode 100644 scm-webapp/src/test/resources/sonia/scm/group/update/groups.xml diff --git a/scm-webapp/src/main/java/sonia/scm/group/update/XmlGroupV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/group/update/XmlGroupV1UpdateStep.java new file mode 100644 index 0000000000..87368919bf --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/group/update/XmlGroupV1UpdateStep.java @@ -0,0 +1,191 @@ +package sonia.scm.group.update; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContextProvider; +import sonia.scm.group.xml.XmlGroupDAO; +import sonia.scm.migration.UpdateException; +import sonia.scm.migration.UpdateStep; +import sonia.scm.plugin.Extension; +import sonia.scm.security.AssignedPermission; +import sonia.scm.store.ConfigurationEntryStore; +import sonia.scm.store.ConfigurationEntryStoreFactory; +import sonia.scm.store.StoreConstants; +import sonia.scm.group.Group; +import sonia.scm.version.Version; + +import javax.inject.Inject; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static java.util.Collections.emptyList; +import static java.util.Optional.empty; +import static java.util.Optional.of; +import static java.util.Optional.ofNullable; +import static sonia.scm.version.Version.parse; + +@Extension +public class XmlGroupV1UpdateStep implements UpdateStep { + + private static final Logger LOG = LoggerFactory.getLogger(XmlGroupV1UpdateStep.class); + + private final SCMContextProvider contextProvider; + private final XmlGroupDAO groupDAO; + private final ConfigurationEntryStoreFactory configurationEntryStoreFactory; + + @Inject + public XmlGroupV1UpdateStep(SCMContextProvider contextProvider, XmlGroupDAO groupDAO, ConfigurationEntryStoreFactory configurationEntryStoreFactory) { + this.contextProvider = contextProvider; + this.groupDAO = groupDAO; + this.configurationEntryStoreFactory = configurationEntryStoreFactory; + } + + @Override + public void doUpdate() throws JAXBException { + Optional v1GroupsFile = determineV1File(); + if (!v1GroupsFile.isPresent()) { + LOG.info("no v1 file for groups found"); + return; + } + Collection adminGroups = determineAdminGroups(); + LOG.debug("found the following admin groups from global config: {}", adminGroups); + XmlGroupV1UpdateStep.V1GroupDatabase v1Database = readV1Database(v1GroupsFile.get()); + ConfigurationEntryStore securityStore = createSecurityStore(); + v1Database.groupList.groups.forEach(group -> update(group, adminGroups, securityStore)); + } + + private Collection determineAdminGroups() throws JAXBException { + Path configDirectory = determineConfigDirectory(); + Path existingConfigFile = configDirectory.resolve("config" + StoreConstants.FILE_EXTENSION); + if (existingConfigFile.toFile().exists()) { + return extractAdminGroupsFromConfigFile(existingConfigFile); + } else { + return emptyList(); + } + } + + private Collection extractAdminGroupsFromConfigFile(Path existingConfigFile) throws JAXBException { + JAXBContext jaxbContext = JAXBContext.newInstance(XmlGroupV1UpdateStep.V1Configuration.class); + V1Configuration v1Configuration = (V1Configuration) jaxbContext.createUnmarshaller().unmarshal(existingConfigFile.toFile()); + return ofNullable(v1Configuration.adminGroups) + .map(groupList -> groupList.split(",")) + .map(Arrays::asList) + .orElse(emptyList()); + } + + @Override + public Version getTargetVersion() { + return parse("2.0.0"); + } + + @Override + public String getAffectedDataType() { + return "sonia.scm.group.xml"; + } + + private void update(V1Group v1Group, Collection adminGroups, ConfigurationEntryStore securityStore) { + LOG.debug("updating group {}", v1Group.name); + Group group = new Group( + v1Group.type, + v1Group.name, + v1Group.members); + group.setDescription(v1Group.description); + group.setCreationDate(v1Group.creationDate); + group.setLastModified(v1Group.lastModified); + groupDAO.add(group); + + if (adminGroups.contains(v1Group.name)) { + LOG.debug("setting admin permissions for group {}", v1Group.name); + securityStore.put(new AssignedPermission(v1Group.name, true, "*")); + } + } + + private XmlGroupV1UpdateStep.V1GroupDatabase readV1Database(Path v1GroupsFile) throws JAXBException { + JAXBContext jaxbContext = JAXBContext.newInstance(XmlGroupV1UpdateStep.V1GroupDatabase.class); + return (XmlGroupV1UpdateStep.V1GroupDatabase) jaxbContext.createUnmarshaller().unmarshal(v1GroupsFile.toFile()); + } + + private ConfigurationEntryStore createSecurityStore() { + return configurationEntryStoreFactory.withType(AssignedPermission.class).withName("security").build(); + } + + private Optional determineV1File() { + Path configDirectory = determineConfigDirectory(); + Path existingGroupsFile = configDirectory.resolve("groups" + StoreConstants.FILE_EXTENSION); + Path groupsV1File = configDirectory.resolve("groupsV1" + StoreConstants.FILE_EXTENSION); + if (existingGroupsFile.toFile().exists()) { + try { + Files.move(existingGroupsFile, groupsV1File); + } catch (IOException e) { + throw new UpdateException("could not move old groups file to " + groupsV1File.toAbsolutePath()); + } + LOG.info("moved old groups file to {}", groupsV1File.toAbsolutePath()); + return of(groupsV1File); + } + return empty(); + } + + private Path determineConfigDirectory() { + return new File(contextProvider.getBaseDirectory(), StoreConstants.CONFIG_DIRECTORY_NAME).toPath(); + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlRootElement(name = "group") + private static class V1Group { + private Map properties; + private long creationDate; + private String description; + private Long lastModified; + private String name; + private String type; + @XmlElement(name = "members") + private List members; + + @Override + public String toString() { + return "V1Group{" + + "properties=" + properties + + ", creationDate=" + creationDate + '\'' + + ", description=" + description + '\'' + + ", lastModified=" + lastModified + '\'' + + ", name='" + name + '\'' + + ", type='" + type + '\'' + + '}'; + } + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlRootElement(name = "scm-config") + private static class V1Configuration { + @XmlElement(name = "admin-groups") + private String adminGroups; + } + + private static class GroupList { + @XmlElement(name = "group") + private List groups; + } + + @XmlRootElement(name = "group-db") + @XmlAccessorType(XmlAccessType.FIELD) + private static class V1GroupDatabase { + private long creationTime; + private Long lastModified; + @XmlElement(name = "groups") + private XmlGroupV1UpdateStep.GroupList groupList; + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/group/update/XmlGroupV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/group/update/XmlGroupV1UpdateStepTest.java new file mode 100644 index 0000000000..02a995317f --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/group/update/XmlGroupV1UpdateStepTest.java @@ -0,0 +1,114 @@ +package sonia.scm.group.update; + +import com.google.common.io.Resources; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.SCMContextProvider; +import sonia.scm.security.AssignedPermission; +import sonia.scm.store.ConfigurationEntryStore; +import sonia.scm.store.ConfigurationEntryStoreFactory; +import sonia.scm.store.InMemoryConfigurationEntryStore; +import sonia.scm.store.InMemoryConfigurationEntryStoreFactory; +import sonia.scm.group.Group; +import sonia.scm.group.xml.XmlGroupDAO; + +import javax.xml.bind.JAXBException; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@ExtendWith(TempDirectory.class) +class XmlGroupV1UpdateStepTest { + + @Mock + SCMContextProvider contextProvider; + @Mock + XmlGroupDAO groupDAO; + + @Captor + ArgumentCaptor groupCaptor; + + XmlGroupV1UpdateStep updateStep; + ConfigurationEntryStore assignedPermissionStore; + + @BeforeEach + void mockScmHome(@TempDirectory.TempDir Path tempDir) { + when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile()); + assignedPermissionStore = new InMemoryConfigurationEntryStore<>(); + ConfigurationEntryStoreFactory inMemoryConfigurationEntryStoreFactory = new InMemoryConfigurationEntryStoreFactory(assignedPermissionStore); + updateStep = new XmlGroupV1UpdateStep(contextProvider, groupDAO, inMemoryConfigurationEntryStoreFactory); + } + + @Nested + class WithExistingDatabase { + + @BeforeEach + void captureStoredRepositories() { + doNothing().when(groupDAO).add(groupCaptor.capture()); + } + + @BeforeEach + void createGroupV1XML(@TempDirectory.TempDir Path tempDir) throws IOException { + Path configDir = tempDir.resolve("config"); + Files.createDirectories(configDir); + copyTestDatabaseFile(configDir, "groups.xml"); + copyTestDatabaseFile(configDir, "config.xml"); + } + + @Test + void shouldCreateNewGroupFromGroupsV1Xml() throws JAXBException { + updateStep.doUpdate(); + verify(groupDAO, times(2)).add(any()); + } + + @Test + void shouldMapAttributesFromGroupsV1Xml() throws JAXBException { + updateStep.doUpdate(); + Optional group = groupCaptor.getAllValues().stream().filter(u -> u.getName().equals("normals")).findFirst(); + assertThat(group) + .get() + .hasFieldOrPropertyWithValue("name", "normals") + .hasFieldOrPropertyWithValue("description", "Normal people") + .hasFieldOrPropertyWithValue("type", "xml") + .hasFieldOrPropertyWithValue("members", asList("trillian", "dent")) + .hasFieldOrPropertyWithValue("lastModified", 1559550955883L) + .hasFieldOrPropertyWithValue("creationDate", 1559548942457L); + } + + @Test + void shouldCreatePermissionForGroupsConfiguredAsAdminInConfig() throws JAXBException { + updateStep.doUpdate(); + Optional assignedPermission = assignedPermissionStore.getAll().values().stream().filter(a -> a.getName().equals("vogons")).findFirst(); + assertThat(assignedPermission.get().getPermission().getValue()).contains("*"); + assertThat(assignedPermission.get().isGroupPermission()).isTrue(); + } + } + + private void copyTestDatabaseFile(Path configDir, String groupsFileName) throws IOException { + URL url = Resources.getResource("sonia/scm/group/update/" + groupsFileName); + Files.copy(url.openStream(), configDir.resolve(groupsFileName)); + } + + @Test + void shouldNotFailForMissingConfigDir() throws JAXBException { + updateStep.doUpdate(); + } +} diff --git a/scm-webapp/src/test/resources/sonia/scm/group/update/config.xml b/scm-webapp/src/test/resources/sonia/scm/group/update/config.xml new file mode 100644 index 0000000000..43a93f7d3a --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/group/update/config.xml @@ -0,0 +1,20 @@ + + + admins,vogons + arthur,dent + http://localhost:8081/scm + false + false + 80 + http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false + 8080 + proxy.mydomain.com + localhost + false + false + 8181 + false + false + Y-m-d H:i:s + false + diff --git a/scm-webapp/src/test/resources/sonia/scm/group/update/groups.xml b/scm-webapp/src/test/resources/sonia/scm/group/update/groups.xml new file mode 100644 index 0000000000..f058a01e7a --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/group/update/groups.xml @@ -0,0 +1,25 @@ + + + 1559548854204 + + + + 1559548913984 + They love poetry + vogons + jeltz + xml + + + + 1559548942457 + Normal people + normals + trillian + dent + 1559550955883 + xml + + + 1559548942458 + diff --git a/scm-webapp/src/test/resources/sonia/scm/user/update/users.xml b/scm-webapp/src/test/resources/sonia/scm/user/update/users.xml index 69556105db..f586996384 100644 --- a/scm-webapp/src/test/resources/sonia/scm/user/update/users.xml +++ b/scm-webapp/src/test/resources/sonia/scm/user/update/users.xml @@ -38,7 +38,7 @@ false 1558597107621 - Jeltz + Prostetnic Vogon Jeltz 1558597185919 edi@edi.de jeltz From a970404717efe18e68cfade7e6929e2dc4e76d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 3 Jun 2019 11:38:42 +0200 Subject: [PATCH 09/10] Move migration of admin flags to own update step --- .../group/update/XmlGroupV1UpdateStep.java | 55 +-------- .../update/XmlSecurityV1UpdateStep.java | 105 ++++++++++++++++++ .../scm/user/update/XmlUserV1UpdateStep.java | 38 +------ .../update/XmlGroupV1UpdateStepTest.java | 20 +--- .../update/XmlSecurityV1UpdateStepTest.java | 93 ++++++++++++++++ .../user/update/XmlUserV1UpdateStepTest.java | 9 -- .../scm/{group => security}/update/config.xml | 2 +- .../sonia/scm/user/update/config.xml | 20 ---- 8 files changed, 210 insertions(+), 132 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/security/update/XmlSecurityV1UpdateStep.java create mode 100644 scm-webapp/src/test/java/sonia/scm/security/update/XmlSecurityV1UpdateStepTest.java rename scm-webapp/src/test/resources/sonia/scm/{group => security}/update/config.xml (94%) delete mode 100644 scm-webapp/src/test/resources/sonia/scm/user/update/config.xml diff --git a/scm-webapp/src/main/java/sonia/scm/group/update/XmlGroupV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/group/update/XmlGroupV1UpdateStep.java index 87368919bf..146a709e7f 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/update/XmlGroupV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/group/update/XmlGroupV1UpdateStep.java @@ -3,15 +3,12 @@ package sonia.scm.group.update; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.SCMContextProvider; +import sonia.scm.group.Group; import sonia.scm.group.xml.XmlGroupDAO; import sonia.scm.migration.UpdateException; import sonia.scm.migration.UpdateStep; import sonia.scm.plugin.Extension; -import sonia.scm.security.AssignedPermission; -import sonia.scm.store.ConfigurationEntryStore; -import sonia.scm.store.ConfigurationEntryStoreFactory; import sonia.scm.store.StoreConstants; -import sonia.scm.group.Group; import sonia.scm.version.Version; import javax.inject.Inject; @@ -25,16 +22,12 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; -import static java.util.Collections.emptyList; import static java.util.Optional.empty; import static java.util.Optional.of; -import static java.util.Optional.ofNullable; import static sonia.scm.version.Version.parse; @Extension @@ -44,13 +37,11 @@ public class XmlGroupV1UpdateStep implements UpdateStep { private final SCMContextProvider contextProvider; private final XmlGroupDAO groupDAO; - private final ConfigurationEntryStoreFactory configurationEntryStoreFactory; @Inject - public XmlGroupV1UpdateStep(SCMContextProvider contextProvider, XmlGroupDAO groupDAO, ConfigurationEntryStoreFactory configurationEntryStoreFactory) { + public XmlGroupV1UpdateStep(SCMContextProvider contextProvider, XmlGroupDAO groupDAO) { this.contextProvider = contextProvider; this.groupDAO = groupDAO; - this.configurationEntryStoreFactory = configurationEntryStoreFactory; } @Override @@ -60,30 +51,8 @@ public class XmlGroupV1UpdateStep implements UpdateStep { LOG.info("no v1 file for groups found"); return; } - Collection adminGroups = determineAdminGroups(); - LOG.debug("found the following admin groups from global config: {}", adminGroups); XmlGroupV1UpdateStep.V1GroupDatabase v1Database = readV1Database(v1GroupsFile.get()); - ConfigurationEntryStore securityStore = createSecurityStore(); - v1Database.groupList.groups.forEach(group -> update(group, adminGroups, securityStore)); - } - - private Collection determineAdminGroups() throws JAXBException { - Path configDirectory = determineConfigDirectory(); - Path existingConfigFile = configDirectory.resolve("config" + StoreConstants.FILE_EXTENSION); - if (existingConfigFile.toFile().exists()) { - return extractAdminGroupsFromConfigFile(existingConfigFile); - } else { - return emptyList(); - } - } - - private Collection extractAdminGroupsFromConfigFile(Path existingConfigFile) throws JAXBException { - JAXBContext jaxbContext = JAXBContext.newInstance(XmlGroupV1UpdateStep.V1Configuration.class); - V1Configuration v1Configuration = (V1Configuration) jaxbContext.createUnmarshaller().unmarshal(existingConfigFile.toFile()); - return ofNullable(v1Configuration.adminGroups) - .map(groupList -> groupList.split(",")) - .map(Arrays::asList) - .orElse(emptyList()); + v1Database.groupList.groups.forEach(group -> update(group)); } @Override @@ -96,7 +65,7 @@ public class XmlGroupV1UpdateStep implements UpdateStep { return "sonia.scm.group.xml"; } - private void update(V1Group v1Group, Collection adminGroups, ConfigurationEntryStore securityStore) { + private void update(V1Group v1Group) { LOG.debug("updating group {}", v1Group.name); Group group = new Group( v1Group.type, @@ -106,11 +75,6 @@ public class XmlGroupV1UpdateStep implements UpdateStep { group.setCreationDate(v1Group.creationDate); group.setLastModified(v1Group.lastModified); groupDAO.add(group); - - if (adminGroups.contains(v1Group.name)) { - LOG.debug("setting admin permissions for group {}", v1Group.name); - securityStore.put(new AssignedPermission(v1Group.name, true, "*")); - } } private XmlGroupV1UpdateStep.V1GroupDatabase readV1Database(Path v1GroupsFile) throws JAXBException { @@ -118,10 +82,6 @@ public class XmlGroupV1UpdateStep implements UpdateStep { return (XmlGroupV1UpdateStep.V1GroupDatabase) jaxbContext.createUnmarshaller().unmarshal(v1GroupsFile.toFile()); } - private ConfigurationEntryStore createSecurityStore() { - return configurationEntryStoreFactory.withType(AssignedPermission.class).withName("security").build(); - } - private Optional determineV1File() { Path configDirectory = determineConfigDirectory(); Path existingGroupsFile = configDirectory.resolve("groups" + StoreConstants.FILE_EXTENSION); @@ -167,13 +127,6 @@ public class XmlGroupV1UpdateStep implements UpdateStep { } } - @XmlAccessorType(XmlAccessType.FIELD) - @XmlRootElement(name = "scm-config") - private static class V1Configuration { - @XmlElement(name = "admin-groups") - private String adminGroups; - } - private static class GroupList { @XmlElement(name = "group") private List groups; diff --git a/scm-webapp/src/main/java/sonia/scm/security/update/XmlSecurityV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/security/update/XmlSecurityV1UpdateStep.java new file mode 100644 index 0000000000..52bf8124e3 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/update/XmlSecurityV1UpdateStep.java @@ -0,0 +1,105 @@ +package sonia.scm.security.update; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContextProvider; +import sonia.scm.migration.UpdateStep; +import sonia.scm.plugin.Extension; +import sonia.scm.security.AssignedPermission; +import sonia.scm.store.ConfigurationEntryStore; +import sonia.scm.store.ConfigurationEntryStoreFactory; +import sonia.scm.store.StoreConstants; +import sonia.scm.version.Version; + +import javax.inject.Inject; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.File; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.function.Consumer; + +import static java.util.Optional.ofNullable; +import static sonia.scm.version.Version.parse; + +@Extension +public class XmlSecurityV1UpdateStep implements UpdateStep { + + private static final Logger LOG = LoggerFactory.getLogger(XmlSecurityV1UpdateStep.class); + + private final SCMContextProvider contextProvider; + private final ConfigurationEntryStoreFactory configurationEntryStoreFactory; + + @Inject + public XmlSecurityV1UpdateStep(SCMContextProvider contextProvider, ConfigurationEntryStoreFactory configurationEntryStoreFactory) { + this.contextProvider = contextProvider; + this.configurationEntryStoreFactory = configurationEntryStoreFactory; + } + + @Override + public void doUpdate() throws JAXBException { + ConfigurationEntryStore securityStore = createSecurityStore(); + + forAllAdmins(user -> createSecurityEntry(user, false, securityStore), + group -> createSecurityEntry(group, true, securityStore)); + } + + private void forAllAdmins(Consumer userConsumer, Consumer groupConsumer) throws JAXBException { + Path configDirectory = determineConfigDirectory(); + Path existingConfigFile = configDirectory.resolve("config" + StoreConstants.FILE_EXTENSION); + if (existingConfigFile.toFile().exists()) { + forAllAdmins(existingConfigFile, userConsumer, groupConsumer); + } + } + + private void forAllAdmins( + Path existingConfigFile, Consumer userConsumer, Consumer groupConsumer + ) throws JAXBException { + JAXBContext jaxbContext = JAXBContext.newInstance(XmlSecurityV1UpdateStep.V1Configuration.class); + V1Configuration v1Configuration = (V1Configuration) jaxbContext.createUnmarshaller().unmarshal(existingConfigFile.toFile()); + + ofNullable(v1Configuration.adminUsers).ifPresent(users -> forAll(users, userConsumer)); + ofNullable(v1Configuration.adminGroups).ifPresent(groups -> forAll(groups, groupConsumer)); + } + + private void forAll(String entries, Consumer consumer) { + Arrays.stream(entries.split(",")).forEach(consumer); + } + + + @Override + public Version getTargetVersion() { + return parse("2.0.0"); + } + + @Override + public String getAffectedDataType() { + return "sonia.scm.security.xml"; + } + + private void createSecurityEntry(String name, boolean group, ConfigurationEntryStore securityStore) { + LOG.debug("setting admin permissions for {} {}", group? "group": "user", name); + securityStore.put(new AssignedPermission(name, group, "*")); + } + + private ConfigurationEntryStore createSecurityStore() { + return configurationEntryStoreFactory.withType(AssignedPermission.class).withName("security").build(); + } + + private Path determineConfigDirectory() { + return new File(contextProvider.getBaseDirectory(), StoreConstants.CONFIG_DIRECTORY_NAME).toPath(); + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlRootElement(name = "scm-config") + private static class V1Configuration { + @XmlElement(name = "admin-users") + private String adminUsers; + @XmlElement(name = "admin-groups") + private String adminGroups; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java index 4a607dbac1..8c5e760710 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java @@ -25,16 +25,12 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; -import static java.util.Collections.emptyList; import static java.util.Optional.empty; import static java.util.Optional.of; -import static java.util.Optional.ofNullable; import static sonia.scm.version.Version.parse; @Extension @@ -60,30 +56,9 @@ public class XmlUserV1UpdateStep implements UpdateStep { LOG.info("no v1 file for users found"); return; } - Collection adminUsers = determineAdminUsers(); - LOG.debug("found the following admin users from global config: {}", adminUsers); XmlUserV1UpdateStep.V1UserDatabase v1Database = readV1Database(v1UsersFile.get()); ConfigurationEntryStore securityStore = createSecurityStore(); - v1Database.userList.users.forEach(user -> update(user, adminUsers, securityStore)); - } - - private Collection determineAdminUsers() throws JAXBException { - Path configDirectory = determineConfigDirectory(); - Path existingConfigFile = configDirectory.resolve("config" + StoreConstants.FILE_EXTENSION); - if (existingConfigFile.toFile().exists()) { - return extractAdminUsersFromConfigFile(existingConfigFile); - } else { - return emptyList(); - } - } - - private Collection extractAdminUsersFromConfigFile(Path existingConfigFile) throws JAXBException { - JAXBContext jaxbContext = JAXBContext.newInstance(XmlUserV1UpdateStep.V1Configuration.class); - V1Configuration v1Configuration = (V1Configuration) jaxbContext.createUnmarshaller().unmarshal(existingConfigFile.toFile()); - return ofNullable(v1Configuration.adminUsers) - .map(userList -> userList.split(",")) - .map(Arrays::asList) - .orElse(emptyList()); + v1Database.userList.users.forEach(user -> update(user, securityStore)); } @Override @@ -96,7 +71,7 @@ public class XmlUserV1UpdateStep implements UpdateStep { return "sonia.scm.user.xml"; } - private void update(V1User v1User, Collection adminUsers, ConfigurationEntryStore securityStore) { + private void update(V1User v1User, ConfigurationEntryStore securityStore) { LOG.debug("updating user {}", v1User.name); User user = new User( v1User.name, @@ -109,7 +84,7 @@ public class XmlUserV1UpdateStep implements UpdateStep { user.setLastModified(v1User.lastModified); userDAO.add(user); - if (v1User.admin || adminUsers.contains(v1User.name)) { + if (v1User.admin) { LOG.debug("setting admin permissions for user {}", v1User.name); securityStore.put(new AssignedPermission(v1User.name, "*")); } @@ -174,13 +149,6 @@ public class XmlUserV1UpdateStep implements UpdateStep { } } - @XmlAccessorType(XmlAccessType.FIELD) - @XmlRootElement(name = "scm-config") - private static class V1Configuration { - @XmlElement(name = "admin-users") - private String adminUsers; - } - private static class UserList { @XmlElement(name = "user") private List users; diff --git a/scm-webapp/src/test/java/sonia/scm/group/update/XmlGroupV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/group/update/XmlGroupV1UpdateStepTest.java index 02a995317f..552298ed77 100644 --- a/scm-webapp/src/test/java/sonia/scm/group/update/XmlGroupV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/group/update/XmlGroupV1UpdateStepTest.java @@ -11,13 +11,11 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.SCMContextProvider; -import sonia.scm.security.AssignedPermission; -import sonia.scm.store.ConfigurationEntryStore; -import sonia.scm.store.ConfigurationEntryStoreFactory; -import sonia.scm.store.InMemoryConfigurationEntryStore; -import sonia.scm.store.InMemoryConfigurationEntryStoreFactory; import sonia.scm.group.Group; import sonia.scm.group.xml.XmlGroupDAO; +import sonia.scm.security.AssignedPermission; +import sonia.scm.store.ConfigurationEntryStore; +import sonia.scm.store.InMemoryConfigurationEntryStore; import javax.xml.bind.JAXBException; import java.io.IOException; @@ -53,8 +51,7 @@ class XmlGroupV1UpdateStepTest { void mockScmHome(@TempDirectory.TempDir Path tempDir) { when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile()); assignedPermissionStore = new InMemoryConfigurationEntryStore<>(); - ConfigurationEntryStoreFactory inMemoryConfigurationEntryStoreFactory = new InMemoryConfigurationEntryStoreFactory(assignedPermissionStore); - updateStep = new XmlGroupV1UpdateStep(contextProvider, groupDAO, inMemoryConfigurationEntryStoreFactory); + updateStep = new XmlGroupV1UpdateStep(contextProvider, groupDAO); } @Nested @@ -70,7 +67,6 @@ class XmlGroupV1UpdateStepTest { Path configDir = tempDir.resolve("config"); Files.createDirectories(configDir); copyTestDatabaseFile(configDir, "groups.xml"); - copyTestDatabaseFile(configDir, "config.xml"); } @Test @@ -92,14 +88,6 @@ class XmlGroupV1UpdateStepTest { .hasFieldOrPropertyWithValue("lastModified", 1559550955883L) .hasFieldOrPropertyWithValue("creationDate", 1559548942457L); } - - @Test - void shouldCreatePermissionForGroupsConfiguredAsAdminInConfig() throws JAXBException { - updateStep.doUpdate(); - Optional assignedPermission = assignedPermissionStore.getAll().values().stream().filter(a -> a.getName().equals("vogons")).findFirst(); - assertThat(assignedPermission.get().getPermission().getValue()).contains("*"); - assertThat(assignedPermission.get().isGroupPermission()).isTrue(); - } } private void copyTestDatabaseFile(Path configDir, String groupsFileName) throws IOException { diff --git a/scm-webapp/src/test/java/sonia/scm/security/update/XmlSecurityV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/security/update/XmlSecurityV1UpdateStepTest.java new file mode 100644 index 0000000000..62e82ce168 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/update/XmlSecurityV1UpdateStepTest.java @@ -0,0 +1,93 @@ +package sonia.scm.security.update; + +import com.google.common.io.Resources; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.SCMContextProvider; +import sonia.scm.security.AssignedPermission; +import sonia.scm.store.ConfigurationEntryStore; +import sonia.scm.store.ConfigurationEntryStoreFactory; +import sonia.scm.store.InMemoryConfigurationEntryStore; +import sonia.scm.store.InMemoryConfigurationEntryStoreFactory; + +import javax.xml.bind.JAXBException; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@ExtendWith(TempDirectory.class) +class XmlSecurityV1UpdateStepTest { + + @Mock + SCMContextProvider contextProvider; + + XmlSecurityV1UpdateStep updateStep; + ConfigurationEntryStore assignedPermissionStore; + + @BeforeEach + void mockScmHome(@TempDirectory.TempDir Path tempDir) { + when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile()); + assignedPermissionStore = new InMemoryConfigurationEntryStore<>(); + ConfigurationEntryStoreFactory inMemoryConfigurationEntryStoreFactory = new InMemoryConfigurationEntryStoreFactory(assignedPermissionStore); + updateStep = new XmlSecurityV1UpdateStep(contextProvider, inMemoryConfigurationEntryStoreFactory); + } + + @Nested + class WithExistingDatabase { + + @BeforeEach + void createConfigV1XML(@TempDirectory.TempDir Path tempDir) throws IOException { + Path configDir = tempDir.resolve("config"); + Files.createDirectories(configDir); + copyTestDatabaseFile(configDir, "config.xml"); + } + + @Test + void shouldCreatePermissionForUsersConfiguredAsAdmin() throws JAXBException { + updateStep.doUpdate(); + List assignedPermission = + assignedPermissionStore.getAll().values() + .stream() + .filter(a -> a.getPermission().getValue().equals("*")) + .filter(a -> !a.isGroupPermission()) + .map(AssignedPermission::getName) + .collect(toList()); + assertThat(assignedPermission).contains("arthur", "dent", "ldap-admin"); + } + + @Test + void shouldCreatePermissionForGroupsConfiguredAsAdmin() throws JAXBException { + updateStep.doUpdate(); + List assignedPermission = + assignedPermissionStore.getAll().values() + .stream() + .filter(a -> a.getPermission().getValue().equals("*")) + .filter(AssignedPermission::isGroupPermission) + .map(AssignedPermission::getName) + .collect(toList()); + assertThat(assignedPermission).contains("admins", "vogons"); + } + } + + private void copyTestDatabaseFile(Path configDir, String fileName) throws IOException { + URL url = Resources.getResource("sonia/scm/security/update/" + fileName); + Files.copy(url.openStream(), configDir.resolve(fileName)); + } + + @Test + void shouldNotFailForMissingConfigDir() throws JAXBException { + updateStep.doUpdate(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/user/update/XmlUserV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/user/update/XmlUserV1UpdateStepTest.java index d5d3ec93e3..764fda2c5d 100644 --- a/scm-webapp/src/test/java/sonia/scm/user/update/XmlUserV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/user/update/XmlUserV1UpdateStepTest.java @@ -69,7 +69,6 @@ class XmlUserV1UpdateStepTest { Path configDir = tempDir.resolve("config"); Files.createDirectories(configDir); copyTestDatabaseFile(configDir, "users.xml"); - copyTestDatabaseFile(configDir, "config.xml"); } @Test @@ -101,14 +100,6 @@ class XmlUserV1UpdateStepTest { .hasFieldOrPropertyWithValue("lastModified", 1558597367492L) .hasFieldOrPropertyWithValue("creationDate", 1558597074732L); } - - @Test - void shouldCreatePermissionForUsersConfiguredAsAdminInConfig() throws JAXBException { - updateStep.doUpdate(); - Optional assignedPermission = assignedPermissionStore.getAll().values().stream().filter(a -> a.getName().equals("dent")).findFirst(); - assertThat(assignedPermission.get().getPermission().getValue()).contains("*"); - assertThat(assignedPermission.get().isGroupPermission()).isFalse(); - } } private void copyTestDatabaseFile(Path configDir, String usersFileName) throws IOException { diff --git a/scm-webapp/src/test/resources/sonia/scm/group/update/config.xml b/scm-webapp/src/test/resources/sonia/scm/security/update/config.xml similarity index 94% rename from scm-webapp/src/test/resources/sonia/scm/group/update/config.xml rename to scm-webapp/src/test/resources/sonia/scm/security/update/config.xml index 43a93f7d3a..a87e80859e 100644 --- a/scm-webapp/src/test/resources/sonia/scm/group/update/config.xml +++ b/scm-webapp/src/test/resources/sonia/scm/security/update/config.xml @@ -1,7 +1,7 @@ admins,vogons - arthur,dent + arthur,dent,ldap-admin http://localhost:8081/scm false false diff --git a/scm-webapp/src/test/resources/sonia/scm/user/update/config.xml b/scm-webapp/src/test/resources/sonia/scm/user/update/config.xml deleted file mode 100644 index 43a93f7d3a..0000000000 --- a/scm-webapp/src/test/resources/sonia/scm/user/update/config.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - admins,vogons - arthur,dent - http://localhost:8081/scm - false - false - 80 - http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false - 8080 - proxy.mydomain.com - localhost - false - false - 8181 - false - false - Y-m-d H:i:s - false - From 055cee2c4d05c3a836788842d95441346fe94ef1 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 3 Jun 2019 13:25:19 +0000 Subject: [PATCH 10/10] Close branch feature/migrate_user_v1