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] 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