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
parent
9a003e2351
commit
7323f4776f
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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[][] {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue