diff --git a/CHANGELOG.md b/CHANGELOG.md index d4dcc480d1..cd845f009c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed +- Modification for mercurial repositories with enabled XSRF protection + ## 2.0.0-rc4 - 2020-02-14 ### Added - Support for Java versions > 8 diff --git a/scm-webapp/src/main/java/sonia/scm/security/Xsrf.java b/scm-core/src/main/java/sonia/scm/security/Xsrf.java similarity index 94% rename from scm-webapp/src/main/java/sonia/scm/security/Xsrf.java rename to scm-core/src/main/java/sonia/scm/security/Xsrf.java index f9ee8a0872..83e83c80b4 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/Xsrf.java +++ b/scm-core/src/main/java/sonia/scm/security/Xsrf.java @@ -32,15 +32,15 @@ package sonia.scm.security; /** * Shared constants for Xsrf related classes. - * + * * @author Sebastian Sdorra * @since 2.0.0 */ public final class Xsrf { - - static final String HEADER_KEY = "X-XSRF-Token"; - - static final String TOKEN_KEY = "xsrf"; + + public static final String HEADER_KEY = "X-XSRF-Token"; + + public static final String TOKEN_KEY = "xsrf"; private Xsrf() { } diff --git a/scm-plugins/scm-hg-plugin/pom.xml b/scm-plugins/scm-hg-plugin/pom.xml index e57652bf0f..c89d7a085d 100644 --- a/scm-plugins/scm-hg-plugin/pom.xml +++ b/scm-plugins/scm-hg-plugin/pom.xml @@ -27,6 +27,7 @@ + diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java index 1d227fb54e..a9328c1129 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java @@ -38,6 +38,9 @@ package sonia.scm.repository; import com.google.inject.ProvisionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.security.AccessToken; +import sonia.scm.security.CipherUtil; +import sonia.scm.security.Xsrf; import sonia.scm.web.HgUtil; import javax.servlet.http.HttpServletRequest; @@ -65,6 +68,8 @@ public final class HgEnvironment private static final String SCM_BEARER_TOKEN = "SCM_BEARER_TOKEN"; + private static final String SCM_XSRF = "SCM_XSRF"; + //~--- constructors --------------------------------------------------------- /** @@ -114,8 +119,9 @@ public final class HgEnvironment } try { - String credentials = hookManager.getCredentials(); - environment.put(SCM_BEARER_TOKEN, credentials); + AccessToken accessToken = hookManager.getAccessToken(); + environment.put(SCM_BEARER_TOKEN, CipherUtil.getInstance().encode(accessToken.compact())); + extractXsrfKey(environment, accessToken); } catch (ProvisionException e) { LOG.debug("could not create bearer token; looks like currently we are not in a request; probably you can ignore the following exception:", e); } @@ -123,4 +129,8 @@ public final class HgEnvironment environment.put(ENV_URL, hookUrl); environment.put(ENV_CHALLENGE, hookManager.getChallenge()); } + + private static void extractXsrfKey(Map environment, AccessToken accessToken) { + environment.put(SCM_XSRF, accessToken.getCustom(Xsrf.TOKEN_KEY).orElse("-")); + } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java index 6815bdad96..314bd85b57 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java @@ -49,7 +49,6 @@ import sonia.scm.config.ScmConfigurationChangedEvent; import sonia.scm.net.ahc.AdvancedHttpClient; import sonia.scm.security.AccessToken; import sonia.scm.security.AccessTokenBuilderFactory; -import sonia.scm.security.CipherUtil; import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; @@ -196,11 +195,9 @@ public class HgHookManager return this.challenge.equals(challenge); } - public String getCredentials() + public AccessToken getAccessToken() { - AccessToken accessToken = accessTokenBuilderFactory.create().build(); - - return CipherUtil.getInstance().encode(accessToken.compact()); + return accessTokenBuilderFactory.create().build(); } //~--- methods -------------------------------------------------------------- @@ -279,7 +276,7 @@ public class HgHookManager //J- return HttpUtil.getUriWithoutEndSeperator( MoreObjects.firstNonNull( - configuration.getBaseUrl(), + configuration.getBaseUrl(), "http://localhost:8080/scm" ) ).concat("/hook/hg/"); diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py index 637aa16331..ca8d7736a7 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py @@ -41,6 +41,7 @@ import os, urllib, urllib2 baseUrl = os.environ['SCM_URL'] challenge = os.environ['SCM_CHALLENGE'] token = os.environ['SCM_BEARER_TOKEN'] +xsrf = os.environ['SCM_XSRF'] repositoryId = os.environ['SCM_REPOSITORY_ID'] def printMessages(ui, msgs): @@ -59,6 +60,7 @@ def callHookUrl(ui, repo, hooktype, node): proxy_handler = urllib2.ProxyHandler({}) opener = urllib2.build_opener(proxy_handler) req = urllib2.Request(url, data) + req.add_header("X-XSRF-Token", xsrf) conn = opener.open(req) if 200 <= conn.code < 300: ui.debug( "scm-hook " + hooktype + " success with status code " + str(conn.code) + "\n" ) @@ -101,7 +103,7 @@ def preHook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs): # older mercurial versions if pending != None: pending() - + # newer mercurial version # we have to make in-memory changes visible to external process # this does not happen automatically, because mercurial treat our hooks as internal hooks diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgEnvironmentTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgEnvironmentTest.java new file mode 100644 index 0000000000..2718e0b899 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgEnvironmentTest.java @@ -0,0 +1,54 @@ +package sonia.scm.repository; + + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.security.AccessToken; +import sonia.scm.security.Xsrf; + +import java.util.HashMap; +import java.util.Map; + +import static java.util.Optional.empty; +import static java.util.Optional.of; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class HgEnvironmentTest { + + @Mock + HgRepositoryHandler handler; + @Mock + HgHookManager hookManager; + + @Test + void shouldExtractXsrfTokenWhenSet() { + AccessToken accessToken = mock(AccessToken.class); + when(accessToken.compact()).thenReturn(""); + when(accessToken.getCustom(Xsrf.TOKEN_KEY)).thenReturn(of("XSRF Token")); + when(hookManager.getAccessToken()).thenReturn(accessToken); + + Map environment = new HashMap<>(); + HgEnvironment.prepareEnvironment(environment, handler, hookManager); + + assertThat(environment).contains(entry("SCM_XSRF", "XSRF Token")); + } + + @Test + void shouldIgnoreXsrfWhenNotSetButStillContainDummy() { + AccessToken accessToken = mock(AccessToken.class); + when(accessToken.compact()).thenReturn(""); + when(accessToken.getCustom(Xsrf.TOKEN_KEY)).thenReturn(empty()); + when(hookManager.getAccessToken()).thenReturn(accessToken); + + Map environment = new HashMap<>(); + HgEnvironment.prepareEnvironment(environment, handler, hookManager); + + assertThat(environment).containsKeys("SCM_XSRF"); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java index ee5117b276..a5be01465f 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java @@ -38,6 +38,7 @@ package sonia.scm.repository; import org.junit.Assume; import sonia.scm.SCMContext; import sonia.scm.TempDirRepositoryLocationResolver; +import sonia.scm.security.AccessToken; import sonia.scm.store.InMemoryConfigurationStoreFactory; import javax.servlet.http.HttpServletRequest; @@ -107,7 +108,6 @@ public final class HgTestUtil RepositoryLocationResolver repositoryLocationResolver = new TempDirRepositoryLocationResolver(directory); HgRepositoryHandler handler = new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver, null, null); - Path repoDir = directory.toPath(); handler.init(context); return handler; @@ -128,7 +128,9 @@ public final class HgTestUtil "http://localhost:8081/scm/hook/hg/"); when(hookManager.createUrl(any(HttpServletRequest.class))).thenReturn( "http://localhost:8081/scm/hook/hg/"); - when(hookManager.getCredentials()).thenReturn(""); + AccessToken accessToken = mock(AccessToken.class); + when(accessToken.compact()).thenReturn(""); + when(hookManager.getAccessToken()).thenReturn(accessToken); return hookManager; }