diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/PublicFlagUpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/PublicFlagUpdateStep.java new file mode 100644 index 0000000000..b2e3c17980 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/PublicFlagUpdateStep.java @@ -0,0 +1,97 @@ +package sonia.scm.update.repository; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContext; +import sonia.scm.SCMContextProvider; +import sonia.scm.migration.UpdateStep; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermission; +import sonia.scm.repository.xml.XmlRepositoryDAO; +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 static sonia.scm.version.Version.parse; + +@Extension +public class PublicFlagUpdateStep implements UpdateStep { + + private static final Logger LOG = LoggerFactory.getLogger(PublicFlagUpdateStep.class); + + private static final String V1_REPOSITORY_BACKUP_FILENAME = "repositories.xml.v1.backup"; + + private final SCMContextProvider contextProvider; + private final XmlUserDAO userDAO; + private final XmlRepositoryDAO repositoryDAO; + + @Inject + public PublicFlagUpdateStep(SCMContextProvider contextProvider, XmlUserDAO userDAO, XmlRepositoryDAO repositoryDAO) { + this.contextProvider = contextProvider; + this.userDAO = userDAO; + this.repositoryDAO = repositoryDAO; + } + + @Override + public void doUpdate() throws JAXBException { + createNewAnonymousUserIfNotExists(); + deleteOldAnonymousUserIfAvailable(); + + JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryHelper.V1RepositoryDatabase.class); + LOG.info("Migrating public flags of repositories as RepositoryRolePermission 'READ' for user '_anonymous'"); + V1RepositoryHelper.readV1Database(jaxbContext, contextProvider, V1_REPOSITORY_BACKUP_FILENAME).ifPresent( + this::addRepositoryReadPermissionForAnonymousUser + ); + } + + @Override + public Version getTargetVersion() { + return parse("2.0.3"); + } + + @Override + public String getAffectedDataType() { + return "sonia.scm.repository.xml"; + } + + private void addRepositoryReadPermissionForAnonymousUser(V1RepositoryHelper.V1RepositoryDatabase v1RepositoryDatabase) { + User v2AnonymousUser = userDAO.get(SCMContext.USER_ANONYMOUS); + v1RepositoryDatabase.repositoryList.repositories + .stream() + .filter(V1Repository::isPublic) + .forEach(v1Repository -> { + Repository v2Repository = repositoryDAO.get(v1Repository.getId()); + LOG.info(String.format("Add RepositoryRole 'READ' to _anonymous user for repository: %s - %s/%s", v2Repository.getId(), v2Repository.getNamespace(), v2Repository.getName())); + v2Repository.addPermission(new RepositoryPermission(v2AnonymousUser.getId(), "READ", false)); + repositoryDAO.modify(v2Repository); + }); + } + + private void createNewAnonymousUserIfNotExists() { + if (!userExists(SCMContext.USER_ANONYMOUS)) { + LOG.info("Create new _anonymous user"); + userDAO.add(SCMContext.ANONYMOUS); + } + } + + private void deleteOldAnonymousUserIfAvailable() { + String oldAnonymous = "anonymous"; + if (userExists(oldAnonymous)) { + User anonymousUser = userDAO.get(oldAnonymous); + LOG.info("Delete obsolete anonymous user"); + userDAO.delete(anonymousUser); + } + } + + private boolean userExists(String username) { + return userDAO + .getAll() + .stream() + .anyMatch(user -> user.getName().equals(username)); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/V1Repository.java b/scm-webapp/src/main/java/sonia/scm/update/repository/V1Repository.java index 4ce823bd33..8b389e467b 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/V1Repository.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/V1Repository.java @@ -4,6 +4,7 @@ import sonia.scm.update.V1Properties; 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.util.List; @@ -16,6 +17,7 @@ public class V1Repository { private String description; private String id; private String name; + @XmlElement(name="public") private boolean isPublic; private boolean archived; private String type; diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/V1RepositoryHelper.java b/scm-webapp/src/main/java/sonia/scm/update/repository/V1RepositoryHelper.java new file mode 100644 index 0000000000..bba89b541f --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/V1RepositoryHelper.java @@ -0,0 +1,51 @@ +package sonia.scm.update.repository; + +import sonia.scm.SCMContextProvider; +import sonia.scm.store.StoreConstants; + +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.Paths; +import java.util.List; +import java.util.Optional; + +import static java.util.Optional.empty; +import static java.util.Optional.of; + +class V1RepositoryHelper { + + static File resolveV1File(SCMContextProvider contextProvider, String filename) { + return contextProvider + .resolve( + Paths.get(StoreConstants.CONFIG_DIRECTORY_NAME).resolve(filename) + ).toFile(); + } + + static Optional readV1Database(JAXBContext jaxbContext, SCMContextProvider contextProvider, String filename) throws JAXBException { + Object unmarshal = jaxbContext.createUnmarshaller().unmarshal(resolveV1File(contextProvider,filename)); + if (unmarshal instanceof V1RepositoryHelper.V1RepositoryDatabase) { + return of((V1RepositoryHelper.V1RepositoryDatabase) unmarshal); + } else { + return empty(); + } + } + + static class RepositoryList { + @XmlElement(name = "repository") + List repositories; + } + + @XmlRootElement(name = "repository-db") + @XmlAccessorType(XmlAccessType.FIELD) + static class V1RepositoryDatabase { + long creationTime; + Long lastModified; + @XmlElement(name = "repositories") + RepositoryList repositoryList; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java index a2f7656498..69354c294a 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java @@ -19,24 +19,17 @@ 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.nio.file.Paths; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; import static java.util.Collections.emptyList; -import static java.util.Optional.empty; -import static java.util.Optional.of; import static sonia.scm.update.V1PropertyReader.REPOSITORY_PROPERTY_READER; +import static sonia.scm.update.repository.V1RepositoryHelper.resolveV1File; import static sonia.scm.version.Version.parse; /** @@ -59,6 +52,8 @@ import static sonia.scm.version.Version.parse; @Extension public class XmlRepositoryV1UpdateStep implements CoreUpdateStep { + private final String V1_REPOSITORY_FILENAME = "repositories" + StoreConstants.FILE_EXTENSION; + private static Logger LOG = LoggerFactory.getLogger(XmlRepositoryV1UpdateStep.class); private final SCMContextProvider contextProvider; @@ -97,12 +92,12 @@ public class XmlRepositoryV1UpdateStep implements CoreUpdateStep { @Override public void doUpdate() throws JAXBException { - if (!resolveV1File().exists()) { + if (!resolveV1File(contextProvider, V1_REPOSITORY_FILENAME).exists()) { LOG.info("no v1 repositories database file found"); return; } - JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class); - readV1Database(jaxbContext).ifPresent( + JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryHelper.V1RepositoryDatabase.class); + V1RepositoryHelper.readV1Database(jaxbContext, contextProvider, V1_REPOSITORY_FILENAME).ifPresent( v1Database -> { v1Database.repositoryList.repositories.forEach(this::readMigrationEntry); v1Database.repositoryList.repositories.forEach(this::update); @@ -112,13 +107,13 @@ public class XmlRepositoryV1UpdateStep implements CoreUpdateStep { } public List getRepositoriesWithoutMigrationStrategies() { - if (!resolveV1File().exists()) { + if (!resolveV1File(contextProvider, V1_REPOSITORY_FILENAME).exists()) { LOG.info("no v1 repositories database file found"); return emptyList(); } try { - JAXBContext jaxbContext = JAXBContext.newInstance(XmlRepositoryV1UpdateStep.V1RepositoryDatabase.class); - return readV1Database(jaxbContext) + JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryHelper.V1RepositoryDatabase.class); + return V1RepositoryHelper.readV1Database(jaxbContext, contextProvider, V1_REPOSITORY_FILENAME) .map(v1Database -> v1Database.repositoryList.repositories.stream()) .orElse(Stream.empty()) .filter(v1Repository -> !this.findMigrationStrategy(v1Repository).isPresent()) @@ -196,33 +191,4 @@ public class XmlRepositoryV1UpdateStep implements CoreUpdateStep { return new RepositoryPermission(v1Permission.getName(), v1Permission.getType(), v1Permission.isGroupPermission()); } - private Optional readV1Database(JAXBContext jaxbContext) throws JAXBException { - Object unmarshal = jaxbContext.createUnmarshaller().unmarshal(resolveV1File()); - if (unmarshal instanceof V1RepositoryDatabase) { - return of((V1RepositoryDatabase) unmarshal); - } else { - return empty(); - } - } - - private File resolveV1File() { - return contextProvider - .resolve( - Paths.get(StoreConstants.CONFIG_DIRECTORY_NAME).resolve("repositories" + StoreConstants.FILE_EXTENSION) - ).toFile(); - } - - private static class RepositoryList { - @XmlElement(name = "repository") - private List repositories; - } - - @XmlRootElement(name = "repository-db") - @XmlAccessorType(XmlAccessType.FIELD) - private static class V1RepositoryDatabase { - private long creationTime; - private Long lastModified; - @XmlElement(name = "repositories") - private RepositoryList repositoryList; - } } diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/PublicFlagUpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/PublicFlagUpdateStepTest.java new file mode 100644 index 0000000000..68edc760c3 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/PublicFlagUpdateStepTest.java @@ -0,0 +1,118 @@ +package sonia.scm.update.repository; + +import org.junit.jupiter.api.BeforeEach; +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.SCMContext; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermission; +import sonia.scm.repository.RepositoryRolePermissions; +import sonia.scm.repository.RepositoryTestData; +import sonia.scm.repository.xml.XmlRepositoryDAO; +import sonia.scm.update.UpdateStepTestUtil; +import sonia.scm.user.User; +import sonia.scm.user.xml.XmlUserDAO; + +import javax.xml.bind.JAXBException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junitpioneer.jupiter.TempDirectory.TempDir; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +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 PublicFlagUpdateStepTest { + + @Mock + XmlUserDAO userDAO; + @Mock + XmlRepositoryDAO repositoryDAO; + @Captor + ArgumentCaptor repositoryCaptor; + + private UpdateStepTestUtil testUtil; + private PublicFlagUpdateStep updateStep; + private Repository REPOSITORY = RepositoryTestData.createHeartOfGold(); + + @BeforeEach + void mockScmHome(@TempDir Path tempDir) throws IOException { + testUtil = new UpdateStepTestUtil(tempDir); + updateStep = new PublicFlagUpdateStep(testUtil.getContextProvider(), userDAO, repositoryDAO); + + //prepare backup xml + V1RepositoryFileSystem.createV1Home(tempDir); + Files.move(tempDir.resolve("config").resolve("repositories.xml"), tempDir.resolve("config").resolve("repositories.xml.v1.backup")); + when(repositoryDAO.get((String) any())).thenReturn(REPOSITORY); + } + + @Test + void shouldDeleteOldAnonymousUserIfExists() throws JAXBException { + when(userDAO.getAll()).thenReturn(Collections.singleton(new User("anonymous"))); + User anonymous = new User("anonymous"); + doReturn(anonymous).when(userDAO).get("anonymous"); + doReturn(SCMContext.ANONYMOUS).when(userDAO).get(SCMContext.USER_ANONYMOUS); + + updateStep.doUpdate(); + + verify(userDAO).delete(anonymous); + } + + @Test + void shouldNotTryToDeleteOldAnonymousUserIfNotExists() throws JAXBException { + when(userDAO.getAll()).thenReturn(Collections.emptyList()); + doReturn(SCMContext.ANONYMOUS).when(userDAO).get(SCMContext.USER_ANONYMOUS); + + updateStep.doUpdate(); + + verify(userDAO, never()).delete(any()); + } + + @Test + void shouldCreateNewAnonymousUserIfNotExists() throws JAXBException { + doReturn(SCMContext.ANONYMOUS).when(userDAO).get(SCMContext.USER_ANONYMOUS); + when(userDAO.getAll()).thenReturn(Collections.singleton(new User("trillian"))); + + updateStep.doUpdate(); + + verify(userDAO).add(SCMContext.ANONYMOUS); + } + + @Test + void shouldNotCreateNewAnonymousUserIfAlreadyExists() throws JAXBException { + doReturn(SCMContext.ANONYMOUS).when(userDAO).get(SCMContext.USER_ANONYMOUS); + when(userDAO.getAll()).thenReturn(Collections.singleton(new User("_anonymous"))); + + updateStep.doUpdate(); + + verify(userDAO, never()).add(SCMContext.ANONYMOUS); + } + + @Test + void shouldMigratePublicFlagToAnonymousRepositoryPermission() throws JAXBException { + when(userDAO.getAll()).thenReturn(Collections.emptyList()); + when(userDAO.get("_anonymous")).thenReturn(SCMContext.ANONYMOUS); + + updateStep.doUpdate(); + + verify(repositoryDAO, times(2)).modify(repositoryCaptor.capture()); + + RepositoryPermission migratedRepositoryPermission = repositoryCaptor.getValue().getPermissions().iterator().next(); + assertThat(migratedRepositoryPermission.getName()).isEqualTo(SCMContext.USER_ANONYMOUS); + assertThat(migratedRepositoryPermission.getRole()).isEqualTo("READ"); + assertThat(migratedRepositoryPermission.isGroupPermission()).isFalse(); + } +} diff --git a/scm-webapp/src/test/resources/sonia/scm/update/repository/scm-home.v1.zip b/scm-webapp/src/test/resources/sonia/scm/update/repository/scm-home.v1.zip index 5d936db5e2..4b21785f20 100644 Binary files a/scm-webapp/src/test/resources/sonia/scm/update/repository/scm-home.v1.zip and b/scm-webapp/src/test/resources/sonia/scm/update/repository/scm-home.v1.zip differ