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/group/update/XmlGroupV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/group/update/XmlGroupV1UpdateStep.java new file mode 100644 index 0000000000..146a709e7f --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/group/update/XmlGroupV1UpdateStep.java @@ -0,0 +1,144 @@ +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.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.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 +public class XmlGroupV1UpdateStep implements UpdateStep { + + private static final Logger LOG = LoggerFactory.getLogger(XmlGroupV1UpdateStep.class); + + private final SCMContextProvider contextProvider; + private final XmlGroupDAO groupDAO; + + @Inject + public XmlGroupV1UpdateStep(SCMContextProvider contextProvider, XmlGroupDAO groupDAO) { + this.contextProvider = contextProvider; + this.groupDAO = groupDAO; + } + + @Override + public void doUpdate() throws JAXBException { + Optional v1GroupsFile = determineV1File(); + if (!v1GroupsFile.isPresent()) { + LOG.info("no v1 file for groups found"); + return; + } + XmlGroupV1UpdateStep.V1GroupDatabase v1Database = readV1Database(v1GroupsFile.get()); + v1Database.groupList.groups.forEach(group -> update(group)); + } + + @Override + public Version getTargetVersion() { + return parse("2.0.0"); + } + + @Override + public String getAffectedDataType() { + return "sonia.scm.group.xml"; + } + + private void update(V1Group v1Group) { + 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); + } + + private XmlGroupV1UpdateStep.V1GroupDatabase readV1Database(Path v1GroupsFile) throws JAXBException { + JAXBContext jaxbContext = JAXBContext.newInstance(XmlGroupV1UpdateStep.V1GroupDatabase.class); + return (XmlGroupV1UpdateStep.V1GroupDatabase) jaxbContext.createUnmarshaller().unmarshal(v1GroupsFile.toFile()); + } + + 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 + '\'' + + '}'; + } + } + + 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/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 new file mode 100644 index 0000000000..8c5e760710 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/user/update/XmlUserV1UpdateStep.java @@ -0,0 +1,166 @@ +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; +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.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 +public class XmlUserV1UpdateStep implements UpdateStep { + + private static final 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 { + Optional v1UsersFile = determineV1File(); + if (!v1UsersFile.isPresent()) { + LOG.info("no v1 file for users found"); + return; + } + XmlUserV1UpdateStep.V1UserDatabase v1Database = readV1Database(v1UsersFile.get()); + ConfigurationEntryStore securityStore = createSecurityStore(); + v1Database.userList.users.forEach(user -> update(user, securityStore)); + } + + @Override + public Version getTargetVersion() { + return parse("2.0.0"); + } + + @Override + public String getAffectedDataType() { + return "sonia.scm.user.xml"; + } + + private void update(V1User v1User, ConfigurationEntryStore securityStore) { + LOG.debug("updating user {}", v1User.name); + User user = new User( + v1User.name, + v1User.displayName, + v1User.mail, + 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(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 Optional determineV1File() { + Path configDirectory = determineConfigDirectory(); + 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 empty(); + } + + private Path determineConfigDirectory() { + return new File(contextProvider.getBaseDirectory(), StoreConstants.CONFIG_DIRECTORY_NAME).toPath(); + } + + @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 + '\'' + + ", 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; + } + +} 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..552298ed77 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/group/update/XmlGroupV1UpdateStepTest.java @@ -0,0 +1,102 @@ +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.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; +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<>(); + updateStep = new XmlGroupV1UpdateStep(contextProvider, groupDAO); + } + + @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"); + } + + @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); + } + } + + 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/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 new file mode 100644 index 0000000000..764fda2c5d --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/user/update/XmlUserV1UpdateStepTest.java @@ -0,0 +1,114 @@ +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.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 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 { + Path configDir = tempDir.resolve("config"); + Files.createDirectories(configDir); + copyTestDatabaseFile(configDir, "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(5)).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") + .hasFieldOrPropertyWithValue("lastModified", 1558597367492L) + .hasFieldOrPropertyWithValue("creationDate", 1558597074732L); + } + } + + 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 + void shouldNotFailForMissingConfigDir() throws JAXBException { + updateStep.doUpdate(); + } +} 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/security/update/config.xml b/scm-webapp/src/test/resources/sonia/scm/security/update/config.xml new file mode 100644 index 0000000000..a87e80859e --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/security/update/config.xml @@ -0,0 +1,20 @@ + + + admins,vogons + arthur,dent,ldap-admin + 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 new file mode 100644 index 0000000000..f586996384 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/user/update/users.xml @@ -0,0 +1,60 @@ + + + 1558597074729 + 1558597185919 + + + + true + 1558597074732 + 1558597367492 + SCM Administrator + scm-admin@scm-manager.com + scmadmin + ff8f5c593a01f9fcd3ed48b09a4b013e8d8f3be7 + xml + + + + false + 1558597074734 + SCM Anonymous + scm-anonymous@scm-manager.com + anonymous + xml + + + + false + 1558597107621 + Arthur Dent + 1558597185919 + edi@edi.de + dent + 30f0d7632401710a20719ec21d21bc4ec232aa31 + xml + + + + false + 1558597107621 + Prostetnic Vogon Jeltz + 1558597185919 + edi@edi.de + jeltz + 30f0d7632401710a20719ec21d21bc4ec232aa31 + xml + + + + false + 1558597107621 + Marvin + 1558597185919 + edi@edi.de + marvin + 30f0d7632401710a20719ec21d21bc4ec232aa31 + xml + + +