From 7323f4776fd0aaf707f076b8baefa3f07837468b Mon Sep 17 00:00:00 2001 From: Igor Vinokur Date: Thu, 26 Jan 2023 15:25:09 +0200 Subject: [PATCH] Apply Bitbucket server url validation by testing an API request (#428) Support factory from public Bitbucket-server repository without any oAuth configuration in the Che side. Add a new check that detects a Bitbucket-server url by testing it by a Bitbucket-server Api request. --- .../security/oauth1/OAuthAuthenticator.java | 4 +- .../bitbucket/BitbucketServerURLParser.java | 30 ++++++++++- .../server/HttpBitbucketServerApiClient.java | 10 ++-- .../BitbucketServerURLParserTest.java | 52 +++++++++++++++++++ 4 files changed, 85 insertions(+), 11 deletions(-) diff --git a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth1/OAuthAuthenticator.java b/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth1/OAuthAuthenticator.java index 73d6a6c58a..863dece3c5 100644 --- a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth1/OAuthAuthenticator.java +++ b/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth1/OAuthAuthenticator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2021 Red Hat, Inc. + * Copyright (c) 2012-2023 Red Hat, Inc. * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ @@ -263,7 +263,7 @@ public abstract class OAuthAuthenticator { return computeAuthorizationHeader( requestMethod, requestUrl, credentials.token, credentials.tokenSecret); } - return null; + return ""; } private OAuthToken getToken(final String userId) { diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerURLParser.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerURLParser.java index 2ebe634c1a..885a51f744 100644 --- a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerURLParser.java +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerURLParser.java @@ -24,15 +24,18 @@ import java.util.regex.Pattern; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; +import org.eclipse.che.api.factory.server.bitbucket.server.HttpBitbucketServerApiClient; import org.eclipse.che.api.factory.server.scm.PersonalAccessToken; import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager; import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException; import org.eclipse.che.api.factory.server.scm.exception.ScmConfigurationPersistenceException; +import org.eclipse.che.api.factory.server.scm.exception.ScmItemNotFoundException; import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException; import org.eclipse.che.api.factory.server.urlfactory.DevfileFilenamesProvider; import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.commons.env.EnvironmentContext; import org.eclipse.che.commons.lang.StringUtils; +import org.eclipse.che.security.oauth1.BitbucketServerOAuthAuthenticator; /** * Parser of String Bitbucket Server URLs and provide {@link BitbucketServerUrl} objects. @@ -90,16 +93,39 @@ public class BitbucketServerURLParser { if (!bitbucketUrlPatterns.isEmpty()) { return bitbucketUrlPatterns.stream().anyMatch(pattern -> pattern.matcher(url).matches()); } else { + return // If Bitbucket server URL is not configured try to find it in a manually added user namespace // token. - return isUserTokenPresent(url); + isUserTokenPresent(url) + // Try to call an API request to see if the URL matches Bitbucket. + || isApiRequestRelevant(url); } } + private boolean isApiRequestRelevant(String repositoryUrl) { + try { + HttpBitbucketServerApiClient bitbucketServerApiClient = + new HttpBitbucketServerApiClient( + getServerUrl(repositoryUrl), new BitbucketServerOAuthAuthenticator("", "", "", "")); + // If the token request catches the unauthorised error, it means that the provided url + // belongs to Bitbucket. + bitbucketServerApiClient.getPersonalAccessToken("", 0L); + } catch (ScmItemNotFoundException | ScmCommunicationException e) { + return false; + } catch (ScmUnauthorizedException e) { + return true; + } + return false; + } + private String getServerUrl(String repositoryUrl) { return repositoryUrl.substring( 0, - repositoryUrl.indexOf("/scm") > 0 ? repositoryUrl.indexOf("/scm") : repositoryUrl.length()); + repositoryUrl.indexOf("/scm") > 0 + ? repositoryUrl.indexOf("/scm") + : repositoryUrl.indexOf("/users") > 0 + ? repositoryUrl.indexOf("/users") + : repositoryUrl.length()); } private Optional getPatternMatcherByUrl(String url) { diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/HttpBitbucketServerApiClient.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/HttpBitbucketServerApiClient.java index 6b55797276..e4071e96b9 100644 --- a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/HttpBitbucketServerApiClient.java +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/HttpBitbucketServerApiClient.java @@ -400,15 +400,11 @@ public class HttpBitbucketServerApiClient implements BitbucketServerApiClient { } private String computeAuthorizationHeader(String requestMethod, String requestUrl) - throws ScmUnauthorizedException, ScmCommunicationException { + throws ScmCommunicationException { try { Subject subject = EnvironmentContext.getCurrent().getSubject(); - String authorizationHeader = - authenticator.computeAuthorizationHeader(subject.getUserId(), requestMethod, requestUrl); - if (Strings.isNullOrEmpty(authorizationHeader)) { - throw buildScmUnauthorizedException(); - } - return authorizationHeader; + return authenticator.computeAuthorizationHeader( + subject.getUserId(), requestMethod, requestUrl); } catch (OAuthAuthenticationException e) { throw new ScmCommunicationException(e.getMessage(), e); } diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerURLParserTest.java b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerURLParserTest.java index 2a4aa55007..5a70069e31 100644 --- a/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerURLParserTest.java +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerURLParserTest.java @@ -11,14 +11,24 @@ */ package org.eclipse.che.api.factory.server.bitbucket; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.mockito.Mockito.mock; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.common.Slf4jNotifier; import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager; import org.eclipse.che.api.factory.server.urlfactory.DevfileFilenamesProvider; import org.mockito.Mock; import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Listeners; @@ -32,6 +42,17 @@ public class BitbucketServerURLParserTest { /** Instance of component that will be tested. */ private BitbucketServerURLParser bitbucketURLParser; + private WireMockServer wireMockServer; + + @BeforeClass + public void prepare() { + wireMockServer = + new WireMockServer(wireMockConfig().notifier(new Slf4jNotifier(false)).dynamicPort()); + wireMockServer.start(); + WireMock.configureFor("localhost", wireMockServer.port()); + new WireMock("localhost", wireMockServer.port()); + } + @BeforeMethod public void setUp() { bitbucketURLParser = @@ -65,6 +86,37 @@ public class BitbucketServerURLParserTest { bitbucketURLParser.parse("https://github.com/org/repo"); } + @Test + public void shouldValidateUrlByApiRequest() { + // given + bitbucketURLParser = + new BitbucketServerURLParser( + null, devfileFilenamesProvider, mock(PersonalAccessTokenManager.class)); + String url = wireMockServer.url("/users/user/repos/repo"); + stubFor( + get(urlEqualTo("/rest/access-tokens/1.0/users/0")).willReturn(aResponse().withStatus(401))); + + // when + boolean result = bitbucketURLParser.isValid(url); + + // then + assertTrue(result); + } + + @Test + public void shouldNotValidateUrlByApiRequest() { + // given + String url = wireMockServer.url("/users/user/repos/repo"); + stubFor( + get(urlEqualTo("/rest/access-tokens/1.0/users/0")).willReturn(aResponse().withStatus(500))); + + // when + boolean result = bitbucketURLParser.isValid(url); + + // then + assertFalse(result); + } + @DataProvider(name = "UrlsProvider") public Object[][] urls() { return new Object[][] {