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.
pull/430/head
Igor Vinokur 2023-01-26 15:25:09 +02:00 committed by GitHub
parent 9a003e2351
commit 7323f4776f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 85 additions and 11 deletions

View File

@ -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 * This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0 * available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/ * which is available at https://www.eclipse.org/legal/epl-2.0/
@ -263,7 +263,7 @@ public abstract class OAuthAuthenticator {
return computeAuthorizationHeader( return computeAuthorizationHeader(
requestMethod, requestUrl, credentials.token, credentials.tokenSecret); requestMethod, requestUrl, credentials.token, credentials.tokenSecret);
} }
return null; return "";
} }
private OAuthToken getToken(final String userId) { private OAuthToken getToken(final String userId) {

View File

@ -24,15 +24,18 @@ import java.util.regex.Pattern;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton; 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.PersonalAccessToken;
import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager; 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.ScmCommunicationException;
import org.eclipse.che.api.factory.server.scm.exception.ScmConfigurationPersistenceException; 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.scm.exception.ScmUnauthorizedException;
import org.eclipse.che.api.factory.server.urlfactory.DevfileFilenamesProvider; import org.eclipse.che.api.factory.server.urlfactory.DevfileFilenamesProvider;
import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.commons.env.EnvironmentContext; import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.lang.StringUtils; 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. * Parser of String Bitbucket Server URLs and provide {@link BitbucketServerUrl} objects.
@ -90,16 +93,39 @@ public class BitbucketServerURLParser {
if (!bitbucketUrlPatterns.isEmpty()) { if (!bitbucketUrlPatterns.isEmpty()) {
return bitbucketUrlPatterns.stream().anyMatch(pattern -> pattern.matcher(url).matches()); return bitbucketUrlPatterns.stream().anyMatch(pattern -> pattern.matcher(url).matches());
} else { } else {
return
// If Bitbucket server URL is not configured try to find it in a manually added user namespace // If Bitbucket server URL is not configured try to find it in a manually added user namespace
// token. // 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) { private String getServerUrl(String repositoryUrl) {
return repositoryUrl.substring( return repositoryUrl.substring(
0, 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<Matcher> getPatternMatcherByUrl(String url) { private Optional<Matcher> getPatternMatcherByUrl(String url) {

View File

@ -400,15 +400,11 @@ public class HttpBitbucketServerApiClient implements BitbucketServerApiClient {
} }
private String computeAuthorizationHeader(String requestMethod, String requestUrl) private String computeAuthorizationHeader(String requestMethod, String requestUrl)
throws ScmUnauthorizedException, ScmCommunicationException { throws ScmCommunicationException {
try { try {
Subject subject = EnvironmentContext.getCurrent().getSubject(); Subject subject = EnvironmentContext.getCurrent().getSubject();
String authorizationHeader = return authenticator.computeAuthorizationHeader(
authenticator.computeAuthorizationHeader(subject.getUserId(), requestMethod, requestUrl); subject.getUserId(), requestMethod, requestUrl);
if (Strings.isNullOrEmpty(authorizationHeader)) {
throw buildScmUnauthorizedException();
}
return authorizationHeader;
} catch (OAuthAuthenticationException e) { } catch (OAuthAuthenticationException e) {
throw new ScmCommunicationException(e.getMessage(), e); throw new ScmCommunicationException(e.getMessage(), e);
} }

View File

@ -11,14 +11,24 @@
*/ */
package org.eclipse.che.api.factory.server.bitbucket; 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.mockito.Mockito.mock;
import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue; 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.scm.PersonalAccessTokenManager;
import org.eclipse.che.api.factory.server.urlfactory.DevfileFilenamesProvider; import org.eclipse.che.api.factory.server.urlfactory.DevfileFilenamesProvider;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener; import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider; import org.testng.annotations.DataProvider;
import org.testng.annotations.Listeners; import org.testng.annotations.Listeners;
@ -32,6 +42,17 @@ public class BitbucketServerURLParserTest {
/** Instance of component that will be tested. */ /** Instance of component that will be tested. */
private BitbucketServerURLParser bitbucketURLParser; 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 @BeforeMethod
public void setUp() { public void setUp() {
bitbucketURLParser = bitbucketURLParser =
@ -65,6 +86,37 @@ public class BitbucketServerURLParserTest {
bitbucketURLParser.parse("https://github.com/org/repo"); 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") @DataProvider(name = "UrlsProvider")
public Object[][] urls() { public Object[][] urls() {
return new Object[][] { return new Object[][] {