diff --git a/CHANGELOG.md b/CHANGELOG.md index a83e5b743b..9b96df8010 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Set individual page title - Copy on write - A new repository can be initialized with a branch (for git and mercurial) and custom files (README.md on default) +- Plugins are validated directly after download ### Changed - Stop fetching commits when it takes too long @@ -20,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Subversion revision 0 leads to error - Create mock subject to satisfy legman - Multiple versions of hibernate-validator caused problems when starting from plugins +- Page title is now set correctly ## 2.0.0-rc1 - 2019-12-02 ### Added diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModifyCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModifyCommand.java index 8475cf7c48..cfb1242fba 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModifyCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModifyCommand.java @@ -1,10 +1,12 @@ package sonia.scm.repository.spi; +import org.apache.shiro.SecurityUtils; import org.tmatesoft.svn.core.SVNCommitInfo; import org.tmatesoft.svn.core.SVNDepth; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.wc.SVNClientManager; import org.tmatesoft.svn.core.wc.SVNWCClient; +import org.tmatesoft.svn.core.wc.SVNWCUtil; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; import sonia.scm.repository.SvnWorkDirFactory; @@ -38,6 +40,7 @@ public class SvnModifyCommand implements ModifyCommand { private String commitChanges(SVNClientManager clientManager, File workingDirectory, String commitMessage) { try { + clientManager.setAuthenticationManager(SVNWCUtil.createDefaultAuthenticationManager(getCurrentUserName(), new char[0])); SVNCommitInfo svnCommitInfo = clientManager.getCommitClient().doCommit( new File[]{workingDirectory}, false, @@ -54,6 +57,14 @@ public class SvnModifyCommand implements ModifyCommand { } } + private String getCurrentUserName() { + if (SecurityUtils.getSubject() != null && SecurityUtils.getSubject().getPrincipal() != null) { + return SecurityUtils.getSubject().getPrincipal().toString(); + } else { + return "SCM-Manager"; + } + } + private void modifyWorkingDirectory(ModifyCommandRequest request, SVNClientManager clientManager, File workingDirectory) { for (ModifyCommandRequest.PartialRequest partialRequest : request.getRequests()) { try { diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnModifyCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnModifyCommandTest.java index 456971d9a2..efd51fb890 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnModifyCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnModifyCommandTest.java @@ -1,5 +1,8 @@ package sonia.scm.repository.spi; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -14,6 +17,8 @@ import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class SvnModifyCommandTest extends AbstractSvnCommandTestBase { @@ -31,6 +36,18 @@ public class SvnModifyCommandTest extends AbstractSvnCommandTestBase { svnModifyCommand = new SvnModifyCommand(context, createRepository(), workDirFactory); } + @Before + public void initSecurityManager() { + Subject subject = mock(Subject.class); + when(subject.getPrincipal()).thenReturn("alThor"); + ThreadContext.bind(subject); + } + + @After + public void cleanUpSecurityManager() { + ThreadContext.unbindSubject(); + } + @Test public void shouldRemoveFiles() { ModifyCommandRequest request = new ModifyCommandRequest(); diff --git a/scm-ui/ui-components/src/layout/Page.tsx b/scm-ui/ui-components/src/layout/Page.tsx index 472acc0410..128337fd73 100644 --- a/scm-ui/ui-components/src/layout/Page.tsx +++ b/scm-ui/ui-components/src/layout/Page.tsx @@ -28,6 +28,13 @@ const PageActionContainer = styled.div` `; export default class Page extends React.Component { + componentDidUpdate() { + const { title } = this.props; + if (title && title !== document.title) { + document.title = title; + } + } + render() { const { error } = this.props; return ( diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstaller.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstaller.java index 6f003c1e31..082058d663 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstaller.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstaller.java @@ -19,11 +19,13 @@ class PluginInstaller { private final SCMContextProvider context; private final AdvancedHttpClient client; + private final SmpDescriptorExtractor smpDescriptorExtractor; @Inject - public PluginInstaller(SCMContextProvider context, AdvancedHttpClient client) { + public PluginInstaller(SCMContextProvider context, AdvancedHttpClient client, SmpDescriptorExtractor smpDescriptorExtractor) { this.context = context; this.client = client; + this.smpDescriptorExtractor = smpDescriptorExtractor; } @SuppressWarnings("squid:S4790") // hashing should be safe @@ -34,6 +36,7 @@ class PluginInstaller { Files.copy(input, file); verifyChecksum(plugin, input.hash(), file); + verifyConditions(plugin, file); return new PendingPluginInstallation(plugin.install(), file); } catch (IOException ex) { cleanup(file); @@ -64,6 +67,20 @@ class PluginInstaller { } } + private void verifyConditions(AvailablePlugin plugin, Path file) throws IOException { + InstalledPluginDescriptor pluginDescriptor = smpDescriptorExtractor.extractPluginDescriptor(file); + if (!pluginDescriptor.getCondition().isSupported()) { + cleanup(file); + throw new PluginConditionFailedException( + pluginDescriptor.getCondition(), + String.format( + "could not load plugin %s, the plugin condition does not match", + plugin.getDescriptor().getInformation().getName() + ) + ); + } + } + private InputStream download(AvailablePlugin plugin) throws IOException { return client.get(plugin.getDescriptor().getUrl()).request().contentAsStream(); } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/SmpDescriptorExtractor.java b/scm-webapp/src/main/java/sonia/scm/plugin/SmpDescriptorExtractor.java new file mode 100644 index 0000000000..b8e9cddabd --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/SmpDescriptorExtractor.java @@ -0,0 +1,28 @@ +package sonia.scm.plugin; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +class SmpDescriptorExtractor { + + InstalledPluginDescriptor extractPluginDescriptor(Path file) throws IOException { + try (ZipInputStream zipInputStream = new ZipInputStream(Files.newInputStream(file), StandardCharsets.UTF_8)) { + ZipEntry nextEntry; + while ((nextEntry = zipInputStream.getNextEntry()) != null) { + if ("META-INF/scm/plugin.xml".equals(nextEntry.getName())) { + JAXBContext context = JAXBContext.newInstance(ScmModule.class, InstalledPluginDescriptor.class); + return (InstalledPluginDescriptor) context.createUnmarshaller().unmarshal(zipInputStream); + } + } + } catch (JAXBException e) { + throw new IOException("failed to read descriptor file META-INF/scm/plugin.xml from plugin", e); + } + throw new IOException("Missing plugin descriptor META-INF/scm/plugin.xml in download package"); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallerTest.java index 3f918cd4fa..fbc22e1ebd 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallerTest.java @@ -32,18 +32,23 @@ class PluginInstallerTest { @Mock(answer = Answers.RETURNS_DEEP_STUBS) private AdvancedHttpClient client; + @Mock + private SmpDescriptorExtractor extractor; + @InjectMocks private PluginInstaller installer; private Path directory; @BeforeEach - void setUpContext(@TempDirectory.TempDir Path directory) { + void setUpContext(@TempDirectory.TempDir Path directory) throws IOException { this.directory = directory; lenient().when(context.resolve(any())).then(ic -> { Path arg = ic.getArgument(0); return directory.resolve(arg); }); + InstalledPluginDescriptor supportedPlugin = createPluginDescriptor(true); + lenient().when(extractor.extractPluginDescriptor(any())).thenReturn(supportedPlugin); } @Test @@ -105,6 +110,15 @@ class PluginInstallerTest { assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist(); } + @Test + void shouldFailForUnsupportedPlugin() throws IOException { + mockContent("42"); + InstalledPluginDescriptor supportedPlugin = createPluginDescriptor(false); + when(extractor.extractPluginDescriptor(any())).thenReturn(supportedPlugin); + + assertThrows(PluginConditionFailedException.class, () -> installer.install(createGitPlugin())); + assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist(); + } private AvailablePlugin createPlugin(String name, String url, String checksum) { PluginInformation information = new PluginInformation(); @@ -114,4 +128,11 @@ class PluginInstallerTest { ); return new AvailablePlugin(descriptor); } + + private InstalledPluginDescriptor createPluginDescriptor(boolean supported) { + InstalledPluginDescriptor installedPluginDescriptor = mock(InstalledPluginDescriptor.class, RETURNS_DEEP_STUBS); + lenient().when(installedPluginDescriptor.getCondition().isSupported()).thenReturn(supported); + lenient().when(installedPluginDescriptor.getInformation().getId()).thenReturn("scm-git-plugin"); + return installedPluginDescriptor; + } } diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/SmpDescriptorExtractorTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/SmpDescriptorExtractorTest.java new file mode 100644 index 0000000000..6ca19a3692 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/SmpDescriptorExtractorTest.java @@ -0,0 +1,76 @@ +package sonia.scm.plugin; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ExtendWith(TempDirectory.class) +class SmpDescriptorExtractorTest { + + private static final String PLUGIN_XML = "\n" + + "\n" + + "\n" + + " 2\n" + + "\n" + + " \n" + + " Test\n" + + " Cloudogu GmbH\n" + + " Testing\n" + + " scm-test-plugin\n" + + "2.0.0\n" + + "Collects information for support cases\n" + + "\n" + + "\n" + + " \n" + + " 2.0.0-rc1\n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + "\n" + + "\n"; + + @Test + void shouldExtractPluginXml(@TempDirectory.TempDir Path tempDir) throws IOException { + Path pluginFile = createZipFile(tempDir, "META-INF/scm/plugin.xml", PLUGIN_XML); + + InstalledPluginDescriptor installedPluginDescriptor = new SmpDescriptorExtractor().extractPluginDescriptor(pluginFile); + + Assertions.assertThat(installedPluginDescriptor.getInformation().getName()).isEqualTo("scm-test-plugin"); + } + + @Test + void shouldFailWithoutPluginXml(@TempDirectory.TempDir Path tempDir) throws IOException { + Path pluginFile = createZipFile(tempDir, "META-INF/wrong/plugin.xml", PLUGIN_XML); + + assertThrows(IOException.class, () -> new SmpDescriptorExtractor().extractPluginDescriptor(pluginFile)); + } + + @Test + void shouldFailWithIllegalPluginXml(@TempDirectory.TempDir Path tempDir) throws IOException { + Path pluginFile = createZipFile(tempDir, "META-INF/scm/plugin.xml", "content"); + + assertThrows(IOException.class, () -> new SmpDescriptorExtractor().extractPluginDescriptor(pluginFile)); + } + + Path createZipFile(Path tempDir, String internalFileName, String content) throws IOException { + Path pluginFile = tempDir.resolve("scm-test-plugin.smp"); + ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(pluginFile), UTF_8); + zipOutputStream.putNextEntry(new ZipEntry(internalFileName)); + zipOutputStream.write(content.getBytes(UTF_8)); + zipOutputStream.closeEntry(); + zipOutputStream.close(); + return pluginFile; + } +}