diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/SingleRepositoryUpdateProcessor.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/SingleRepositoryUpdateProcessor.java new file mode 100644 index 0000000000..eeb95f75b0 --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/SingleRepositoryUpdateProcessor.java @@ -0,0 +1,15 @@ +package sonia.scm.repository.xml; + +import javax.inject.Inject; +import java.nio.file.Path; +import java.util.function.BiConsumer; + +public class SingleRepositoryUpdateProcessor { + + @Inject + private PathBasedRepositoryLocationResolver locationResolver; + + public void doUpdate(BiConsumer forEachRepository) { + locationResolver.forAllPaths(forEachRepository); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/MigrateVerbsToPermissionRoles.java b/scm-webapp/src/main/java/sonia/scm/repository/update/MigrateVerbsToPermissionRoles.java new file mode 100644 index 0000000000..122b9a25e8 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/MigrateVerbsToPermissionRoles.java @@ -0,0 +1,143 @@ +package sonia.scm.repository.update; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.migration.UpdateException; +import sonia.scm.migration.UpdateStep; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.HealthCheckFailure; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermission; +import sonia.scm.repository.RepositoryRole; +import sonia.scm.repository.xml.SingleRepositoryUpdateProcessor; +import sonia.scm.security.SystemRepositoryPermissionProvider; +import sonia.scm.version.Version; + +import javax.inject.Inject; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; +import java.nio.file.Path; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +@Extension +public class MigrateVerbsToPermissionRoles extends RepositoryUpdates.RepositoryUpdateType implements UpdateStep { + + public static final Logger LOG = LoggerFactory.getLogger(MigrateVerbsToPermissionRoles.class); + + private final SingleRepositoryUpdateProcessor updateProcessor; + private final SystemRepositoryPermissionProvider systemRepositoryPermissionProvider; + + @Inject + public MigrateVerbsToPermissionRoles(SingleRepositoryUpdateProcessor updateProcessor, SystemRepositoryPermissionProvider systemRepositoryPermissionProvider) { + this.updateProcessor = updateProcessor; + this.systemRepositoryPermissionProvider = systemRepositoryPermissionProvider; + } + + @Override + public void doUpdate() { + updateProcessor.doUpdate(this::update); + } + + void update(String repositoryId, Path path) { + LOG.info("updating repository {}", repositoryId); + OldRepository oldRepository = readOldRepository(path); + Repository newRepository = createNewRepository(oldRepository); + writeNewRepository(path, newRepository); + } + + private void writeNewRepository(Path path, Repository newRepository) { + try { + JAXBContext jaxbContext = JAXBContext.newInstance(Repository.class); + Marshaller marshaller = jaxbContext.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + marshaller.marshal(newRepository, path.resolve("metadata.xml").toFile()); + } catch (JAXBException e) { + throw new UpdateException("could not read old repository structure", e); + } + } + + private OldRepository readOldRepository(Path path) { + try { + JAXBContext jaxbContext = JAXBContext.newInstance(OldRepository.class); + return (OldRepository) jaxbContext.createUnmarshaller().unmarshal(path.resolve("metadata.xml").toFile()); + } catch (JAXBException e) { + throw new UpdateException("could not read old repository structure", e); + } + } + + private Repository createNewRepository(OldRepository oldRepository) { + Repository repository = new Repository( + oldRepository.id, + oldRepository.type, + oldRepository.namespace, + oldRepository.name, + oldRepository.contact, + oldRepository.description, + oldRepository.permissions.stream().map(this::updatePermission).toArray(RepositoryPermission[]::new) + ); + repository.setCreationDate(oldRepository.creationDate); + repository.setHealthCheckFailures(oldRepository.healthCheckFailures); + repository.setLastModified(oldRepository.lastModified); + repository.setPublicReadable(oldRepository.publicReadable); + repository.setArchived(oldRepository.archived); + return repository; + } + + private RepositoryPermission updatePermission(RepositoryPermission repositoryPermission) { + return findMatchingRole(repositoryPermission.getVerbs()) + .map(roleName -> copyRepositoryPermissionWithRole(repositoryPermission, roleName)) + .orElse(repositoryPermission); + } + + private RepositoryPermission copyRepositoryPermissionWithRole(RepositoryPermission repositoryPermission, String roleName) { + return new RepositoryPermission(repositoryPermission.getName(), roleName, repositoryPermission.isGroupPermission()); + } + + private Optional findMatchingRole(Collection verbs) { + return systemRepositoryPermissionProvider.availableRoles() + .stream() + .filter(r -> roleMatchesVerbs(verbs, r)) + .map(RepositoryRole::getName) + .findFirst(); + } + + private boolean roleMatchesVerbs(Collection verbs, RepositoryRole r) { + return verbs.size() == r.getVerbs().size() && r.getVerbs().containsAll(verbs); + } + + @Override + public Version getTargetVersion() { + return Version.parse("1"); + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlRootElement(name = "repositories") + private static class OldRepository { + private String contact; + private Long creationDate; + private String description; + @XmlElement(name = "healthCheckFailure") + @XmlElementWrapper(name = "healthCheckFailures") + private List healthCheckFailures; + private String id; + private Long lastModified; + private String namespace; + private String name; + @XmlElement(name = "permission") + private final Set permissions = new HashSet<>(); + @XmlElement(name = "public") + private boolean publicReadable = false; + private boolean archived = false; + private String type; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/RepositoryUpdates.java b/scm-webapp/src/main/java/sonia/scm/repository/update/RepositoryUpdates.java new file mode 100644 index 0000000000..2c814605c4 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/RepositoryUpdates.java @@ -0,0 +1,10 @@ +package sonia.scm.repository.update; + +public class RepositoryUpdates { + + static class RepositoryUpdateType { + public String getAffectedDataType() { + return "repository"; + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/SystemRepositoryPermissionProvider.java b/scm-webapp/src/main/java/sonia/scm/security/SystemRepositoryPermissionProvider.java index 0350698352..444ee3e941 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/SystemRepositoryPermissionProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/security/SystemRepositoryPermissionProvider.java @@ -25,7 +25,7 @@ import java.util.Set; import static java.util.Collections.unmodifiableCollection; import static java.util.stream.Collectors.toList; -class SystemRepositoryPermissionProvider { +public class SystemRepositoryPermissionProvider { private static final Logger logger = LoggerFactory.getLogger(SystemRepositoryPermissionProvider.class); private static final String REPOSITORY_PERMISSION_DESCRIPTOR = "META-INF/scm/repository-permissions.xml"; diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/MigrateVerbsToPermissionRolesTest.java b/scm-webapp/src/test/java/sonia/scm/repository/update/MigrateVerbsToPermissionRolesTest.java new file mode 100644 index 0000000000..8480bd76d0 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/MigrateVerbsToPermissionRolesTest.java @@ -0,0 +1,76 @@ +package sonia.scm.repository.update; + +import com.google.common.io.Resources; +import org.assertj.core.api.Assertions; +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.repository.RepositoryRole; +import sonia.scm.repository.xml.SingleRepositoryUpdateProcessor; +import sonia.scm.security.SystemRepositoryPermissionProvider; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; + +import static java.util.Arrays.asList; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@ExtendWith(TempDirectory.class) +class MigrateVerbsToPermissionRolesTest { + + private static final String EXISTING_REPOSITORY_ID = "id"; + + @Mock + private SingleRepositoryUpdateProcessor singleRepositoryUpdateProcessor; + @Mock + private SystemRepositoryPermissionProvider systemRepositoryPermissionProvider; + + @InjectMocks + private MigrateVerbsToPermissionRoles migration; + + @BeforeEach + void init(@TempDirectory.TempDir Path tempDir) throws IOException { + URL metadataUrl = Resources.getResource("sonia/scm/repository/update/metadataWithoutRoles.xml"); + Files.copy(metadataUrl.openStream(), tempDir.resolve("metadata.xml")); + doAnswer(invocation -> { + ((BiConsumer) invocation.getArgument(0)).accept(EXISTING_REPOSITORY_ID, tempDir); + return null; + }).when(singleRepositoryUpdateProcessor).doUpdate(any()); + when(systemRepositoryPermissionProvider.availableRoles()).thenReturn(Collections.singletonList(new RepositoryRole("ROLE", asList("read", "write"), ""))); + } + + @Test + void x(@TempDirectory.TempDir Path tempDir) throws IOException { + migration.doUpdate(); + + List newMetadata = Files.readAllLines(tempDir.resolve("metadata.xml")); + Assertions.assertThat(newMetadata.stream().map(String::trim)). + containsSubsequence( + "false", + "user", + "ROLE" + ) + .containsSubsequence( + "true", + "group", + "special" + ) + .doesNotContain( + "read", + "write" + ); + } + +} diff --git a/scm-webapp/src/test/resources/sonia/scm/repository/update/metadataWithoutRoles.xml b/scm-webapp/src/test/resources/sonia/scm/repository/update/metadataWithoutRoles.xml new file mode 100644 index 0000000000..4716943f47 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/repository/update/metadataWithoutRoles.xml @@ -0,0 +1,25 @@ + + + + ich@du.er + 1557729536519 + + B3RQKYNzo2 + 1557825677782 + scmadmin + git + + false + user + read + write + + + true + group + special + + false + false + git +