chore: Support configuring the GitHub OAuth endpoint (#350)
Currently the GitHub OAuth provider is hardcoded to https://github.com endpoint. In order to support Github Enterprise Server, the endpoint of the GitHub OAuth provider is configurable by the oauth secret.pull/354/head
parent
9f72001242
commit
c2a2d09194
|
|
@ -204,6 +204,10 @@ che.oauth.github.authuri= https://github.com/login/oauth/authorize
|
|||
# GitHub OAuth token URI.
|
||||
che.oauth.github.tokenuri= https://github.com/login/oauth/access_token
|
||||
|
||||
# GitHub server address.
|
||||
# Prerequisite: OAuth 2 integration is configured on the GitHub server.
|
||||
che.integration.github.oauth_endpoint=NULL
|
||||
|
||||
# GitHub OAuth redirect URIs.
|
||||
# Separate multiple values with comma, for example: URI,URI,URI
|
||||
che.oauth.github.redirecturis= http://localhost:${CHE_PORT}/api/oauth/callback
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2012-2018 Red Hat, Inc.
|
||||
* Copyright (c) 2012-2022 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/
|
||||
|
|
@ -33,7 +33,7 @@ public class OpenShiftGitHubOAuthAuthenticator extends GitHubOAuthAuthenticator
|
|||
@Nullable @Named("che.oauth.github.tokenuri") String tokenUri)
|
||||
throws IOException {
|
||||
|
||||
super("NULL", "NULL", redirectUris, authUri, tokenUri);
|
||||
super("NULL", "NULL", redirectUris, null, authUri, tokenUri);
|
||||
|
||||
if (!isNullOrEmpty(authUri)
|
||||
&& !isNullOrEmpty(tokenUri)
|
||||
|
|
|
|||
|
|
@ -59,6 +59,10 @@
|
|||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-commons-inject</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-commons-lang</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2012-2021 Red Hat, Inc.
|
||||
* Copyright (c) 2012-2022 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/
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
package org.eclipse.che.security.oauth;
|
||||
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
import static org.eclipse.che.commons.lang.StringUtils.trimEnd;
|
||||
|
||||
import com.google.api.client.util.store.MemoryDataStoreFactory;
|
||||
import jakarta.mail.internet.AddressException;
|
||||
|
|
@ -24,17 +25,27 @@ import org.eclipse.che.security.oauth.shared.User;
|
|||
/** OAuth authentication for github account. */
|
||||
@Singleton
|
||||
public class GitHubOAuthAuthenticator extends OAuthAuthenticator {
|
||||
private final String userRequestUrl;
|
||||
|
||||
public GitHubOAuthAuthenticator(
|
||||
String clientId, String clientSecret, String[] redirectUris, String authUri, String tokenUri)
|
||||
String clientId,
|
||||
String clientSecret,
|
||||
String[] redirectUris,
|
||||
String authEndpoint,
|
||||
String authUri,
|
||||
String tokenUri)
|
||||
throws IOException {
|
||||
userRequestUrl =
|
||||
isNullOrEmpty(authEndpoint) || trimEnd(authEndpoint, '/').equals("https://github.com")
|
||||
? "https://api.github.com/user"
|
||||
: trimEnd(authEndpoint, '/') + "/api/v3/user";
|
||||
configure(
|
||||
clientId, clientSecret, redirectUris, authUri, tokenUri, new MemoryDataStoreFactory());
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getUser(OAuthToken accessToken) throws OAuthAuthenticationException {
|
||||
GitHubUser user =
|
||||
getJson("https://api.github.com/user", accessToken.getToken(), GitHubUser.class);
|
||||
GitHubUser user = getJson(userRequestUrl, accessToken.getToken(), GitHubUser.class);
|
||||
final String email = user.getEmail();
|
||||
|
||||
if (isNullOrEmpty(email)) {
|
||||
|
|
@ -65,7 +76,7 @@ public class GitHubOAuthAuthenticator extends OAuthAuthenticator {
|
|||
if (token == null
|
||||
|| token.getToken() == null
|
||||
|| token.getToken().isEmpty()
|
||||
|| getJson("https://api.github.com/user", token.getToken(), GitHubUser.class) == null) {
|
||||
|| getJson(userRequestUrl, token.getToken(), GitHubUser.class) == null) {
|
||||
return null;
|
||||
}
|
||||
} catch (OAuthAuthenticationException e) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
package org.eclipse.che.security.oauth;
|
||||
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
import static org.eclipse.che.commons.lang.StringUtils.trimEnd;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
|
@ -42,12 +43,18 @@ public class GitHubOAuthAuthenticatorProvider implements Provider<OAuthAuthentic
|
|||
@Nullable @Named("che.oauth2.github.clientid_filepath") String gitHubClientIdPath,
|
||||
@Nullable @Named("che.oauth2.github.clientsecret_filepath") String gitHubClientSecretPath,
|
||||
@Nullable @Named("che.oauth.github.redirecturis") String[] redirectUris,
|
||||
@Nullable @Named("che.integration.github.oauth_endpoint") String oauthEndpoint,
|
||||
@Nullable @Named("che.oauth.github.authuri") String authUri,
|
||||
@Nullable @Named("che.oauth.github.tokenuri") String tokenUri)
|
||||
throws IOException {
|
||||
authenticator =
|
||||
getOAuthAuthenticator(
|
||||
gitHubClientIdPath, gitHubClientSecretPath, redirectUris, authUri, tokenUri);
|
||||
gitHubClientIdPath,
|
||||
gitHubClientSecretPath,
|
||||
redirectUris,
|
||||
oauthEndpoint,
|
||||
authUri,
|
||||
tokenUri);
|
||||
LOG.debug("{} GitHub OAuth Authenticator is used.", authenticator);
|
||||
}
|
||||
|
||||
|
|
@ -60,10 +67,20 @@ public class GitHubOAuthAuthenticatorProvider implements Provider<OAuthAuthentic
|
|||
String clientIdPath,
|
||||
String clientSecretPath,
|
||||
String[] redirectUris,
|
||||
String oauthEndpoint,
|
||||
String authUri,
|
||||
String tokenUri)
|
||||
throws IOException {
|
||||
|
||||
String trimmedOauthEndpoint = isNullOrEmpty(oauthEndpoint) ? null : trimEnd(oauthEndpoint, '/');
|
||||
authUri =
|
||||
isNullOrEmpty(trimmedOauthEndpoint)
|
||||
? authUri
|
||||
: trimmedOauthEndpoint + "/login/oauth/authorize";
|
||||
tokenUri =
|
||||
isNullOrEmpty(trimmedOauthEndpoint)
|
||||
? tokenUri
|
||||
: trimmedOauthEndpoint + "/login/oauth/access_token";
|
||||
if (!isNullOrEmpty(clientIdPath)
|
||||
&& !isNullOrEmpty(clientSecretPath)
|
||||
&& !isNullOrEmpty(authUri)
|
||||
|
|
@ -74,7 +91,7 @@ public class GitHubOAuthAuthenticatorProvider implements Provider<OAuthAuthentic
|
|||
final String clientSecret = Files.readString(Path.of(clientSecretPath)).trim();
|
||||
if (!isNullOrEmpty(clientId) && !isNullOrEmpty(clientSecret)) {
|
||||
return new GitHubOAuthAuthenticator(
|
||||
clientId, clientSecret, redirectUris, authUri, tokenUri);
|
||||
clientId, clientSecret, redirectUris, trimmedOauthEndpoint, authUri, tokenUri);
|
||||
}
|
||||
}
|
||||
return new NoopOAuthAuthenticator();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2012-2021 Red Hat, Inc.
|
||||
* Copyright (c) 2012-2022 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/
|
||||
|
|
@ -41,13 +41,19 @@ public class GitHubOAuthAuthenticatorProviderTest {
|
|||
String gitHubClientIdPath,
|
||||
String gitHubClientSecretPath,
|
||||
String[] redirectUris,
|
||||
String oauthEndpoint,
|
||||
String authUri,
|
||||
String tokenUri)
|
||||
throws IOException {
|
||||
// given
|
||||
GitHubOAuthAuthenticatorProvider provider =
|
||||
new GitHubOAuthAuthenticatorProvider(
|
||||
gitHubClientIdPath, gitHubClientSecretPath, redirectUris, authUri, tokenUri);
|
||||
gitHubClientIdPath,
|
||||
gitHubClientSecretPath,
|
||||
redirectUris,
|
||||
oauthEndpoint,
|
||||
authUri,
|
||||
tokenUri);
|
||||
// when
|
||||
OAuthAuthenticator authenticator = provider.get();
|
||||
// then
|
||||
|
|
@ -62,7 +68,12 @@ public class GitHubOAuthAuthenticatorProviderTest {
|
|||
// given
|
||||
GitHubOAuthAuthenticatorProvider provider =
|
||||
new GitHubOAuthAuthenticatorProvider(
|
||||
emptyFile.getPath(), emptyFile.getPath(), new String[] {TEST_URI}, TEST_URI, TEST_URI);
|
||||
emptyFile.getPath(),
|
||||
emptyFile.getPath(),
|
||||
new String[] {TEST_URI},
|
||||
null,
|
||||
TEST_URI,
|
||||
TEST_URI);
|
||||
// when
|
||||
OAuthAuthenticator authenticator = provider.get();
|
||||
// then
|
||||
|
|
@ -80,6 +91,27 @@ public class GitHubOAuthAuthenticatorProviderTest {
|
|||
credentialFile.getPath(),
|
||||
credentialFile.getPath(),
|
||||
new String[] {TEST_URI},
|
||||
null,
|
||||
TEST_URI,
|
||||
TEST_URI);
|
||||
// when
|
||||
OAuthAuthenticator authenticator = provider.get();
|
||||
|
||||
// then
|
||||
assertNotNull(authenticator);
|
||||
assertTrue(GitHubOAuthAuthenticator.class.isAssignableFrom(authenticator.getClass()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldProvideValidGitHubOAuthAuthenticatorWithConfiguredOAuthEndpoint()
|
||||
throws IOException {
|
||||
// given
|
||||
GitHubOAuthAuthenticatorProvider provider =
|
||||
new GitHubOAuthAuthenticatorProvider(
|
||||
credentialFile.getPath(),
|
||||
credentialFile.getPath(),
|
||||
new String[] {TEST_URI},
|
||||
"https://custom.github.com/",
|
||||
TEST_URI,
|
||||
TEST_URI);
|
||||
// when
|
||||
|
|
@ -93,22 +125,40 @@ public class GitHubOAuthAuthenticatorProviderTest {
|
|||
@DataProvider(name = "noopConfig")
|
||||
public Object[][] noopConfig() {
|
||||
return new Object[][] {
|
||||
{null, null, null, null, null},
|
||||
{credentialFile.getPath(), emptyFile.getPath(), null, TEST_URI, null},
|
||||
{emptyFile.getPath(), emptyFile.getPath(), null, null, TEST_URI},
|
||||
{null, emptyFile.getPath(), null, TEST_URI, TEST_URI},
|
||||
{null, credentialFile.getPath(), new String[] {}, null, null},
|
||||
{emptyFile.getPath(), null, new String[] {}, "", ""},
|
||||
{credentialFile.getPath(), null, new String[] {}, "", null},
|
||||
{null, emptyFile.getPath(), new String[] {}, null, ""},
|
||||
{credentialFile.getPath(), null, new String[] {}, TEST_URI, null},
|
||||
{null, emptyFile.getPath(), new String[] {}, TEST_URI, TEST_URI},
|
||||
{emptyFile.getPath(), null, new String[] {TEST_URI}, null, null},
|
||||
{credentialFile.getPath(), null, new String[] {TEST_URI}, "", ""},
|
||||
{credentialFile.getPath(), credentialFile.getPath(), new String[] {TEST_URI}, null, TEST_URI},
|
||||
{credentialFile.getPath(), emptyFile.getPath(), new String[] {TEST_URI}, TEST_URI, null},
|
||||
{credentialFile.getPath(), credentialFile.getPath(), new String[] {TEST_URI}, TEST_URI, ""},
|
||||
{emptyFile.getPath(), emptyFile.getPath(), new String[] {TEST_URI}, "", TEST_URI}
|
||||
{null, null, null, null, null, null},
|
||||
{null, null, null, "", null, null},
|
||||
{null, null, null, TEST_URI, null, null},
|
||||
{credentialFile.getPath(), emptyFile.getPath(), null, null, TEST_URI, null},
|
||||
{emptyFile.getPath(), emptyFile.getPath(), null, null, null, TEST_URI},
|
||||
{null, emptyFile.getPath(), null, null, TEST_URI, TEST_URI},
|
||||
{null, credentialFile.getPath(), new String[] {}, null, null, null},
|
||||
{emptyFile.getPath(), null, new String[] {}, null, "", ""},
|
||||
{credentialFile.getPath(), null, new String[] {}, null, "", null},
|
||||
{null, emptyFile.getPath(), new String[] {}, null, null, ""},
|
||||
{credentialFile.getPath(), null, new String[] {}, null, TEST_URI, null},
|
||||
{null, emptyFile.getPath(), new String[] {}, null, TEST_URI, TEST_URI},
|
||||
{emptyFile.getPath(), null, new String[] {TEST_URI}, null, null, null},
|
||||
{credentialFile.getPath(), null, new String[] {TEST_URI}, null, "", ""},
|
||||
{
|
||||
credentialFile.getPath(),
|
||||
credentialFile.getPath(),
|
||||
new String[] {TEST_URI},
|
||||
null,
|
||||
null,
|
||||
TEST_URI
|
||||
},
|
||||
{
|
||||
credentialFile.getPath(), emptyFile.getPath(), new String[] {TEST_URI}, null, TEST_URI, null
|
||||
},
|
||||
{
|
||||
credentialFile.getPath(),
|
||||
credentialFile.getPath(),
|
||||
new String[] {TEST_URI},
|
||||
null,
|
||||
TEST_URI,
|
||||
""
|
||||
},
|
||||
{emptyFile.getPath(), emptyFile.getPath(), new String[] {TEST_URI}, null, "", TEST_URI}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,10 @@
|
|||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-workspace-shared</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-commons-annotations</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-commons-lang</artifactId>
|
||||
|
|
|
|||
|
|
@ -11,11 +11,13 @@
|
|||
*/
|
||||
package org.eclipse.che.api.factory.server.github;
|
||||
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
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.commons.lang.StringUtils.trimEnd;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.base.Charsets;
|
||||
|
|
@ -37,6 +39,7 @@ 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.annotation.Nullable;
|
||||
import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
|
@ -46,11 +49,8 @@ public class GithubApiClient {
|
|||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GithubApiClient.class);
|
||||
|
||||
/** GitHub API endpoint URL. */
|
||||
public static final String GITHUB_API_SERVER = "https://api.github.com";
|
||||
|
||||
/** GitHub endpoint URL. */
|
||||
public static final String GITHUB_SERVER = "https://github.com";
|
||||
public static final String GITHUB_SAAS_ENDPOINT = "https://github.com";
|
||||
|
||||
/** GitHub HTTP header containing OAuth scopes. */
|
||||
public static final String GITHUB_OAUTH_SCOPES_HEADER = "X-OAuth-Scopes";
|
||||
|
|
@ -62,19 +62,16 @@ public class GithubApiClient {
|
|||
private static final Duration DEFAULT_HTTP_TIMEOUT = ofSeconds(10);
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
/** Default constructor, binds http client to https://api.github.com */
|
||||
public GithubApiClient() {
|
||||
this(GITHUB_API_SERVER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for URL injection in testing.
|
||||
*
|
||||
* @param apiServerUrl the GitHub API url
|
||||
*/
|
||||
GithubApiClient(final String apiServerUrl) {
|
||||
this.apiServerUrl = URI.create(apiServerUrl);
|
||||
this.scmServerUrl = URI.create(GITHUB_SERVER);
|
||||
/** Default constructor, binds http client to GitHub API url */
|
||||
public GithubApiClient(@Nullable String serverUrl) {
|
||||
String trimmedServerUrl = !isNullOrEmpty(serverUrl) ? trimEnd(serverUrl, '/') : null;
|
||||
this.apiServerUrl =
|
||||
URI.create(
|
||||
isNullOrEmpty(trimmedServerUrl) || trimmedServerUrl.equals(GITHUB_SAAS_ENDPOINT)
|
||||
? "https://api.github.com/"
|
||||
: trimmedServerUrl + "/api/v3/");
|
||||
this.scmServerUrl =
|
||||
URI.create(isNullOrEmpty(trimmedServerUrl) ? GITHUB_SAAS_ENDPOINT : trimmedServerUrl);
|
||||
this.httpClient =
|
||||
HttpClient.newBuilder()
|
||||
.executor(
|
||||
|
|
@ -101,7 +98,7 @@ public class GithubApiClient {
|
|||
*/
|
||||
public GithubUser getUser(String authenticationToken)
|
||||
throws ScmItemNotFoundException, ScmCommunicationException, ScmBadRequestException {
|
||||
final URI uri = apiServerUrl.resolve("/user");
|
||||
final URI uri = apiServerUrl.resolve("./user");
|
||||
HttpRequest request = buildGithubApiRequest(uri, authenticationToken);
|
||||
LOG.trace("executeRequest={}", request);
|
||||
return executeRequest(
|
||||
|
|
@ -120,7 +117,7 @@ public class GithubApiClient {
|
|||
String id, String username, String repoName, String authenticationToken)
|
||||
throws ScmItemNotFoundException, ScmCommunicationException, ScmBadRequestException {
|
||||
final URI uri =
|
||||
apiServerUrl.resolve(String.format("/repos/%1s/%2s/pulls/%3s", username, repoName, id));
|
||||
apiServerUrl.resolve(String.format("./repos/%1s/%2s/pulls/%3s", username, repoName, id));
|
||||
HttpRequest request = buildGithubApiRequest(uri, authenticationToken);
|
||||
LOG.trace("executeRequest={}", request);
|
||||
return executeRequest(
|
||||
|
|
@ -149,7 +146,7 @@ public class GithubApiClient {
|
|||
*/
|
||||
public String[] getTokenScopes(String authenticationToken)
|
||||
throws ScmItemNotFoundException, ScmCommunicationException, ScmBadRequestException {
|
||||
final URI uri = apiServerUrl.resolve("/user");
|
||||
final URI uri = apiServerUrl.resolve("./user");
|
||||
HttpRequest request = buildGithubApiRequest(uri, authenticationToken);
|
||||
LOG.trace("executeRequest={}", request);
|
||||
return executeRequest(
|
||||
|
|
@ -213,6 +210,6 @@ public class GithubApiClient {
|
|||
* @return If the provided url is recognized by the current client
|
||||
*/
|
||||
public boolean isConnected(String scmServerUrl) {
|
||||
return this.scmServerUrl.equals(URI.create(scmServerUrl));
|
||||
return this.scmServerUrl.equals(URI.create(trimEnd(scmServerUrl, '/')));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationExceptio
|
|||
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.UnknownScmProviderException;
|
||||
import org.eclipse.che.commons.annotation.Nullable;
|
||||
import org.eclipse.che.commons.lang.NameGenerator;
|
||||
import org.eclipse.che.commons.subject.Subject;
|
||||
import org.eclipse.che.security.oauth.OAuthAPI;
|
||||
|
|
@ -117,8 +118,11 @@ public class GithubPersonalAccessTokenFetcher implements PersonalAccessTokenFetc
|
|||
.build();
|
||||
|
||||
@Inject
|
||||
public GithubPersonalAccessTokenFetcher(@Named("che.api") String apiEndpoint, OAuthAPI oAuthAPI) {
|
||||
this(apiEndpoint, oAuthAPI, new GithubApiClient());
|
||||
public GithubPersonalAccessTokenFetcher(
|
||||
@Named("che.api") String apiEndpoint,
|
||||
@Nullable @Named("che.integration.github.oauth_endpoint") String oauthEndpoint,
|
||||
OAuthAPI oAuthAPI) {
|
||||
this(apiEndpoint, oAuthAPI, new GithubApiClient(oauthEndpoint));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -161,7 +165,7 @@ public class GithubPersonalAccessTokenFetcher implements PersonalAccessTokenFetc
|
|||
if (valid.isEmpty()) {
|
||||
throw new ScmCommunicationException(
|
||||
"Unable to verify if current token is a valid GitHub token. Token's scm-url needs to be '"
|
||||
+ GithubApiClient.GITHUB_SERVER
|
||||
+ GithubApiClient.GITHUB_SAAS_ENDPOINT
|
||||
+ "' and was '"
|
||||
+ token.getScmProviderUrl()
|
||||
+ "'");
|
||||
|
|
|
|||
|
|
@ -11,13 +11,19 @@
|
|||
*/
|
||||
package org.eclipse.che.api.factory.server.github;
|
||||
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
import static java.lang.String.format;
|
||||
import static java.util.regex.Pattern.compile;
|
||||
import static org.eclipse.che.api.factory.server.ApiExceptionMapper.toApiException;
|
||||
import static org.eclipse.che.api.factory.server.github.GithubApiClient.GITHUB_SAAS_ENDPOINT;
|
||||
import static org.eclipse.che.commons.lang.StringUtils.trimEnd;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
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.core.ApiException;
|
||||
import org.eclipse.che.api.factory.server.scm.PersonalAccessToken;
|
||||
|
|
@ -30,6 +36,7 @@ import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException
|
|||
import org.eclipse.che.api.factory.server.scm.exception.UnknownScmProviderException;
|
||||
import org.eclipse.che.api.factory.server.scm.exception.UnsatisfiedScmPreconditionException;
|
||||
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.subject.Subject;
|
||||
import org.slf4j.Logger;
|
||||
|
|
@ -47,27 +54,42 @@ public class GithubURLParser {
|
|||
private final PersonalAccessTokenManager tokenManager;
|
||||
private final DevfileFilenamesProvider devfileFilenamesProvider;
|
||||
private final GithubApiClient apiClient;
|
||||
private final String oauthEndpoint;
|
||||
/**
|
||||
* Regexp to find repository details (repository name, project name and branch and subfolder)
|
||||
* Examples of valid URLs are in the test class.
|
||||
*/
|
||||
private final Pattern githubPattern;
|
||||
|
||||
@Inject
|
||||
public GithubURLParser(
|
||||
PersonalAccessTokenManager tokenManager,
|
||||
DevfileFilenamesProvider devfileFilenamesProvider,
|
||||
GithubApiClient apiClient) {
|
||||
this.tokenManager = tokenManager;
|
||||
this.devfileFilenamesProvider = devfileFilenamesProvider;
|
||||
this.apiClient = apiClient;
|
||||
@Nullable @Named("che.integration.github.oauth_endpoint") String oauthEndpoint) {
|
||||
this(tokenManager, devfileFilenamesProvider, new GithubApiClient(oauthEndpoint), oauthEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Regexp to find repository details (repository name, project name and branch and subfolder)
|
||||
* Examples of valid URLs are in the test class.
|
||||
*/
|
||||
protected static final Pattern GITHUB_PATTERN =
|
||||
Pattern.compile(
|
||||
"^(?:http)(?:s)?(?:\\:\\/\\/)github.com/(?<repoUser>[^/]++)/(?<repoName>[^/]++)((/)|(?:/tree/(?<branchName>[^/]++)(?:/(?<subFolder>.*))?)|(/pull/(?<pullRequestId>[^/]++)))?$");
|
||||
/** Constructor used for testing only. */
|
||||
GithubURLParser(
|
||||
PersonalAccessTokenManager tokenManager,
|
||||
DevfileFilenamesProvider devfileFilenamesProvider,
|
||||
GithubApiClient githubApiClient,
|
||||
String oauthEndpoint) {
|
||||
this.tokenManager = tokenManager;
|
||||
this.devfileFilenamesProvider = devfileFilenamesProvider;
|
||||
this.apiClient = githubApiClient;
|
||||
this.oauthEndpoint = oauthEndpoint;
|
||||
String endpoint =
|
||||
isNullOrEmpty(oauthEndpoint) ? GITHUB_SAAS_ENDPOINT : trimEnd(oauthEndpoint, '/');
|
||||
this.githubPattern =
|
||||
compile(
|
||||
format(
|
||||
"^%s/(?<repoUser>[^/]++)/(?<repoName>[^/]++)((/)|(?:/tree/(?<branchName>[^/]++)(?:/(?<subFolder>.*))?)|(/pull/(?<pullRequestId>[^/]++)))?$",
|
||||
endpoint));
|
||||
}
|
||||
|
||||
public boolean isValid(@NotNull String url) {
|
||||
return GITHUB_PATTERN.matcher(url).matches();
|
||||
return githubPattern.matcher(url).matches();
|
||||
}
|
||||
|
||||
public GithubUrl parseWithoutAuthentication(String url) throws ApiException {
|
||||
|
|
@ -80,14 +102,18 @@ public class GithubURLParser {
|
|||
|
||||
private GithubUrl parse(String url, boolean authenticationRequired) throws ApiException {
|
||||
// Apply github url to the regexp
|
||||
Matcher matcher = GITHUB_PATTERN.matcher(url);
|
||||
Matcher matcher = githubPattern.matcher(url);
|
||||
if (!matcher.matches()) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format(
|
||||
format(
|
||||
"The given github url %s is not a valid URL github url. It should start with https://github.com/<user>/<repo>",
|
||||
url));
|
||||
}
|
||||
|
||||
String serverUrl =
|
||||
isNullOrEmpty(oauthEndpoint) || trimEnd(oauthEndpoint, '/').equals(GITHUB_SAAS_ENDPOINT)
|
||||
? null
|
||||
: trimEnd(oauthEndpoint, '/');
|
||||
String repoUser = matcher.group("repoUser");
|
||||
String repoName = matcher.group("repoName");
|
||||
if (repoName.matches("^[\\w-][\\w.-]*?\\.git$")) {
|
||||
|
|
@ -98,7 +124,8 @@ public class GithubURLParser {
|
|||
String pullRequestId = matcher.group("pullRequestId");
|
||||
if (pullRequestId != null) {
|
||||
try {
|
||||
String githubEndpoint = "https://github.com";
|
||||
String githubEndpoint =
|
||||
isNullOrEmpty(oauthEndpoint) ? GITHUB_SAAS_ENDPOINT : trimEnd(oauthEndpoint, '/');
|
||||
Subject subject = EnvironmentContext.getCurrent().getSubject();
|
||||
PersonalAccessToken personalAccessToken = null;
|
||||
Optional<PersonalAccessToken> tokenOptional = tokenManager.get(subject, githubEndpoint);
|
||||
|
|
@ -114,7 +141,7 @@ public class GithubURLParser {
|
|||
String prState = pullRequest.getState();
|
||||
if (!"open".equalsIgnoreCase(prState)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format(
|
||||
format(
|
||||
"The given Pull Request url %s is not Opened, (found %s), thus it can't be opened as branch may have been removed.",
|
||||
url, prState));
|
||||
}
|
||||
|
|
@ -139,6 +166,7 @@ public class GithubURLParser {
|
|||
return new GithubUrl()
|
||||
.withUsername(repoUser)
|
||||
.withRepository(repoName)
|
||||
.withServerUrl(serverUrl)
|
||||
.withBranch(branchName)
|
||||
.withSubfolder(matcher.group("subFolder"))
|
||||
.withDevfileFilenames(devfileFilenamesProvider.getConfiguredDevfileFilenames());
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2012-2021 Red Hat, Inc.
|
||||
* Copyright (c) 2012-2022 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/
|
||||
|
|
@ -12,8 +12,8 @@
|
|||
package org.eclipse.che.api.factory.server.github;
|
||||
|
||||
import static com.google.common.base.MoreObjects.firstNonNull;
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
|
@ -47,6 +47,8 @@ public class GithubUrl implements RemoteFactoryUrl {
|
|||
/** Subfolder if any */
|
||||
private String subfolder;
|
||||
|
||||
private String serverUrl;
|
||||
|
||||
/** Devfile filenames list */
|
||||
private final List<String> devfileFilenames = new ArrayList<>();
|
||||
|
||||
|
|
@ -110,7 +112,7 @@ public class GithubUrl implements RemoteFactoryUrl {
|
|||
}
|
||||
|
||||
protected GithubUrl withBranch(String branch) {
|
||||
if (!Strings.isNullOrEmpty(branch)) {
|
||||
if (!isNullOrEmpty(branch)) {
|
||||
this.branch = branch;
|
||||
}
|
||||
return this;
|
||||
|
|
@ -136,6 +138,11 @@ public class GithubUrl implements RemoteFactoryUrl {
|
|||
return this;
|
||||
}
|
||||
|
||||
public GithubUrl withServerUrl(String serverUrl) {
|
||||
this.serverUrl = serverUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides list of configured devfile filenames with locations
|
||||
*
|
||||
|
|
@ -167,7 +174,7 @@ public class GithubUrl implements RemoteFactoryUrl {
|
|||
*/
|
||||
public String rawFileLocation(String fileName) {
|
||||
return new StringJoiner("/")
|
||||
.add("https://raw.githubusercontent.com")
|
||||
.add(isNullOrEmpty(serverUrl) ? "https://raw.githubusercontent.com" : serverUrl + "/raw")
|
||||
.add(username)
|
||||
.add(repository)
|
||||
.add(firstNonNull(branch, "HEAD"))
|
||||
|
|
@ -177,7 +184,7 @@ public class GithubUrl implements RemoteFactoryUrl {
|
|||
|
||||
@Override
|
||||
public String getHostName() {
|
||||
return HOSTNAME;
|
||||
return isNullOrEmpty(serverUrl) ? HOSTNAME : serverUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -186,6 +193,11 @@ public class GithubUrl implements RemoteFactoryUrl {
|
|||
* @return location of the repository.
|
||||
*/
|
||||
protected String repositoryLocation() {
|
||||
return HOSTNAME + "/" + this.username + "/" + this.repository + ".git";
|
||||
return (isNullOrEmpty(serverUrl) ? HOSTNAME : serverUrl)
|
||||
+ "/"
|
||||
+ this.username
|
||||
+ "/"
|
||||
+ this.repository
|
||||
+ ".git";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ 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.api.factory.server.scm.exception.ScmUnauthorizedException;
|
||||
import org.eclipse.che.commons.annotation.Nullable;
|
||||
import org.eclipse.che.commons.env.EnvironmentContext;
|
||||
import org.eclipse.che.commons.subject.Subject;
|
||||
import org.eclipse.che.security.oauth.OAuthAPI;
|
||||
|
|
@ -49,8 +50,11 @@ public class GithubUserDataFetcher implements GitUserDataFetcher {
|
|||
ImmutableSet.of("repo", "user:email", "read:user");
|
||||
|
||||
@Inject
|
||||
public GithubUserDataFetcher(@Named("che.api") String apiEndpoint, OAuthAPI oAuthAPI) {
|
||||
this(apiEndpoint, oAuthAPI, new GithubApiClient());
|
||||
public GithubUserDataFetcher(
|
||||
@Named("che.api") String apiEndpoint,
|
||||
@Nullable @Named("che.integration.github.oauth_endpoint") String oauthEndpoint,
|
||||
OAuthAPI oAuthAPI) {
|
||||
this(apiEndpoint, oAuthAPI, new GithubApiClient(oauthEndpoint));
|
||||
}
|
||||
|
||||
/** Constructor used for testing only. */
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2012-2021 Red Hat, Inc.
|
||||
* Copyright (c) 2012-2022 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/
|
||||
|
|
@ -58,7 +58,7 @@ public class GithubApiClientTest {
|
|||
@Test
|
||||
public void testGetUser() throws Exception {
|
||||
stubFor(
|
||||
get(urlEqualTo("/user"))
|
||||
get(urlEqualTo("/api/v3/user"))
|
||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("token token1"))
|
||||
.willReturn(
|
||||
aResponse()
|
||||
|
|
@ -81,7 +81,7 @@ public class GithubApiClientTest {
|
|||
@Test
|
||||
public void testGetTokenScopes() throws Exception {
|
||||
stubFor(
|
||||
get(urlEqualTo("/user"))
|
||||
get(urlEqualTo("/api/v3/user"))
|
||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("token token1"))
|
||||
.willReturn(
|
||||
aResponse()
|
||||
|
|
@ -99,7 +99,7 @@ public class GithubApiClientTest {
|
|||
@Test
|
||||
public void testGetTokenScopesWithNoScopeHeader() throws Exception {
|
||||
stubFor(
|
||||
get(urlEqualTo("/user"))
|
||||
get(urlEqualTo("/api/v3/user"))
|
||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("token token1"))
|
||||
.willReturn(
|
||||
aResponse()
|
||||
|
|
@ -119,7 +119,7 @@ public class GithubApiClientTest {
|
|||
@Test
|
||||
public void testGetTokenScopesWithNoScope() throws Exception {
|
||||
stubFor(
|
||||
get(urlEqualTo("/user"))
|
||||
get(urlEqualTo("/api/v3/user"))
|
||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("token token1"))
|
||||
.willReturn(
|
||||
aResponse()
|
||||
|
|
@ -144,6 +144,6 @@ public class GithubApiClientTest {
|
|||
|
||||
@Test
|
||||
public void shouldReturnTrueWhenConnectedToGithub() {
|
||||
assertTrue(client.isConnected("https://github.com"));
|
||||
assertTrue(client.isConnected(wireMockServer.url("/")));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import static org.mockito.ArgumentMatchers.any;
|
|||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
|
@ -102,8 +101,7 @@ public class GithubFactoryParametersResolverTest {
|
|||
@BeforeMethod
|
||||
protected void init() {
|
||||
githubUrlParser =
|
||||
new GithubURLParser(
|
||||
personalAccessTokenManager, devfileFilenamesProvider, mock(GithubApiClient.class));
|
||||
new GithubURLParser(personalAccessTokenManager, devfileFilenamesProvider, null);
|
||||
assertNotNull(this.githubUrlParser);
|
||||
githubFactoryParametersResolver =
|
||||
new GithubFactoryParametersResolver(
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ public class GithubGitUserDataFetcherTest {
|
|||
new GithubUserDataFetcher(
|
||||
"http://che.api", oAuthAPI, new GithubApiClient(wireMockServer.url("/")));
|
||||
stubFor(
|
||||
get(urlEqualTo("/user"))
|
||||
get(urlEqualTo("/api/v3/user"))
|
||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("token " + githubOauthToken))
|
||||
.willReturn(
|
||||
aResponse()
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ public class GithubPersonalAccessTokenFetcherTest {
|
|||
when(oAuthAPI.getToken(anyString())).thenReturn(oAuthToken);
|
||||
|
||||
stubFor(
|
||||
get(urlEqualTo("/user"))
|
||||
get(urlEqualTo("/api/v3/user"))
|
||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("token " + githubOauthToken))
|
||||
.willReturn(
|
||||
aResponse()
|
||||
|
|
@ -151,7 +151,7 @@ public class GithubPersonalAccessTokenFetcherTest {
|
|||
.withHeader(GithubApiClient.GITHUB_OAUTH_SCOPES_HEADER, "")
|
||||
.withBodyFile("github/rest/user/response.json")));
|
||||
|
||||
githubPATFetcher.fetchPersonalAccessToken(subject, GithubApiClient.GITHUB_SERVER);
|
||||
githubPATFetcher.fetchPersonalAccessToken(subject, wireMockServer.url("/"));
|
||||
}
|
||||
|
||||
@Test(
|
||||
|
|
@ -161,7 +161,7 @@ public class GithubPersonalAccessTokenFetcherTest {
|
|||
Subject subject = new SubjectImpl("Username", "id1", "token", false);
|
||||
when(oAuthAPI.getToken(anyString())).thenThrow(UnauthorizedException.class);
|
||||
|
||||
githubPATFetcher.fetchPersonalAccessToken(subject, GithubApiClient.GITHUB_SERVER);
|
||||
githubPATFetcher.fetchPersonalAccessToken(subject, wireMockServer.url("/"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -171,7 +171,7 @@ public class GithubPersonalAccessTokenFetcherTest {
|
|||
when(oAuthAPI.getToken(anyString())).thenReturn(oAuthToken);
|
||||
|
||||
stubFor(
|
||||
get(urlEqualTo("/user"))
|
||||
get(urlEqualTo("/api/v3/user"))
|
||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("token " + githubOauthToken))
|
||||
.willReturn(
|
||||
aResponse()
|
||||
|
|
@ -182,14 +182,14 @@ public class GithubPersonalAccessTokenFetcherTest {
|
|||
.withBodyFile("github/rest/user/response.json")));
|
||||
|
||||
PersonalAccessToken token =
|
||||
githubPATFetcher.fetchPersonalAccessToken(subject, GithubApiClient.GITHUB_SERVER);
|
||||
githubPATFetcher.fetchPersonalAccessToken(subject, wireMockServer.url("/"));
|
||||
assertNotNull(token);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldValidatePersonalToken() throws Exception {
|
||||
stubFor(
|
||||
get(urlEqualTo("/user"))
|
||||
get(urlEqualTo("/api/v3/user"))
|
||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("token " + githubOauthToken))
|
||||
.willReturn(
|
||||
aResponse()
|
||||
|
|
@ -201,7 +201,7 @@ public class GithubPersonalAccessTokenFetcherTest {
|
|||
|
||||
PersonalAccessToken token =
|
||||
new PersonalAccessToken(
|
||||
"https://github.com",
|
||||
wireMockServer.url("/"),
|
||||
"cheUser",
|
||||
"username",
|
||||
"123456789",
|
||||
|
|
@ -215,7 +215,7 @@ public class GithubPersonalAccessTokenFetcherTest {
|
|||
@Test
|
||||
public void shouldValidateOauthToken() throws Exception {
|
||||
stubFor(
|
||||
get(urlEqualTo("/user"))
|
||||
get(urlEqualTo("/api/v3/user"))
|
||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("token " + githubOauthToken))
|
||||
.willReturn(
|
||||
aResponse()
|
||||
|
|
@ -227,7 +227,7 @@ public class GithubPersonalAccessTokenFetcherTest {
|
|||
|
||||
PersonalAccessToken token =
|
||||
new PersonalAccessToken(
|
||||
"https://github.com",
|
||||
wireMockServer.url("/"),
|
||||
"cheUser",
|
||||
"username",
|
||||
"123456789",
|
||||
|
|
@ -240,11 +240,11 @@ public class GithubPersonalAccessTokenFetcherTest {
|
|||
|
||||
@Test
|
||||
public void shouldNotValidateExpiredOauthToken() throws Exception {
|
||||
stubFor(get(urlEqualTo("/user")).willReturn(aResponse().withStatus(HTTP_FORBIDDEN)));
|
||||
stubFor(get(urlEqualTo("/api/v3/user")).willReturn(aResponse().withStatus(HTTP_FORBIDDEN)));
|
||||
|
||||
PersonalAccessToken token =
|
||||
new PersonalAccessToken(
|
||||
"https://github.com",
|
||||
wireMockServer.url("/"),
|
||||
"cheUser",
|
||||
"username",
|
||||
"123456789",
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ package org.eclipse.che.api.factory.server.github;
|
|||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.testng.Assert.*;
|
||||
|
||||
|
|
@ -45,8 +44,7 @@ public class GithubScmFileResolverTest {
|
|||
@BeforeMethod
|
||||
protected void init() {
|
||||
githubURLParser =
|
||||
new GithubURLParser(
|
||||
personalAccessTokenManager, devfileFilenamesProvider, mock(GithubApiClient.class));
|
||||
new GithubURLParser(personalAccessTokenManager, devfileFilenamesProvider, null);
|
||||
assertNotNull(this.githubURLParser);
|
||||
githubScmFileResolver =
|
||||
new GithubScmFileResolver(
|
||||
|
|
@ -65,7 +63,7 @@ public class GithubScmFileResolverTest {
|
|||
@Test
|
||||
public void checkValidAcceptUrl() {
|
||||
// should be accepted
|
||||
assertTrue(githubScmFileResolver.accept("http://github.com/test/repo.git"));
|
||||
assertTrue(githubScmFileResolver.accept("https://github.com/test/repo.git"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -77,7 +75,8 @@ public class GithubScmFileResolverTest {
|
|||
when(personalAccessTokenManager.fetchAndSave(any(), anyString()))
|
||||
.thenReturn(personalAccessToken);
|
||||
|
||||
String content = githubScmFileResolver.fileContent("http://github.com/test/repo.git", filename);
|
||||
String content =
|
||||
githubScmFileResolver.fileContent("https://github.com/test/repo.git", filename);
|
||||
|
||||
assertEquals(content, rawContent);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue