From 672dbb2b9831892c5359753e4c616ba577c003b7 Mon Sep 17 00:00:00 2001 From: ivinokur Date: Thu, 16 Jan 2025 17:32:40 +0200 Subject: [PATCH] commit --- ...AzureDevOpsPersonalAccessTokenFetcher.java | 15 +- .../devops/AzureDevOpsServerApiClient.java | 149 ++++++++++++++++++ .../devops/AzureDevOpsServerUserIdentity.java | 36 +++++ .../devops/AzureDevOpsServerUserProfile.java | 34 ++++ .../azure/devops/AzureDevOpsURLParser.java | 125 ++++++++++++--- .../server/azure/devops/AzureDevOpsUrl.java | 9 +- ...eDevOpsPersonalAccessTokenFetcherTest.java | 95 +++++++++++ .../devops/AzureDevOpsURLParserTest.java | 6 +- .../azure/devops/AzureDevOpsURLTest.java | 6 +- .../rest/user/response.json | 5 + .../rest/user/email/response.json | 3 + .../azure-devops/rest/user/response.json | 9 ++ ...tbucketPersonalAccessTokenFetcherTest.java | 2 +- .../gitlab/AbstractGitlabUrlParser.java | 3 +- 14 files changed, 468 insertions(+), 29 deletions(-) create mode 100644 wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsServerApiClient.java create mode 100644 wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsServerUserIdentity.java create mode 100644 wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsServerUserProfile.java create mode 100644 wsmaster/che-core-api-factory-azure-devops/src/test/resources/__files/azure-devops-server/rest/user/response.json create mode 100644 wsmaster/che-core-api-factory-azure-devops/src/test/resources/__files/azure-devops/rest/user/email/response.json create mode 100644 wsmaster/che-core-api-factory-azure-devops/src/test/resources/__files/azure-devops/rest/user/response.json diff --git a/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsPersonalAccessTokenFetcher.java b/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsPersonalAccessTokenFetcher.java index 2c671c33ac7..f58c4a3c4a9 100644 --- a/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsPersonalAccessTokenFetcher.java +++ b/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsPersonalAccessTokenFetcher.java @@ -172,8 +172,19 @@ public Optional isValid(PersonalAccessToken personalAccessToken) { public Optional> isValid(PersonalAccessTokenParams params) throws ScmCommunicationException { if (!isValidScmServerUrl(params.getScmProviderUrl())) { - LOG.debug("not a valid url {} for current fetcher ", params.getScmProviderUrl()); - return Optional.empty(); + if (OAUTH_PROVIDER_NAME.equals(params.getScmProviderName())) { + AzureDevOpsServerApiClient azureDevOpsServerApiClient = + new AzureDevOpsServerApiClient(params.getScmProviderUrl(), params.getOrganization()); + try { + AzureDevOpsServerUserProfile user = azureDevOpsServerApiClient.getUser(params.getToken()); + return Optional.of(Pair.of(Boolean.TRUE, user.getIdentity().getAccountName())); + } catch (ScmItemNotFoundException | ScmBadRequestException e) { + return Optional.empty(); + } + } else { + LOG.debug("not a valid url {} for current fetcher ", params.getScmProviderUrl()); + return Optional.empty(); + } } try { diff --git a/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsServerApiClient.java b/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsServerApiClient.java new file mode 100644 index 00000000000..77412e68aa8 --- /dev/null +++ b/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsServerApiClient.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2012-2024 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/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.api.factory.server.azure.devops; + +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; +import static java.net.HttpURLConnection.HTTP_NO_CONTENT; +import static java.net.HttpURLConnection.HTTP_OK; +import static java.time.Duration.ofSeconds; +import static org.eclipse.che.api.factory.server.azure.devops.AzureDevOps.formatAuthorizationHeader; +import static org.eclipse.che.commons.lang.StringUtils.trimEnd; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Charsets; +import com.google.common.io.CharStreams; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Base64; +import java.util.concurrent.Executors; +import java.util.function.Function; +import org.eclipse.che.api.factory.server.scm.exception.ScmBadRequestException; +import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException; +import org.eclipse.che.api.factory.server.scm.exception.ScmItemNotFoundException; +import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Azure DevOps Service API operations helper. */ +public class AzureDevOpsServerApiClient { + + private static final Logger LOG = LoggerFactory.getLogger(AzureDevOpsServerApiClient.class); + + private final HttpClient httpClient; + private final String azureDevOpsServerApiEndpoint; + private final String azureDevOpsServerCollection; + private static final Duration DEFAULT_HTTP_TIMEOUT = ofSeconds(10); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + public AzureDevOpsServerApiClient( + String azureDevOpsServerApiEndpoint, String azureDevOpsServerCollection) { + this.azureDevOpsServerApiEndpoint = trimEnd(azureDevOpsServerApiEndpoint, '/'); + this.azureDevOpsServerCollection = azureDevOpsServerCollection; + this.httpClient = + HttpClient.newBuilder() + .executor( + Executors.newCachedThreadPool( + new ThreadFactoryBuilder() + .setUncaughtExceptionHandler(LoggingUncaughtExceptionHandler.getInstance()) + .setNameFormat(AzureDevOpsServerApiClient.class.getName() + "-%d") + .setDaemon(true) + .build())) + .connectTimeout(DEFAULT_HTTP_TIMEOUT) + .version(HttpClient.Version.HTTP_1_1) + .build(); + } + + /** + * Returns the user associated with the provided PAT. The difference from {@code + * getUserWithOAuthToken} is in authorization header and the fact that PAT is associated with + * organization. + */ + public AzureDevOpsServerUserProfile getUser(String token) + throws ScmItemNotFoundException, ScmCommunicationException, ScmBadRequestException { + final String url = + String.format( + "%s/%s/_api/_common/GetUserProfile", + azureDevOpsServerApiEndpoint, azureDevOpsServerCollection); + return getUser(url, formatAuthorizationHeader(token)); + } + + /** The authorization request varies depending on the type of token. */ + private static String formatAuthorizationHeader(String token) { + return "Basic " + + Base64.getEncoder().encodeToString((":" + token).getBytes(StandardCharsets.UTF_8)); + } + + private AzureDevOpsServerUserProfile getUser(String url, String authorizationHeader) + throws ScmItemNotFoundException, ScmCommunicationException, ScmBadRequestException { + final HttpRequest userDataRequest = + HttpRequest.newBuilder(URI.create(url)) + .headers("Authorization", authorizationHeader) + .timeout(DEFAULT_HTTP_TIMEOUT) + .build(); + + LOG.trace("executeRequest={}", userDataRequest); + return executeRequest( + httpClient, + userDataRequest, + response -> { + try { + String result = + CharStreams.toString(new InputStreamReader(response.body(), Charsets.UTF_8)); + return OBJECT_MAPPER.readValue(result, AzureDevOpsServerUserProfile.class); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + + private T executeRequest( + HttpClient httpClient, + HttpRequest request, + Function, T> responseConverter) + throws ScmBadRequestException, ScmItemNotFoundException, ScmCommunicationException { + try { + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); + LOG.trace("executeRequest={} response {}", request, response.statusCode()); + if (response.statusCode() == HTTP_OK) { + return responseConverter.apply(response); + } else if (response.statusCode() == HTTP_NO_CONTENT) { + return null; + } else { + String body = CharStreams.toString(new InputStreamReader(response.body(), Charsets.UTF_8)); + switch (response.statusCode()) { + case HTTP_BAD_REQUEST: + throw new ScmBadRequestException(body); + case HTTP_NOT_FOUND: + throw new ScmItemNotFoundException(body); + default: + throw new ScmCommunicationException( + "Unexpected status code " + response.statusCode() + " " + response, + response.statusCode(), + "azure-devops"); + } + } + } catch (IOException | InterruptedException | UncheckedIOException e) { + throw new ScmCommunicationException(e.getMessage(), e); + } + } +} diff --git a/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsServerUserIdentity.java b/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsServerUserIdentity.java new file mode 100644 index 00000000000..3622c23e1eb --- /dev/null +++ b/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsServerUserIdentity.java @@ -0,0 +1,36 @@ +/* + * 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/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +package org.eclipse.che.api.factory.server.azure.devops; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** Azure DevOps Server user's identity. */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class AzureDevOpsServerUserIdentity { + private String accountName; + + public String getAccountName() { + return accountName; + } + + @JsonProperty("AccountName") + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + @Override + public String toString() { + return "AzureDevOpsServerUserIdentity{" + "accountName='" + accountName + '\'' + '}'; + } +} diff --git a/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsServerUserProfile.java b/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsServerUserProfile.java new file mode 100644 index 00000000000..5b3edb90c83 --- /dev/null +++ b/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsServerUserProfile.java @@ -0,0 +1,34 @@ +/* + * 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/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +package org.eclipse.che.api.factory.server.azure.devops; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** Azure DevOps Server user's profile. */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class AzureDevOpsServerUserProfile { + private AzureDevOpsServerUserIdentity identity; + + public AzureDevOpsServerUserIdentity getIdentity() { + return identity; + } + + public void setIdentity(AzureDevOpsServerUserIdentity identity) { + this.identity = identity; + } + + @Override + public String toString() { + return "AzureDevOpsServerUserProfile{" + "identity=" + identity + '}'; + } +} diff --git a/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsURLParser.java b/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsURLParser.java index b3e5a4e3774..6b416b48a59 100644 --- a/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsURLParser.java +++ b/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsURLParser.java @@ -17,12 +17,19 @@ import static org.eclipse.che.commons.lang.StringUtils.trimEnd; import jakarta.validation.constraints.NotNull; +import java.net.URI; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; +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.urlfactory.DevfileFilenamesProvider; +import org.eclipse.che.commons.env.EnvironmentContext; /** * Parser of String Azure DevOps URLs and provide {@link AzureDevOpsUrl} objects. @@ -33,6 +40,7 @@ public class AzureDevOpsURLParser { private final DevfileFilenamesProvider devfileFilenamesProvider; + private final PersonalAccessTokenManager tokenManager; private final String azureDevOpsScmApiEndpointHost; /** * Regexp to find repository details (repository name, organization name and branch or tag) @@ -41,45 +49,116 @@ public class AzureDevOpsURLParser { private final Pattern azureDevOpsPattern; private final Pattern azureSSHDevOpsPattern; + private final String azureSSHDevOpsPatternTemplate = + "^git@ssh\\.%s:v3/(?.*)/(?.*)/(?.*)$"; + private final String azureSSHDevOpsServerPatternTemplate = + "^ssh://\\.%s(:d*)?/(?.*)/(?.*)/_git/(?.*)$"; + private final String azureDevOpsPatternTemplate = + "^https?://(?[^@]++)?@?%s/(?[^/]++)/((?[^/]++)/)?_git/" + + "(?[^?]++)" + + "([?&]path=(?[^&]++))?" + + "([?&]version=GT(?[^&]++))?" + + "([?&]version=GB(?[^&]++))?" + + "(.*)"; + private static final String PROVIDER_NAME = "azure-devops"; @Inject public AzureDevOpsURLParser( DevfileFilenamesProvider devfileFilenamesProvider, + PersonalAccessTokenManager tokenManager, @Named("che.integration.azure.devops.scm.api_endpoint") String azureDevOpsScmApiEndpoint) { this.devfileFilenamesProvider = devfileFilenamesProvider; + this.tokenManager = tokenManager; this.azureDevOpsScmApiEndpointHost = - trimEnd(azureDevOpsScmApiEndpoint, '/').substring("https://".length()); + trimEnd(azureDevOpsScmApiEndpoint, '/').replaceFirst("https?://", ""); this.azureDevOpsPattern = - compile( - format( - "^https://(?[^@]++)?@?%s/(?[^/]++)/((?[^/]++)/)?_git/" - + "(?[^?]++)" - + "([?&]path=(?[^&]++))?" - + "([?&]version=GT(?[^&]++))?" - + "([?&]version=GB(?[^&]++))?" - + "(.*)", - azureDevOpsScmApiEndpointHost)); + compile(format(azureDevOpsPatternTemplate, azureDevOpsScmApiEndpointHost)); this.azureSSHDevOpsPattern = - compile( - format( - "^git@ssh\\.%s:v3/(?.*)/(?.*)/(?.*)$", - azureDevOpsScmApiEndpointHost)); + compile(format(azureSSHDevOpsPatternTemplate, azureDevOpsScmApiEndpointHost)); } public boolean isValid(@NotNull String url) { - return azureDevOpsPattern.matcher(url).matches() - || azureSSHDevOpsPattern.matcher(url).matches(); + String trimmedUrl = trimEnd(url, '/'); + return azureDevOpsPattern.matcher(trimmedUrl).matches() + || azureSSHDevOpsPattern.matcher(trimmedUrl).matches() + // Check whether PAT is configured for the GitHub server URL. It is sufficient to confirm + // that the URL is a valid GitHub URL. + || isUserTokenPresent(trimmedUrl); + } + + // Try to find the given url in a manually added user namespace token secret. + private boolean isUserTokenPresent(String repositoryUrl) { + Optional serverUrlOptional = getServerUrl(repositoryUrl); + if (serverUrlOptional.isPresent()) { + String serverUrl = serverUrlOptional.get(); + try { + Optional token = + tokenManager.get(EnvironmentContext.getCurrent().getSubject(), serverUrl); + if (token.isPresent()) { + PersonalAccessToken accessToken = token.get(); + return accessToken.getScmTokenName().equals(PROVIDER_NAME); + } + } catch (ScmConfigurationPersistenceException | ScmCommunicationException exception) { + return false; + } + } + return false; + } + + private Optional getServerUrl(String repositoryUrl) { + // If the given repository url is an SSH url, generate the base url from the pattern: + // https://. + if (repositoryUrl.startsWith("git@")) { + String substring = repositoryUrl.substring(4); + return Optional.of("https://" + substring.substring(0, substring.indexOf(":"))); + } + // Otherwise, extract the base url from the given repository url by cutting the url after the + // first slash. + Matcher serverUrlMatcher = compile("[^/|:]/").matcher(repositoryUrl); + if (serverUrlMatcher.find()) { + return Optional.of( + repositoryUrl.substring(0, repositoryUrl.indexOf(serverUrlMatcher.group()) + 1)); + } + return Optional.empty(); + } + + private Optional getPatternMatcherByUrl(String url) { + String host = URI.create(url).getHost(); + Matcher matcher = compile(format(azureDevOpsPatternTemplate, host)).matcher(url); + if (matcher.matches()) { + return Optional.of(matcher); + } else { + matcher = compile(format(azureSSHDevOpsPatternTemplate, host)).matcher(url); + if (matcher.matches()) { + return Optional.of(matcher); + } else { + matcher = compile(format(azureSSHDevOpsServerPatternTemplate, host)).matcher(url); + } + return matcher.matches() ? Optional.of(matcher) : Optional.empty(); + } + } + + private IllegalArgumentException buildIllegalArgumentException(String url) { + return new IllegalArgumentException( + format("The given url %s is not a valid Azure DevOps URL. ", url)); } public AzureDevOpsUrl parse(String url) { + Matcher matcher; boolean isHTTPSUrl = azureDevOpsPattern.matcher(url).matches(); - Matcher matcher = - isHTTPSUrl ? azureDevOpsPattern.matcher(url) : azureSSHDevOpsPattern.matcher(url); + if (isHTTPSUrl) { + matcher = azureDevOpsPattern.matcher(url); + } else if (azureSSHDevOpsPattern.matcher(url).matches()) { + matcher = azureSSHDevOpsPattern.matcher(url); + } else { + matcher = getPatternMatcherByUrl(url).orElseThrow(() -> buildIllegalArgumentException(url)); + isHTTPSUrl = url.startsWith("http"); + } if (!matcher.matches()) { - throw new IllegalArgumentException(format("The given url %s is not a valid.", url)); + throw buildIllegalArgumentException(url); } - + String serverUrl = getServerUrl(url).orElseThrow(() -> buildIllegalArgumentException(url)); String repoName = matcher.group("repoName"); String project = matcher.group("project"); if (project == null) { @@ -93,6 +172,7 @@ public AzureDevOpsUrl parse(String url) { String tag = null; String organization = matcher.group("organization"); + String newUrl = url; if (isHTTPSUrl) { branch = matcher.group("branch"); tag = matcher.group("tag"); @@ -104,7 +184,7 @@ public AzureDevOpsUrl parse(String url) { // TODO: return empty credentials like the BitBucketUrl String organizationCanIgnore = matcher.group("organizationCanIgnore"); if (!isNullOrEmpty(organization) && organization.equals(organizationCanIgnore)) { - url = url.replace(organizationCanIgnore + "@", ""); + newUrl = newUrl.replace(organizationCanIgnore + "@", ""); } } @@ -117,6 +197,7 @@ public AzureDevOpsUrl parse(String url) { .withBranch(branch) .withTag(tag) .withDevfileFilenames(devfileFilenamesProvider.getConfiguredDevfileFilenames()) - .withUrl(url); + .withServerUrl(serverUrl) + .withUrl(newUrl); } } diff --git a/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsUrl.java b/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsUrl.java index 6ae07240a65..3e1388f81f1 100644 --- a/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsUrl.java +++ b/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsUrl.java @@ -43,6 +43,8 @@ public class AzureDevOpsUrl extends DefaultFactoryUrl { private String tag; + private String serverUrl; + private final List devfileFilenames = new ArrayList<>(); protected AzureDevOpsUrl() {} @@ -100,7 +102,7 @@ public AzureDevOpsUrl withBranch(String branch) { @Override public String getProviderUrl() { - return "https://" + hostName; + return isNullOrEmpty(serverUrl) ? "https://" + hostName : serverUrl; } protected AzureDevOpsUrl withDevfileFilenames(List devfileFilenames) { @@ -108,6 +110,11 @@ protected AzureDevOpsUrl withDevfileFilenames(List devfileFilenames) { return this; } + public AzureDevOpsUrl withServerUrl(String serverUrl) { + this.serverUrl = serverUrl; + return this; + } + @Override public void setDevfileFilename(String devfileName) { this.devfileFilenames.clear(); diff --git a/wsmaster/che-core-api-factory-azure-devops/src/test/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsPersonalAccessTokenFetcherTest.java b/wsmaster/che-core-api-factory-azure-devops/src/test/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsPersonalAccessTokenFetcherTest.java index ed51fe091fe..6f9c32dd93f 100644 --- a/wsmaster/che-core-api-factory-azure-devops/src/test/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsPersonalAccessTokenFetcherTest.java +++ b/wsmaster/che-core-api-factory-azure-devops/src/test/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsPersonalAccessTokenFetcherTest.java @@ -18,6 +18,8 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static java.net.HttpURLConnection.HTTP_FORBIDDEN; +import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; +import static org.eclipse.che.api.factory.server.scm.PersonalAccessTokenFetcher.OAUTH_2_PREFIX; import static org.eclipse.che.dto.server.DtoFactory.newDto; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -29,9 +31,13 @@ import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.common.Slf4jNotifier; import com.google.common.net.HttpHeaders; +import java.util.Base64; +import java.util.Optional; import org.eclipse.che.api.auth.shared.dto.OAuthToken; import org.eclipse.che.api.factory.server.scm.PersonalAccessToken; +import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenParams; import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException; +import org.eclipse.che.commons.lang.Pair; import org.eclipse.che.commons.subject.Subject; import org.eclipse.che.commons.subject.SubjectImpl; import org.eclipse.che.security.oauth.OAuthAPI; @@ -51,6 +57,8 @@ public class AzureDevOpsPersonalAccessTokenFetcherTest { @Mock private OAuthToken oAuthToken; @Mock private AzureDevOpsUser azureDevOpsUser; + private final String azureDevOpsToken = "token"; + final int httpPort = 3301; WireMockServer wireMockServer; WireMock wireMock; @@ -120,4 +128,91 @@ public void shouldThrowUnauthorizedExceptionIfTokenIsNotValid() throws Exception personalAccessTokenFetcher.fetchPersonalAccessToken(subject, wireMockServer.url("/")); } + + @Test + public void shouldValidateSAASPersonalAccessToken() throws Exception { + stubFor( + get(urlEqualTo("/organization/_apis/profile/profiles/me?api-version=7.0")) + .withHeader( + HttpHeaders.AUTHORIZATION, + equalTo( + "Basic " + + Base64.getEncoder().encodeToString((":" + azureDevOpsToken).getBytes()))) + .willReturn( + aResponse() + .withHeader("Content-Type", "application/json; charset=utf-8") + .withBodyFile("azure-devops/rest/user/response.json"))); + + PersonalAccessTokenParams params = + new PersonalAccessTokenParams( + wireMockServer.url("/"), + "azure-devops", + "token-name", + "tid-23434", + azureDevOpsToken, + "organization"); + + Optional> valid = personalAccessTokenFetcher.isValid(params); + assertTrue(valid.isPresent()); + assertTrue(valid.get().first); + } + + @Test + public void shouldValidateServerPersonalAccessToken() throws Exception { + personalAccessTokenFetcher = + new AzureDevOpsPersonalAccessTokenFetcher( + "localhost", + "https://dev.azure-server.com", + new String[] {}, + new AzureDevOpsApiClient(wireMockServer.url("/")), + oAuthAPI); + stubFor( + get(urlEqualTo("/organization/_api/_common/GetUserProfile")) + .withHeader( + HttpHeaders.AUTHORIZATION, + equalTo( + "Basic " + + Base64.getEncoder().encodeToString((":" + azureDevOpsToken).getBytes()))) + .willReturn( + aResponse() + .withHeader("Content-Type", "application/json; charset=utf-8") + .withBodyFile("azure-devops-server/rest/user/response.json"))); + + PersonalAccessTokenParams params = + new PersonalAccessTokenParams( + wireMockServer.url("/"), + "azure-devops", + "token-name", + "tid-23434", + azureDevOpsToken, + "organization"); + + Optional> valid = personalAccessTokenFetcher.isValid(params); + assertTrue(valid.isPresent()); + assertTrue(valid.get().first); + } + + @Test + public void shouldValidateOauthToken() throws Exception { + stubFor( + get(urlEqualTo("/_apis/profile/profiles/me?api-version=7.0")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer " + azureDevOpsToken)) + .willReturn( + aResponse() + .withHeader("Content-Type", "application/json; charset=utf-8") + .withBodyFile("azure-devops/rest/user/response.json"))); + + PersonalAccessTokenParams params = + new PersonalAccessTokenParams( + wireMockServer.url("/"), + "dev-azure", + OAUTH_2_PREFIX + "-token-name", + "tid-23434", + azureDevOpsToken, + "organization"); + + Optional> valid = personalAccessTokenFetcher.isValid(params); + assertTrue(valid.isPresent()); + assertTrue(valid.get().first); + } } diff --git a/wsmaster/che-core-api-factory-azure-devops/src/test/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsURLParserTest.java b/wsmaster/che-core-api-factory-azure-devops/src/test/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsURLParserTest.java index 7265d3baa66..2f3321dc714 100644 --- a/wsmaster/che-core-api-factory-azure-devops/src/test/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsURLParserTest.java +++ b/wsmaster/che-core-api-factory-azure-devops/src/test/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsURLParserTest.java @@ -15,6 +15,7 @@ import static org.testng.Assert.assertEquals; import java.util.Optional; +import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager; import org.eclipse.che.api.factory.server.urlfactory.DevfileFilenamesProvider; import org.mockito.testng.MockitoTestNGListener; import org.testng.annotations.BeforeMethod; @@ -31,7 +32,10 @@ public class AzureDevOpsURLParserTest { @BeforeMethod protected void start() { azureDevOpsURLParser = - new AzureDevOpsURLParser(mock(DevfileFilenamesProvider.class), "https://dev.azure.com/"); + new AzureDevOpsURLParser( + mock(DevfileFilenamesProvider.class), + mock(PersonalAccessTokenManager.class), + "https://dev.azure.com/"); } @Test(expectedExceptions = IllegalArgumentException.class) diff --git a/wsmaster/che-core-api-factory-azure-devops/src/test/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsURLTest.java b/wsmaster/che-core-api-factory-azure-devops/src/test/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsURLTest.java index 240c590df72..ec7b6209db1 100644 --- a/wsmaster/che-core-api-factory-azure-devops/src/test/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsURLTest.java +++ b/wsmaster/che-core-api-factory-azure-devops/src/test/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsURLTest.java @@ -17,6 +17,7 @@ import java.util.Arrays; import java.util.Iterator; +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.RemoteFactoryUrl; import org.mockito.testng.MockitoTestNGListener; @@ -32,7 +33,10 @@ public class AzureDevOpsURLTest { @BeforeMethod protected void init() { azureDevOpsURLParser = - new AzureDevOpsURLParser(mock(DevfileFilenamesProvider.class), "https://dev.azure.com/"); + new AzureDevOpsURLParser( + mock(DevfileFilenamesProvider.class), + mock(PersonalAccessTokenManager.class), + "https://dev.azure.com/"); } @Test(dataProvider = "urlsProvider") diff --git a/wsmaster/che-core-api-factory-azure-devops/src/test/resources/__files/azure-devops-server/rest/user/response.json b/wsmaster/che-core-api-factory-azure-devops/src/test/resources/__files/azure-devops-server/rest/user/response.json new file mode 100644 index 00000000000..2b754f3c0f3 --- /dev/null +++ b/wsmaster/che-core-api-factory-azure-devops/src/test/resources/__files/azure-devops-server/rest/user/response.json @@ -0,0 +1,5 @@ +{ + "identity": { + "AccountName": "Azure DevOps" + } +} \ No newline at end of file diff --git a/wsmaster/che-core-api-factory-azure-devops/src/test/resources/__files/azure-devops/rest/user/email/response.json b/wsmaster/che-core-api-factory-azure-devops/src/test/resources/__files/azure-devops/rest/user/email/response.json new file mode 100644 index 00000000000..d9d61e057be --- /dev/null +++ b/wsmaster/che-core-api-factory-azure-devops/src/test/resources/__files/azure-devops/rest/user/email/response.json @@ -0,0 +1,3 @@ +{ + "values": [{"email" : "bitbucketuser@email.com"}] +} \ No newline at end of file diff --git a/wsmaster/che-core-api-factory-azure-devops/src/test/resources/__files/azure-devops/rest/user/response.json b/wsmaster/che-core-api-factory-azure-devops/src/test/resources/__files/azure-devops/rest/user/response.json new file mode 100644 index 00000000000..cfca319b5e2 --- /dev/null +++ b/wsmaster/che-core-api-factory-azure-devops/src/test/resources/__files/azure-devops/rest/user/response.json @@ -0,0 +1,9 @@ +{ + "displayName": "Normal Paulk", + "publicAlias": "d6245f20-2af8-44f4-9451-8107cb2767db", + "emailAddress": "fabrikamfiber16@hotmail.com", + "coreRevision": 1647, + "timeStamp": "2014-05-12T22:23:07.727+00:00", + "id": "d6245f20-2af8-44f4-9451-8107cb2767db", + "revision": 1647 +} \ No newline at end of file diff --git a/wsmaster/che-core-api-factory-bitbucket/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketPersonalAccessTokenFetcherTest.java b/wsmaster/che-core-api-factory-bitbucket/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketPersonalAccessTokenFetcherTest.java index 6bd77b2d3b9..96f6248cd70 100644 --- a/wsmaster/che-core-api-factory-bitbucket/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketPersonalAccessTokenFetcherTest.java +++ b/wsmaster/che-core-api-factory-bitbucket/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketPersonalAccessTokenFetcherTest.java @@ -84,7 +84,7 @@ public void shouldNotValidateSCMServerWithTrailingSlash() throws Exception { .willReturn( aResponse() .withHeader("Content-Type", "application/json; charset=utf-8") - .withBodyFile("bitbucket/rest/user/response.json"))); + .withBodyFile("azure-devops/rest/user/response.json"))); PersonalAccessTokenParams personalAccessTokenParams = new PersonalAccessTokenParams( "https://bitbucket.org/", diff --git a/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabUrlParser.java b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabUrlParser.java index 76aa1e13270..684946f1219 100644 --- a/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabUrlParser.java +++ b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabUrlParser.java @@ -112,7 +112,8 @@ private boolean isApiRequestRelevant(String repositoryUrl) { // belongs to Gitlab. gitlabApiClient.getOAuthTokenInfo(""); } catch (ScmUnauthorizedException e) { - return true; + // the error message is a JSON if it is a response from Gitlab. + return e.getMessage().startsWith("{"); } catch (ScmItemNotFoundException | IllegalArgumentException | ScmCommunicationException e) { return false; }