feat: adding OAuth support to GitHub factory. (#28)
* Initial work on adding OAuth support to GitHub factory. Signed-off-by: cccs-eric <eric.ladouceur@cyber.gc.ca>pull/41/head
parent
0d7a511a67
commit
21edcc0cda
|
|
@ -178,6 +178,7 @@ public class WsMasterModule extends AbstractModule {
|
|||
install(new org.eclipse.che.api.factory.server.scm.KubernetesScmModule());
|
||||
install(new org.eclipse.che.api.factory.server.bitbucket.BitbucketServerModule());
|
||||
install(new org.eclipse.che.api.factory.server.gitlab.GitlabModule());
|
||||
install(new org.eclipse.che.api.factory.server.github.GithubModule());
|
||||
|
||||
bind(org.eclipse.che.api.core.rest.ApiInfoService.class);
|
||||
bind(org.eclipse.che.api.ssh.server.SshService.class);
|
||||
|
|
|
|||
|
|
@ -26,10 +26,22 @@
|
|||
<findbugs.failonerror>true</findbugs.failonerror>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.inject</groupId>
|
||||
<artifactId>guice</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.inject</groupId>
|
||||
<artifactId>javax.inject</artifactId>
|
||||
|
|
@ -38,6 +50,14 @@
|
|||
<groupId>javax.validation</groupId>
|
||||
<artifactId>validation-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-auth</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-auth-shared</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-core</artifactId>
|
||||
|
|
@ -66,11 +86,29 @@
|
|||
<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-lang</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.tomakehurst</groupId>
|
||||
<artifactId>wiremock-jre8-standalone</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-commons-json</artifactId>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* Copyright (c) 2012-2021 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.github;
|
||||
|
||||
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 com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.base.Splitter;
|
||||
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.time.Duration;
|
||||
import java.util.Optional;
|
||||
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;
|
||||
|
||||
/** GitHub API operations helper. */
|
||||
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";
|
||||
|
||||
/** GitHub HTTP header containing OAuth scopes. */
|
||||
public static final String GITHUB_OAUTH_SCOPES_HEADER = "X-OAuth-Scopes";
|
||||
|
||||
private final HttpClient httpClient;
|
||||
private final URI apiServerUrl;
|
||||
private final URI scmServerUrl;
|
||||
|
||||
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);
|
||||
this.httpClient =
|
||||
HttpClient.newBuilder()
|
||||
.executor(
|
||||
Executors.newCachedThreadPool(
|
||||
new ThreadFactoryBuilder()
|
||||
.setUncaughtExceptionHandler(LoggingUncaughtExceptionHandler.getInstance())
|
||||
.setNameFormat(GithubApiClient.class.getName() + "-%d")
|
||||
.setDaemon(true)
|
||||
.build()))
|
||||
.connectTimeout(DEFAULT_HTTP_TIMEOUT)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user associated with the provided OAuth access token.
|
||||
*
|
||||
* @see https://docs.github.com/en/rest/reference/users#get-the-authenticated-user
|
||||
* @param authenticationToken OAuth access token used by the user.
|
||||
* @return Information about the user associated with the token
|
||||
* @throws ScmItemNotFoundException
|
||||
* @throws ScmCommunicationException
|
||||
* @throws ScmBadRequestException
|
||||
*/
|
||||
public GithubUser getUser(String authenticationToken)
|
||||
throws ScmItemNotFoundException, ScmCommunicationException, ScmBadRequestException {
|
||||
final URI uri = apiServerUrl.resolve("/user");
|
||||
HttpRequest request = buildGithubApiRequest(uri, authenticationToken);
|
||||
LOG.trace("executeRequest={}", request);
|
||||
return executeRequest(
|
||||
httpClient,
|
||||
request,
|
||||
response -> {
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(response.body(), GithubUser.class);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the scopes of the OAuth token.
|
||||
*
|
||||
* <p>See GitHub documentation at
|
||||
* https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps
|
||||
*
|
||||
* @param authenticationToken The OAuth token to inspect.
|
||||
* @return Array of scopes from the supplied token, empty array if no scope.
|
||||
* @throws ScmItemNotFoundException
|
||||
* @throws ScmCommunicationException
|
||||
* @throws ScmBadRequestException
|
||||
*/
|
||||
public String[] getTokenScopes(String authenticationToken)
|
||||
throws ScmItemNotFoundException, ScmCommunicationException, ScmBadRequestException {
|
||||
final URI uri = apiServerUrl.resolve("/user");
|
||||
HttpRequest request = buildGithubApiRequest(uri, authenticationToken);
|
||||
LOG.trace("executeRequest={}", request);
|
||||
return executeRequest(
|
||||
httpClient,
|
||||
request,
|
||||
response -> {
|
||||
Optional<String> scopes = response.headers().firstValue(GITHUB_OAUTH_SCOPES_HEADER);
|
||||
return Splitter.on(',')
|
||||
.trimResults()
|
||||
.omitEmptyStrings()
|
||||
.splitToList(scopes.orElse(""))
|
||||
.toArray(String[]::new);
|
||||
});
|
||||
}
|
||||
|
||||
private HttpRequest buildGithubApiRequest(URI uri, String authenticationToken) {
|
||||
return HttpRequest.newBuilder(uri)
|
||||
.headers(
|
||||
"Authorization",
|
||||
"token " + authenticationToken,
|
||||
"Accept",
|
||||
"application/vnd.github.v3+json")
|
||||
.timeout(DEFAULT_HTTP_TIMEOUT)
|
||||
.build();
|
||||
}
|
||||
|
||||
private <T> T executeRequest(
|
||||
HttpClient httpClient,
|
||||
HttpRequest request,
|
||||
Function<HttpResponse<InputStream>, T> responseConverter)
|
||||
throws ScmBadRequestException, ScmItemNotFoundException, ScmCommunicationException {
|
||||
try {
|
||||
HttpResponse<InputStream> 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.toString());
|
||||
}
|
||||
}
|
||||
} catch (IOException | InterruptedException | UncheckedIOException e) {
|
||||
throw new ScmCommunicationException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided url belongs to this client (GitHub)
|
||||
*
|
||||
* @param scmServerUrl the SCM url to verify
|
||||
* @return If the provided url is recognized by the current client
|
||||
*/
|
||||
public boolean isConnected(String scmServerUrl) {
|
||||
return this.scmServerUrl.equals(URI.create(scmServerUrl));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (c) 2012-2021 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.github;
|
||||
|
||||
import org.eclipse.che.api.factory.server.scm.AuthorizingFileContentProvider;
|
||||
import org.eclipse.che.api.factory.server.scm.GitCredentialManager;
|
||||
import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager;
|
||||
import org.eclipse.che.api.workspace.server.devfile.URLFetcher;
|
||||
|
||||
/** Github specific authorizing file content provider. */
|
||||
class GithubAuthorizingFileContentProvider extends AuthorizingFileContentProvider<GithubUrl> {
|
||||
|
||||
GithubAuthorizingFileContentProvider(
|
||||
GithubUrl githubUrl,
|
||||
URLFetcher urlFetcher,
|
||||
GitCredentialManager gitCredentialManager,
|
||||
PersonalAccessTokenManager personalAccessTokenManager) {
|
||||
super(githubUrl, urlFetcher, personalAccessTokenManager, gitCredentialManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatting OAuth token as HTTP Authorization header.
|
||||
*
|
||||
* <p>GitHub Authorization HTTP header format is described here:
|
||||
* https://docs.github.com/en/rest/overview/resources-in-the-rest-api#oauth2-token-sent-in-a-header
|
||||
*/
|
||||
@Override
|
||||
protected String formatAuthorization(String token) {
|
||||
return "token " + token;
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,8 @@ import javax.validation.constraints.NotNull;
|
|||
import org.eclipse.che.api.core.ApiException;
|
||||
import org.eclipse.che.api.core.BadRequestException;
|
||||
import org.eclipse.che.api.factory.server.DefaultFactoryParameterResolver;
|
||||
import org.eclipse.che.api.factory.server.scm.GitCredentialManager;
|
||||
import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager;
|
||||
import org.eclipse.che.api.factory.server.urlfactory.ProjectConfigDtoMerger;
|
||||
import org.eclipse.che.api.factory.server.urlfactory.URLFactoryBuilder;
|
||||
import org.eclipse.che.api.factory.shared.dto.FactoryDevfileV2Dto;
|
||||
|
|
@ -42,13 +44,19 @@ import org.eclipse.che.api.workspace.shared.dto.devfile.ProjectDto;
|
|||
public class GithubFactoryParametersResolver extends DefaultFactoryParameterResolver {
|
||||
|
||||
/** Parser which will allow to check validity of URLs and create objects. */
|
||||
private GithubURLParser githubUrlParser;
|
||||
private final GithubURLParser githubUrlParser;
|
||||
|
||||
/** Builder allowing to build objects from github URL. */
|
||||
private GithubSourceStorageBuilder githubSourceStorageBuilder;
|
||||
private final GithubSourceStorageBuilder githubSourceStorageBuilder;
|
||||
|
||||
/** ProjectDtoMerger */
|
||||
private ProjectConfigDtoMerger projectConfigDtoMerger;
|
||||
private final ProjectConfigDtoMerger projectConfigDtoMerger;
|
||||
|
||||
/** Git credential manager. */
|
||||
private final GitCredentialManager gitCredentialManager;
|
||||
|
||||
/** Personal Access Token manager used when fetching protected content. */
|
||||
private final PersonalAccessTokenManager personalAccessTokenManager;
|
||||
|
||||
@Inject
|
||||
public GithubFactoryParametersResolver(
|
||||
|
|
@ -56,11 +64,15 @@ public class GithubFactoryParametersResolver extends DefaultFactoryParameterReso
|
|||
URLFetcher urlFetcher,
|
||||
GithubSourceStorageBuilder githubSourceStorageBuilder,
|
||||
URLFactoryBuilder urlFactoryBuilder,
|
||||
ProjectConfigDtoMerger projectConfigDtoMerger) {
|
||||
ProjectConfigDtoMerger projectConfigDtoMerger,
|
||||
GitCredentialManager gitCredentialManager,
|
||||
PersonalAccessTokenManager personalAccessTokenManager) {
|
||||
super(urlFactoryBuilder, urlFetcher);
|
||||
this.githubUrlParser = githubUrlParser;
|
||||
this.githubSourceStorageBuilder = githubSourceStorageBuilder;
|
||||
this.projectConfigDtoMerger = projectConfigDtoMerger;
|
||||
this.gitCredentialManager = gitCredentialManager;
|
||||
this.personalAccessTokenManager = personalAccessTokenManager;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -93,7 +105,8 @@ public class GithubFactoryParametersResolver extends DefaultFactoryParameterReso
|
|||
return urlFactoryBuilder
|
||||
.createFactoryFromDevfile(
|
||||
githubUrl,
|
||||
new GithubFileContentProvider(githubUrl, urlFetcher),
|
||||
new GithubAuthorizingFileContentProvider(
|
||||
githubUrl, urlFetcher, gitCredentialManager, personalAccessTokenManager),
|
||||
extractOverrideParams(factoryParameters))
|
||||
.orElseGet(() -> newDto(FactoryDto.class).withV(CURRENT_VERSION).withSource("repo"))
|
||||
.acceptVisitor(new GithubFactoryVisitor(githubUrl));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2012-2021 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.github;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.multibindings.Multibinder;
|
||||
import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenFetcher;
|
||||
|
||||
public class GithubModule extends AbstractModule {
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
Multibinder<PersonalAccessTokenFetcher> tokenFetcherMultibinder =
|
||||
Multibinder.newSetBinder(binder(), PersonalAccessTokenFetcher.class);
|
||||
tokenFetcherMultibinder.addBinding().to(GithubPersonalAccessTokenFetcher.class);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* Copyright (c) 2012-2021 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.github;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import org.eclipse.che.api.auth.shared.dto.OAuthToken;
|
||||
import org.eclipse.che.api.core.BadRequestException;
|
||||
import org.eclipse.che.api.core.ConflictException;
|
||||
import org.eclipse.che.api.core.ForbiddenException;
|
||||
import org.eclipse.che.api.core.NotFoundException;
|
||||
import org.eclipse.che.api.core.ServerException;
|
||||
import org.eclipse.che.api.core.UnauthorizedException;
|
||||
import org.eclipse.che.api.factory.server.scm.PersonalAccessToken;
|
||||
import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenFetcher;
|
||||
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.lang.NameGenerator;
|
||||
import org.eclipse.che.commons.subject.Subject;
|
||||
import org.eclipse.che.security.oauth.OAuthAPI;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/** GitHub OAuth token retriever. */
|
||||
public class GithubPersonalAccessTokenFetcher implements PersonalAccessTokenFetcher {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GithubPersonalAccessTokenFetcher.class);
|
||||
private final String apiEndpoint;
|
||||
private final OAuthAPI oAuthAPI;
|
||||
|
||||
/** GitHub API client. */
|
||||
private final GithubApiClient githubApiClient;
|
||||
|
||||
/** Name of this OAuth provider as found in OAuthAPI. */
|
||||
private static final String OAUTH_PROVIDER_NAME = "github";
|
||||
|
||||
/** Collection of OAuth scopes required to make integration with GitHub work. */
|
||||
public static final Set<String> DEFAULT_TOKEN_SCOPES = ImmutableSet.of("repo");
|
||||
|
||||
/**
|
||||
* Map of OAuth GitHub scopes where each key is a scope and its value is the parent scope. The
|
||||
* parent scope includes all of its children scopes. This map is used when determining if a token
|
||||
* has the required scopes. See
|
||||
* https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps#available-scopes
|
||||
*/
|
||||
private static final Map<String, String> SCOPE_MAP =
|
||||
ImmutableMap.<String, String>builderWithExpectedSize(35)
|
||||
.put("repo", "repo")
|
||||
.put("repo:status", "repo")
|
||||
.put("repo_deployment", "repo")
|
||||
.put("public_repo", "repo")
|
||||
.put("repo:invite", "repo")
|
||||
.put("security_events", "repo")
|
||||
//
|
||||
.put("workflow", "workflow")
|
||||
//
|
||||
.put("write:packages", "write:packages")
|
||||
.put("read:packages", "write:packages")
|
||||
//
|
||||
.put("delete:packages", "delete:packages")
|
||||
//
|
||||
.put("admin:org", "admin:org")
|
||||
.put("write:org", "admin:org")
|
||||
.put("read:org", "admin:org")
|
||||
//
|
||||
.put("admin:public_key", "admin:public_key")
|
||||
.put("write:public_key", "admin:public_key")
|
||||
.put("read:public_key", "admin:public_key")
|
||||
//
|
||||
.put("admin:repo_hook", "admin:repo_hook")
|
||||
.put("write:repo_hook", "admin:repo_hook")
|
||||
.put("read:repo_hook", "admin:repo_hook")
|
||||
//
|
||||
.put("admin:org_hook", "admin:org_hook")
|
||||
//
|
||||
.put("gist", "gist")
|
||||
//
|
||||
.put("notifications", "notifications")
|
||||
//
|
||||
.put("user", "user")
|
||||
.put("read:user", "user")
|
||||
.put("user:email", "user")
|
||||
.put("user:follow", "user")
|
||||
//
|
||||
.put("delete_repo", "delete_repo")
|
||||
//
|
||||
.put("write:discussion", "write:discussion")
|
||||
.put("read:discussion", "write:discussion")
|
||||
//
|
||||
.put("admin:enterprise", "admin:enterprise")
|
||||
.put("manage_billing:enterprise", "admin:enterprise")
|
||||
.put("read:enterprise", "admin:enterprise")
|
||||
//
|
||||
.put("admin:gpg_key", "admin:gpg_key")
|
||||
.put("write:gpg_key", "admin:gpg_key")
|
||||
.put("read:gpg_key", "admin:gpg_key")
|
||||
.build();
|
||||
|
||||
@Inject
|
||||
public GithubPersonalAccessTokenFetcher(@Named("che.api") String apiEndpoint, OAuthAPI oAuthAPI) {
|
||||
this(apiEndpoint, oAuthAPI, new GithubApiClient());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor used for testing only.
|
||||
*
|
||||
* @param apiEndpoint
|
||||
* @param oAuthAPI
|
||||
* @param githubApiClient
|
||||
*/
|
||||
GithubPersonalAccessTokenFetcher(
|
||||
String apiEndpoint, OAuthAPI oAuthAPI, GithubApiClient githubApiClient) {
|
||||
this.apiEndpoint = apiEndpoint;
|
||||
this.oAuthAPI = oAuthAPI;
|
||||
this.githubApiClient = githubApiClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PersonalAccessToken fetchPersonalAccessToken(Subject cheSubject, String scmServerUrl)
|
||||
throws ScmUnauthorizedException, ScmCommunicationException {
|
||||
OAuthToken oAuthToken;
|
||||
try {
|
||||
oAuthToken = oAuthAPI.getToken(OAUTH_PROVIDER_NAME);
|
||||
// Find the user associated to the OAuth token by querying the GitHub API.
|
||||
GithubUser user = githubApiClient.getUser(oAuthToken.getToken());
|
||||
PersonalAccessToken token =
|
||||
new PersonalAccessToken(
|
||||
scmServerUrl,
|
||||
cheSubject.getUserId(),
|
||||
user.getLogin(),
|
||||
Long.toString(user.getId()),
|
||||
NameGenerator.generate("oauth2-", 5),
|
||||
NameGenerator.generate("id-", 5),
|
||||
oAuthToken.getToken());
|
||||
Optional<Boolean> valid = isValid(token);
|
||||
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
|
||||
+ "' and was '"
|
||||
+ token.getScmProviderUrl()
|
||||
+ "'");
|
||||
} else if (!valid.get()) {
|
||||
throw new ScmCommunicationException(
|
||||
"Current token doesn't have the necessary privileges. Please make sure Che app scopes are correct and containing at least: "
|
||||
+ DEFAULT_TOKEN_SCOPES.toString());
|
||||
}
|
||||
return token;
|
||||
} catch (UnauthorizedException e) {
|
||||
throw new ScmUnauthorizedException(
|
||||
cheSubject.getUserName()
|
||||
+ " is not authorized in "
|
||||
+ OAUTH_PROVIDER_NAME
|
||||
+ " OAuth provider.",
|
||||
OAUTH_PROVIDER_NAME,
|
||||
"2.0",
|
||||
getLocalAuthenticateUrl());
|
||||
} catch (NotFoundException
|
||||
| ServerException
|
||||
| ForbiddenException
|
||||
| BadRequestException
|
||||
| ScmItemNotFoundException
|
||||
| ScmBadRequestException
|
||||
| ConflictException e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
throw new ScmCommunicationException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Boolean> isValid(PersonalAccessToken personalAccessToken)
|
||||
throws ScmCommunicationException, ScmUnauthorizedException {
|
||||
if (!githubApiClient.isConnected(personalAccessToken.getScmProviderUrl())) {
|
||||
LOG.debug("not a valid url {} for current fetcher ", personalAccessToken.getScmProviderUrl());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
String[] scopes = githubApiClient.getTokenScopes(personalAccessToken.getToken());
|
||||
return Optional.of(Boolean.valueOf(containsScopes(scopes, DEFAULT_TOKEN_SCOPES)));
|
||||
} catch (ScmItemNotFoundException | ScmCommunicationException | ScmBadRequestException e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
throw new ScmCommunicationException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the tokenScopes array contains the requiredScopes.
|
||||
*
|
||||
* @param tokenScopes Scopes from token
|
||||
* @param requiredScopes Mandatory scopes
|
||||
* @return If all mandatory scopes are contained in the token's scopes
|
||||
*/
|
||||
boolean containsScopes(String[] tokenScopes, Set<String> requiredScopes) {
|
||||
Arrays.sort(tokenScopes);
|
||||
// We need check that the token has the required minimal scopes. The scopes can be normalized
|
||||
// by GitHub, so we need to be careful for sub-scopes being included in parent scopes.
|
||||
for (String requiredScope : requiredScopes) {
|
||||
String parentScope = SCOPE_MAP.get(requiredScope);
|
||||
if (parentScope == null) {
|
||||
// requiredScope is not recognized as a GitHub scope, so just skip it.
|
||||
continue;
|
||||
}
|
||||
if (Arrays.binarySearch(tokenScopes, parentScope) < 0
|
||||
&& Arrays.binarySearch(tokenScopes, requiredScope) < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private String getLocalAuthenticateUrl() {
|
||||
return apiEndpoint
|
||||
+ "/oauth/authenticate?oauth_provider="
|
||||
+ OAUTH_PROVIDER_NAME
|
||||
+ "&request_method=POST&signature_method=rsa";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright (c) 2012-2021 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.github;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import java.util.Objects;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class GithubUser {
|
||||
|
||||
private long id;
|
||||
private String login;
|
||||
private String email;
|
||||
private String name;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getLogin() {
|
||||
return login;
|
||||
}
|
||||
|
||||
public void setLogin(String login) {
|
||||
this.login = login;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GithubUser{"
|
||||
+ "id="
|
||||
+ id
|
||||
+ ", login='"
|
||||
+ login
|
||||
+ '\''
|
||||
+ ", email='"
|
||||
+ email
|
||||
+ '\''
|
||||
+ ", name='"
|
||||
+ name
|
||||
+ '\''
|
||||
+ '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
GithubUser that = (GithubUser) o;
|
||||
return id == that.id
|
||||
&& Objects.equals(login, that.login)
|
||||
&& Objects.equals(email, that.email)
|
||||
&& Objects.equals(name, that.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, login, email, name);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Copyright (c) 2012-2021 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.github;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
|
||||
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.testng.Assert.assertEquals;
|
||||
import static org.testng.Assert.assertEqualsNoOrder;
|
||||
import static org.testng.Assert.assertFalse;
|
||||
import static org.testng.Assert.assertNotNull;
|
||||
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 com.google.common.net.HttpHeaders;
|
||||
import org.mockito.testng.MockitoTestNGListener;
|
||||
import org.testng.annotations.AfterMethod;
|
||||
import org.testng.annotations.BeforeMethod;
|
||||
import org.testng.annotations.Listeners;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
@Listeners(MockitoTestNGListener.class)
|
||||
public class GithubApiClientTest {
|
||||
|
||||
private GithubApiClient client;
|
||||
WireMockServer wireMockServer;
|
||||
WireMock wireMock;
|
||||
|
||||
@BeforeMethod
|
||||
void start() {
|
||||
wireMockServer =
|
||||
new WireMockServer(wireMockConfig().notifier(new Slf4jNotifier(false)).dynamicPort());
|
||||
wireMockServer.start();
|
||||
WireMock.configureFor("localhost", wireMockServer.port());
|
||||
wireMock = new WireMock("localhost", wireMockServer.port());
|
||||
client = new GithubApiClient(wireMockServer.url("/"));
|
||||
}
|
||||
|
||||
@AfterMethod
|
||||
void stop() {
|
||||
wireMockServer.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUser() throws Exception {
|
||||
stubFor(
|
||||
get(urlEqualTo("/user"))
|
||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("token token1"))
|
||||
.willReturn(
|
||||
aResponse()
|
||||
.withHeader("Content-Type", "application/json; charset=utf-8")
|
||||
.withBodyFile("github/rest/user/response.json")));
|
||||
|
||||
GithubUser user = client.getUser("token1");
|
||||
assertNotNull(user, "GitHub API should have returned a non-null user object");
|
||||
assertEquals(user.getId(), 123456789, "GitHub user id was not parsed properly by client");
|
||||
assertEquals(
|
||||
user.getLogin(), "github-user", "GitHub user login was not parsed properly by client");
|
||||
assertEquals(
|
||||
user.getEmail(),
|
||||
"github-user@acme.com",
|
||||
"GitHub user email was not parsed properly by client");
|
||||
assertEquals(
|
||||
user.getName(), "Github User", "GitHub user name was not parsed properly by client");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetTokenScopes() throws Exception {
|
||||
stubFor(
|
||||
get(urlEqualTo("/user"))
|
||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("token token1"))
|
||||
.willReturn(
|
||||
aResponse()
|
||||
.withHeader("Content-Type", "application/json; charset=utf-8")
|
||||
.withHeader(GithubApiClient.GITHUB_OAUTH_SCOPES_HEADER, "repo, user:email")
|
||||
.withBodyFile("github/rest/user/response.json")));
|
||||
|
||||
String[] scopes = client.getTokenScopes("token1");
|
||||
String[] expectedScopes = {"repo", "user:email"};
|
||||
assertNotNull(scopes, "GitHub API should have returned a non-null scope array");
|
||||
assertEqualsNoOrder(
|
||||
scopes, expectedScopes, "Returned scope array does not match expected values");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetTokenScopesWithNoScopeHeader() throws Exception {
|
||||
stubFor(
|
||||
get(urlEqualTo("/user"))
|
||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("token token1"))
|
||||
.willReturn(
|
||||
aResponse()
|
||||
.withHeader("Content-Type", "application/json; charset=utf-8")
|
||||
.withBodyFile("github/rest/user/response.json")));
|
||||
|
||||
String[] scopes = client.getTokenScopes("token1");
|
||||
assertNotNull(scopes, "GitHub API should have returned a non-null scope array");
|
||||
assertEquals(
|
||||
scopes.length,
|
||||
0,
|
||||
"A response with no "
|
||||
+ GithubApiClient.GITHUB_OAUTH_SCOPES_HEADER
|
||||
+ " header should return an empty array");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetTokenScopesWithNoScope() throws Exception {
|
||||
stubFor(
|
||||
get(urlEqualTo("/user"))
|
||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("token token1"))
|
||||
.willReturn(
|
||||
aResponse()
|
||||
.withHeader("Content-Type", "application/json; charset=utf-8")
|
||||
.withHeader(GithubApiClient.GITHUB_OAUTH_SCOPES_HEADER, "")
|
||||
.withBodyFile("github/rest/user/response.json")));
|
||||
|
||||
String[] scopes = client.getTokenScopes("token1");
|
||||
assertNotNull(scopes, "GitHub API should have returned a non-null scope array");
|
||||
assertEquals(
|
||||
scopes.length,
|
||||
0,
|
||||
"A response with empty "
|
||||
+ GithubApiClient.GITHUB_OAUTH_SCOPES_HEADER
|
||||
+ " header should return an empty array");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnFalseOnConnectedToOtherHost() {
|
||||
assertFalse(client.isConnected("https://other.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnTrueWhenConnectedToGithub() {
|
||||
assertTrue(client.isConnected("https://github.com"));
|
||||
}
|
||||
}
|
||||
|
|
@ -14,18 +14,30 @@ package org.eclipse.che.api.factory.server.github;
|
|||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import org.eclipse.che.api.factory.server.scm.GitCredentialManager;
|
||||
import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager;
|
||||
import org.eclipse.che.api.workspace.server.devfile.FileContentProvider;
|
||||
import org.eclipse.che.api.workspace.server.devfile.URLFetcher;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.testng.MockitoTestNGListener;
|
||||
import org.testng.annotations.Listeners;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
public class GithubFileContentProviderTest {
|
||||
@Listeners(MockitoTestNGListener.class)
|
||||
public class GithubAuthorizingFileContentProviderTest {
|
||||
|
||||
@Mock private GitCredentialManager gitCredentialManager;
|
||||
|
||||
@Mock private PersonalAccessTokenManager personalAccessTokenManager;
|
||||
|
||||
@Test
|
||||
public void shouldExpandRelativePaths() throws Exception {
|
||||
URLFetcher urlFetcher = Mockito.mock(URLFetcher.class);
|
||||
GithubUrl githubUrl = new GithubUrl().withUsername("eclipse").withRepository("che");
|
||||
FileContentProvider fileContentProvider = new GithubFileContentProvider(githubUrl, urlFetcher);
|
||||
FileContentProvider fileContentProvider =
|
||||
new GithubAuthorizingFileContentProvider(
|
||||
githubUrl, urlFetcher, gitCredentialManager, personalAccessTokenManager);
|
||||
fileContentProvider.fetchContent("devfile.yaml");
|
||||
verify(urlFetcher).fetch(eq("https://raw.githubusercontent.com/eclipse/che/HEAD/devfile.yaml"));
|
||||
}
|
||||
|
|
@ -34,7 +46,9 @@ public class GithubFileContentProviderTest {
|
|||
public void shouldPreserveAbsolutePaths() throws Exception {
|
||||
URLFetcher urlFetcher = Mockito.mock(URLFetcher.class);
|
||||
GithubUrl githubUrl = new GithubUrl().withUsername("eclipse").withRepository("che");
|
||||
FileContentProvider fileContentProvider = new GithubFileContentProvider(githubUrl, urlFetcher);
|
||||
FileContentProvider fileContentProvider =
|
||||
new GithubAuthorizingFileContentProvider(
|
||||
githubUrl, urlFetcher, gitCredentialManager, personalAccessTokenManager);
|
||||
String url = "https://raw.githubusercontent.com/foo/bar/devfile.yaml";
|
||||
fileContentProvider.fetchContent(url);
|
||||
verify(urlFetcher).fetch(eq(url));
|
||||
|
|
@ -33,6 +33,8 @@ import java.util.Collections;
|
|||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.eclipse.che.api.core.model.factory.ScmInfo;
|
||||
import org.eclipse.che.api.factory.server.scm.GitCredentialManager;
|
||||
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.ProjectConfigDtoMerger;
|
||||
import org.eclipse.che.api.factory.server.urlfactory.RemoteFactoryUrl;
|
||||
|
|
@ -79,6 +81,11 @@ public class GithubFactoryParametersResolverTest {
|
|||
/** Parser which will allow to check validity of URLs and create objects. */
|
||||
@Mock private URLFactoryBuilder urlFactoryBuilder;
|
||||
|
||||
// TODO: Verify if we should add test cases involving credential manager and patManager
|
||||
@Mock private GitCredentialManager gitCredentialManager;
|
||||
|
||||
@Mock private PersonalAccessTokenManager personalAccessTokenManager;
|
||||
|
||||
/**
|
||||
* Capturing the location parameter when calling {@link
|
||||
* URLFactoryBuilder#createFactoryFromDevfile(RemoteFactoryUrl, FileContentProvider, Map)}
|
||||
|
|
@ -98,7 +105,9 @@ public class GithubFactoryParametersResolverTest {
|
|||
urlFetcher,
|
||||
githubSourceStorageBuilder,
|
||||
urlFactoryBuilder,
|
||||
projectConfigDtoMerger);
|
||||
projectConfigDtoMerger,
|
||||
gitCredentialManager,
|
||||
personalAccessTokenManager);
|
||||
assertNotNull(this.githubFactoryParametersResolver);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
* Copyright (c) 2012-2021 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.github;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
|
||||
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.eclipse.che.dto.server.DtoFactory.newDto;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.testng.Assert.*;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.client.WireMock;
|
||||
import com.github.tomakehurst.wiremock.common.Slf4jNotifier;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import java.util.Collections;
|
||||
import org.eclipse.che.api.auth.shared.dto.OAuthToken;
|
||||
import org.eclipse.che.api.core.UnauthorizedException;
|
||||
import org.eclipse.che.api.factory.server.scm.PersonalAccessToken;
|
||||
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
|
||||
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
|
||||
import org.eclipse.che.commons.subject.Subject;
|
||||
import org.eclipse.che.commons.subject.SubjectImpl;
|
||||
import org.eclipse.che.security.oauth.OAuthAPI;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.testng.MockitoTestNGListener;
|
||||
import org.testng.annotations.AfterMethod;
|
||||
import org.testng.annotations.BeforeMethod;
|
||||
import org.testng.annotations.Listeners;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
@Listeners(MockitoTestNGListener.class)
|
||||
public class GithubPersonalAccessTokenFetcherTest {
|
||||
|
||||
@Mock OAuthAPI oAuthAPI;
|
||||
GithubPersonalAccessTokenFetcher githubPATFetcher;
|
||||
|
||||
final int httpPort = 3301;
|
||||
WireMockServer wireMockServer;
|
||||
WireMock wireMock;
|
||||
|
||||
final String githubOauthToken = "gho_token1";
|
||||
|
||||
@BeforeMethod
|
||||
void start() {
|
||||
|
||||
wireMockServer =
|
||||
new WireMockServer(wireMockConfig().notifier(new Slf4jNotifier(false)).port(httpPort));
|
||||
wireMockServer.start();
|
||||
WireMock.configureFor("localhost", httpPort);
|
||||
wireMock = new WireMock("localhost", httpPort);
|
||||
githubPATFetcher =
|
||||
new GithubPersonalAccessTokenFetcher(
|
||||
"http://che.api", oAuthAPI, new GithubApiClient(wireMockServer.url("/")));
|
||||
}
|
||||
|
||||
@AfterMethod
|
||||
void stop() {
|
||||
wireMockServer.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotValidateSCMServerWithTrailingSlash() throws Exception {
|
||||
stubFor(
|
||||
get(urlEqualTo("/user"))
|
||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("token " + githubOauthToken))
|
||||
.willReturn(
|
||||
aResponse()
|
||||
.withHeader("Content-Type", "application/json; charset=utf-8")
|
||||
.withHeader(GithubApiClient.GITHUB_OAUTH_SCOPES_HEADER, "repo")
|
||||
.withBodyFile("github/rest/user/response.json")));
|
||||
PersonalAccessToken personalAccessToken =
|
||||
new PersonalAccessToken(
|
||||
"https://github.com/",
|
||||
"cheUserId",
|
||||
"scmUserName",
|
||||
"scmUserId",
|
||||
"scmTokenName",
|
||||
"scmTokenId",
|
||||
githubOauthToken);
|
||||
assertTrue(
|
||||
githubPATFetcher.isValid(personalAccessToken).isEmpty(),
|
||||
"Should not validate SCM server with trailing /");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContainsScope() {
|
||||
String[] tokenScopes = {"repo", "notifications", "write:org", "admin:gpg_key"};
|
||||
assertTrue(
|
||||
githubPATFetcher.containsScopes(tokenScopes, ImmutableSet.of("repo")),
|
||||
"'repo' scope should have matched directly.");
|
||||
assertTrue(
|
||||
githubPATFetcher.containsScopes(tokenScopes, ImmutableSet.of("public_repo")),
|
||||
"'public_repo' scope should have matched since token has parent scope 'repo'.");
|
||||
assertTrue(
|
||||
githubPATFetcher.containsScopes(
|
||||
tokenScopes, ImmutableSet.of("read:gpg_key", "write:gpg_key")),
|
||||
"'admin:gpg_key' token scope should cover both scope requirement.");
|
||||
assertFalse(
|
||||
githubPATFetcher.containsScopes(tokenScopes, ImmutableSet.of("admin:org")),
|
||||
"'admin:org' scope should not match since token only has scope 'write:org'.");
|
||||
assertFalse(
|
||||
githubPATFetcher.containsScopes(tokenScopes, ImmutableSet.of("gist")),
|
||||
"'gist' shouldn't matche since it is not present in token scope");
|
||||
assertTrue(
|
||||
githubPATFetcher.containsScopes(tokenScopes, ImmutableSet.of("unknown", "repo")),
|
||||
"'unknown' is not even a valid GitHub scope, so it shouldn't have any impact.");
|
||||
assertTrue(
|
||||
githubPATFetcher.containsScopes(tokenScopes, Collections.emptySet()),
|
||||
"No required scope should always return true");
|
||||
assertFalse(
|
||||
githubPATFetcher.containsScopes(new String[0], ImmutableSet.of("repo")),
|
||||
"Token has no scope, so it should not match");
|
||||
assertTrue(
|
||||
githubPATFetcher.containsScopes(new String[0], Collections.emptySet()),
|
||||
"No scope requirement and a token with no scope should match");
|
||||
}
|
||||
|
||||
@Test(
|
||||
expectedExceptions = ScmCommunicationException.class,
|
||||
expectedExceptionsMessageRegExp =
|
||||
"Current token doesn't have the necessary privileges. Please make sure Che app scopes are correct and containing at least: \\[repo\\]")
|
||||
public void shouldThrowExceptionOnInsufficientTokenScopes() throws Exception {
|
||||
Subject subject = new SubjectImpl("Username", "id1", "token", false);
|
||||
OAuthToken oAuthToken = newDto(OAuthToken.class).withToken(githubOauthToken).withScope("");
|
||||
when(oAuthAPI.getToken(anyString())).thenReturn(oAuthToken);
|
||||
|
||||
stubFor(
|
||||
get(urlEqualTo("/user"))
|
||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("token " + githubOauthToken))
|
||||
.willReturn(
|
||||
aResponse()
|
||||
.withHeader("Content-Type", "application/json; charset=utf-8")
|
||||
.withHeader(GithubApiClient.GITHUB_OAUTH_SCOPES_HEADER, "")
|
||||
.withBodyFile("github/rest/user/response.json")));
|
||||
|
||||
githubPATFetcher.fetchPersonalAccessToken(subject, GithubApiClient.GITHUB_SERVER);
|
||||
}
|
||||
|
||||
@Test(
|
||||
expectedExceptions = ScmUnauthorizedException.class,
|
||||
expectedExceptionsMessageRegExp = "Username is not authorized in github OAuth provider.")
|
||||
public void shouldThrowUnauthorizedExceptionWhenUserNotLoggedIn() throws Exception {
|
||||
Subject subject = new SubjectImpl("Username", "id1", "token", false);
|
||||
when(oAuthAPI.getToken(anyString())).thenThrow(UnauthorizedException.class);
|
||||
|
||||
githubPATFetcher.fetchPersonalAccessToken(subject, GithubApiClient.GITHUB_SERVER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnToken() throws Exception {
|
||||
Subject subject = new SubjectImpl("Username", "id1", "token", false);
|
||||
OAuthToken oAuthToken = newDto(OAuthToken.class).withToken(githubOauthToken).withScope("repo");
|
||||
when(oAuthAPI.getToken(anyString())).thenReturn(oAuthToken);
|
||||
|
||||
stubFor(
|
||||
get(urlEqualTo("/user"))
|
||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("token " + githubOauthToken))
|
||||
.willReturn(
|
||||
aResponse()
|
||||
.withHeader("Content-Type", "application/json; charset=utf-8")
|
||||
.withHeader(GithubApiClient.GITHUB_OAUTH_SCOPES_HEADER, "repo")
|
||||
.withBodyFile("github/rest/user/response.json")));
|
||||
|
||||
PersonalAccessToken token =
|
||||
githubPATFetcher.fetchPersonalAccessToken(subject, GithubApiClient.GITHUB_SERVER);
|
||||
assertNotNull(token);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"login": "github-user",
|
||||
"id": 123456789,
|
||||
"node_id": "ddfsSDSDDJKHSDjhd",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/123456789?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/github-user",
|
||||
"html_url": "https://github.com/github-user",
|
||||
"followers_url": "https://api.github.com/users/github-user/followers",
|
||||
"following_url": "https://api.github.com/users/github-user/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/github-user/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/github-user/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/github-user/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/github-user/orgs",
|
||||
"repos_url": "https://api.github.com/users/github-user/repos",
|
||||
"events_url": "https://api.github.com/users/github-user/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/github-user/received_events",
|
||||
"type": "User",
|
||||
"site_admin": false,
|
||||
"name": "Github User",
|
||||
"company": "ACME",
|
||||
"blog": "https://acme.com/",
|
||||
"location": "Planet Earth",
|
||||
"email": "github-user@acme.com",
|
||||
"hireable": null,
|
||||
"bio": null,
|
||||
"twitter_username": null,
|
||||
"public_repos": 12,
|
||||
"public_gists": 3,
|
||||
"followers": 1,
|
||||
"following": 0,
|
||||
"created_at": "2019-10-11T15:46:45Z",
|
||||
"updated_at": "2021-06-21T12:38:04Z"
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Copyright (c) 2012-2021 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
|
||||
|
||||
-->
|
||||
<configuration>
|
||||
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n%nopex</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="stdout"/>
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
|
|
@ -68,7 +68,8 @@ public class AuthorizingFileContentProvider<T extends RemoteFactoryUrl>
|
|||
EnvironmentContext.getCurrent().getSubject(), remoteFactoryUrl.getHostName());
|
||||
if (token.isPresent()) {
|
||||
PersonalAccessToken personalAccessToken = token.get();
|
||||
String content = urlFetcher.fetch(requestURL, "Bearer " + personalAccessToken.getToken());
|
||||
String content =
|
||||
urlFetcher.fetch(requestURL, formatAuthorization(personalAccessToken.getToken()));
|
||||
gitCredentialManager.createOrReplace(personalAccessToken);
|
||||
return content;
|
||||
} else {
|
||||
|
|
@ -81,7 +82,7 @@ public class AuthorizingFileContentProvider<T extends RemoteFactoryUrl>
|
|||
personalAccessTokenManager.fetchAndSave(
|
||||
EnvironmentContext.getCurrent().getSubject(), remoteFactoryUrl.getHostName());
|
||||
String content =
|
||||
urlFetcher.fetch(requestURL, "Bearer " + personalAccessToken.getToken());
|
||||
urlFetcher.fetch(requestURL, formatAuthorization(personalAccessToken.getToken()));
|
||||
gitCredentialManager.createOrReplace(personalAccessToken);
|
||||
return content;
|
||||
} catch (ScmUnauthorizedException
|
||||
|
|
@ -109,4 +110,8 @@ public class AuthorizingFileContentProvider<T extends RemoteFactoryUrl>
|
|||
throw new DevfileException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
protected String formatAuthorization(String token) {
|
||||
return "Bearer " + token;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue