feat: Support enabling Github enterprise and SaaS simultaneously on Dev Spaces
Signed-off-by: Anatolii Bazko <abazko@redhat.com>pull/598/head
parent
2c198333ef
commit
d382f47b5e
|
|
@ -48,7 +48,9 @@ import org.eclipse.che.api.factory.server.bitbucket.BitbucketServerScmFileResolv
|
|||
import org.eclipse.che.api.factory.server.git.ssh.GitSshFactoryParametersResolver;
|
||||
import org.eclipse.che.api.factory.server.git.ssh.GitSshScmFileResolver;
|
||||
import org.eclipse.che.api.factory.server.github.GithubFactoryParametersResolver;
|
||||
import org.eclipse.che.api.factory.server.github.GithubFactoryParametersResolverSecond;
|
||||
import org.eclipse.che.api.factory.server.github.GithubScmFileResolver;
|
||||
import org.eclipse.che.api.factory.server.github.GithubScmFileResolverSecond;
|
||||
import org.eclipse.che.api.factory.server.gitlab.GitlabFactoryParametersResolver;
|
||||
import org.eclipse.che.api.factory.server.gitlab.GitlabScmFileResolver;
|
||||
import org.eclipse.che.api.metrics.WsMasterMetricsModule;
|
||||
|
|
@ -169,6 +171,9 @@ public class WsMasterModule extends AbstractModule {
|
|||
Multibinder<FactoryParametersResolver> factoryParametersResolverMultibinder =
|
||||
Multibinder.newSetBinder(binder(), FactoryParametersResolver.class);
|
||||
factoryParametersResolverMultibinder.addBinding().to(GithubFactoryParametersResolver.class);
|
||||
factoryParametersResolverMultibinder
|
||||
.addBinding()
|
||||
.to(GithubFactoryParametersResolverSecond.class);
|
||||
factoryParametersResolverMultibinder
|
||||
.addBinding()
|
||||
.to(BitbucketServerAuthorizingFactoryParametersResolver.class);
|
||||
|
|
@ -185,6 +190,7 @@ public class WsMasterModule extends AbstractModule {
|
|||
Multibinder<ScmFileResolver> scmFileResolverResolverMultibinder =
|
||||
Multibinder.newSetBinder(binder(), ScmFileResolver.class);
|
||||
scmFileResolverResolverMultibinder.addBinding().to(GithubScmFileResolver.class);
|
||||
scmFileResolverResolverMultibinder.addBinding().to(GithubScmFileResolverSecond.class);
|
||||
scmFileResolverResolverMultibinder.addBinding().to(BitbucketScmFileResolver.class);
|
||||
scmFileResolverResolverMultibinder.addBinding().to(GitlabScmFileResolver.class);
|
||||
scmFileResolverResolverMultibinder.addBinding().to(BitbucketServerScmFileResolver.class);
|
||||
|
|
|
|||
|
|
@ -144,22 +144,28 @@ che.oauth.azure.devops.redirecturis=https://${CHE_HOST}/api/oauth/callback
|
|||
# Configuration of the GitHub OAuth2 client. Used to obtain personal access tokens.
|
||||
# Location of the file with GitHub client id.
|
||||
che.oauth2.github.clientid_filepath=NULL
|
||||
che.oauth2.github.clientid_filepath_2=NULL
|
||||
|
||||
# Location of the file with GitHub client secret.
|
||||
che.oauth2.github.clientsecret_filepath=NULL
|
||||
che.oauth2.github.clientsecret_filepath_2=NULL
|
||||
|
||||
# GitHub OAuth authorization URI.
|
||||
che.oauth.github.authuri= https://github.com/login/oauth/authorize
|
||||
che.oauth.github.authuri_2= https://github.com/login/oauth/authorize
|
||||
|
||||
# GitHub OAuth token URI.
|
||||
che.oauth.github.tokenuri= https://github.com/login/oauth/access_token
|
||||
che.oauth.github.tokenuri_2= 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
|
||||
che.integration.github.oauth_endpoint_2=NULL
|
||||
|
||||
# GitHub server disable subdomain isolation flag.
|
||||
che.integration.github.disable_subdomain_isolation=false
|
||||
che.integration.github.disable_subdomain_isolation_2=false
|
||||
|
||||
# GitHub OAuth redirect URIs.
|
||||
# Separate multiple values with comma, for example: URI,URI,URI.
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@
|
|||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-auth-github</artifactId>
|
||||
<artifactId>che-core-api-auth-github-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ public class OpenShiftGitHubOAuthAuthenticator extends GitHubOAuthAuthenticator
|
|||
@Nullable @Named("che.oauth.github.tokenuri") String tokenUri)
|
||||
throws IOException {
|
||||
|
||||
super("NULL", "NULL", redirectUris, null, authUri, tokenUri);
|
||||
super("NULL", "NULL", redirectUris, null, authUri, tokenUri, "github");
|
||||
|
||||
if (!isNullOrEmpty(authUri)
|
||||
&& !isNullOrEmpty(tokenUri)
|
||||
|
|
|
|||
10
pom.xml
10
pom.xml
|
|
@ -687,6 +687,11 @@
|
|||
<artifactId>che-core-api-auth-github</artifactId>
|
||||
<version>${che.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-auth-github-common</artifactId>
|
||||
<version>${che.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-auth-gitlab</artifactId>
|
||||
|
|
@ -769,6 +774,11 @@
|
|||
<artifactId>che-core-api-factory-github</artifactId>
|
||||
<version>${che.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-factory-github-common</artifactId>
|
||||
<version>${che.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-factory-gitlab</artifactId>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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
|
||||
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<artifactId>che-master-parent</artifactId>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<version>7.76.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>che-core-api-auth-github-common</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Che Core :: API :: Authentication Github Common</name>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.http-client</groupId>
|
||||
<artifactId>google-http-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.inject</groupId>
|
||||
<artifactId>jakarta.inject-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-commons-lang</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.tomakehurst</groupId>
|
||||
<artifactId>wiremock-jre8-standalone</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testng</groupId>
|
||||
<artifactId>testng</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.wiremock</groupId>
|
||||
<artifactId>wiremock-standalone</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* 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.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;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Provides implementation of GitHub {@link OAuthAuthenticator} based on available configuration.
|
||||
*
|
||||
* @author Pavol Baran
|
||||
*/
|
||||
public abstract class AbstractGitHubOAuthAuthenticatorProvider
|
||||
implements Provider<OAuthAuthenticator> {
|
||||
private static final Logger LOG =
|
||||
LoggerFactory.getLogger(AbstractGitHubOAuthAuthenticatorProvider.class);
|
||||
private final String providerName;
|
||||
private final OAuthAuthenticator authenticator;
|
||||
|
||||
public AbstractGitHubOAuthAuthenticatorProvider(
|
||||
String gitHubClientIdPath,
|
||||
String gitHubClientSecretPath,
|
||||
String[] redirectUris,
|
||||
String oauthEndpoint,
|
||||
String authUri,
|
||||
String tokenUri,
|
||||
String providerName)
|
||||
throws IOException {
|
||||
this.providerName = providerName;
|
||||
authenticator =
|
||||
getOAuthAuthenticator(
|
||||
gitHubClientIdPath,
|
||||
gitHubClientSecretPath,
|
||||
redirectUris,
|
||||
oauthEndpoint,
|
||||
authUri,
|
||||
tokenUri);
|
||||
LOG.debug("{} GitHub OAuth Authenticator is used.", authenticator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuthAuthenticator get() {
|
||||
return authenticator;
|
||||
}
|
||||
|
||||
private OAuthAuthenticator getOAuthAuthenticator(
|
||||
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)
|
||||
&& !isNullOrEmpty(tokenUri)
|
||||
&& Objects.nonNull(redirectUris)
|
||||
&& redirectUris.length != 0) {
|
||||
final String clientId = Files.readString(Path.of(clientIdPath)).trim();
|
||||
final String clientSecret = Files.readString(Path.of(clientSecretPath)).trim();
|
||||
if (!isNullOrEmpty(clientId) && !isNullOrEmpty(clientSecret)) {
|
||||
return new GitHubOAuthAuthenticator(
|
||||
clientId,
|
||||
clientSecret,
|
||||
redirectUris,
|
||||
trimmedOauthEndpoint,
|
||||
authUri,
|
||||
tokenUri,
|
||||
providerName);
|
||||
}
|
||||
}
|
||||
return new NoopOAuthAuthenticator();
|
||||
}
|
||||
|
||||
static class NoopOAuthAuthenticator extends OAuthAuthenticator {
|
||||
@Override
|
||||
public String getOAuthProvider() {
|
||||
return "Noop";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEndpointUrl() {
|
||||
return "Noop";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ public class GitHubOAuthAuthenticator extends OAuthAuthenticator {
|
|||
private final String clientSecret;
|
||||
private final String githubApiUrl;
|
||||
private final String providerUrl;
|
||||
private final String providerName;
|
||||
|
||||
public GitHubOAuthAuthenticator(
|
||||
String clientId,
|
||||
|
|
@ -37,10 +38,12 @@ public class GitHubOAuthAuthenticator extends OAuthAuthenticator {
|
|||
String[] redirectUris,
|
||||
String authEndpoint,
|
||||
String authUri,
|
||||
String tokenUri)
|
||||
String tokenUri,
|
||||
String providerName)
|
||||
throws IOException {
|
||||
this.clientId = clientId;
|
||||
this.clientSecret = clientSecret;
|
||||
this.providerName = providerName;
|
||||
providerUrl = isNullOrEmpty(authEndpoint) ? "https://github.com" : trimEnd(authEndpoint, '/');
|
||||
githubApiUrl =
|
||||
providerUrl.equals("https://github.com")
|
||||
|
|
@ -52,7 +55,7 @@ public class GitHubOAuthAuthenticator extends OAuthAuthenticator {
|
|||
|
||||
@Override
|
||||
public final String getOAuthProvider() {
|
||||
return "github";
|
||||
return providerName;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2012-2021 Red Hat, Inc.
|
||||
* Copyright (c) 2012-2023 Red Hat, Inc.
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
|
|
@ -27,10 +27,6 @@
|
|||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.http-client</groupId>
|
||||
<artifactId>google-http-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.inject</groupId>
|
||||
<artifactId>guice</artifactId>
|
||||
|
|
@ -45,7 +41,11 @@
|
|||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-auth-shared</artifactId>
|
||||
<artifactId>che-core-api-auth-github-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-auth-github-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
|
|
@ -55,14 +55,6 @@
|
|||
<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>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.tomakehurst</groupId>
|
||||
<artifactId>wiremock-jre8-standalone</artifactId>
|
||||
|
|
|
|||
|
|
@ -11,20 +11,11 @@
|
|||
*/
|
||||
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;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
import javax.inject.Singleton;
|
||||
import org.eclipse.che.commons.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Provides implementation of GitHub {@link OAuthAuthenticator} based on available configuration.
|
||||
|
|
@ -32,9 +23,8 @@ import org.slf4j.LoggerFactory;
|
|||
* @author Pavol Baran
|
||||
*/
|
||||
@Singleton
|
||||
public class GitHubOAuthAuthenticatorProvider implements Provider<OAuthAuthenticator> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GitHubOAuthAuthenticatorProvider.class);
|
||||
private final OAuthAuthenticator authenticator;
|
||||
public class GitHubOAuthAuthenticatorProvider extends AbstractGitHubOAuthAuthenticatorProvider {
|
||||
private static final String PROVIDER_NAME = "github";
|
||||
|
||||
@Inject
|
||||
public GitHubOAuthAuthenticatorProvider(
|
||||
|
|
@ -45,65 +35,13 @@ public class GitHubOAuthAuthenticatorProvider implements Provider<OAuthAuthentic
|
|||
@Nullable @Named("che.oauth.github.authuri") String authUri,
|
||||
@Nullable @Named("che.oauth.github.tokenuri") String tokenUri)
|
||||
throws IOException {
|
||||
authenticator =
|
||||
getOAuthAuthenticator(
|
||||
gitHubClientIdPath,
|
||||
gitHubClientSecretPath,
|
||||
redirectUris,
|
||||
oauthEndpoint,
|
||||
authUri,
|
||||
tokenUri);
|
||||
LOG.debug("{} GitHub OAuth Authenticator is used.", authenticator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuthAuthenticator get() {
|
||||
return authenticator;
|
||||
}
|
||||
|
||||
private OAuthAuthenticator getOAuthAuthenticator(
|
||||
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)
|
||||
&& !isNullOrEmpty(tokenUri)
|
||||
&& Objects.nonNull(redirectUris)
|
||||
&& redirectUris.length != 0) {
|
||||
final String clientId = Files.readString(Path.of(clientIdPath)).trim();
|
||||
final String clientSecret = Files.readString(Path.of(clientSecretPath)).trim();
|
||||
if (!isNullOrEmpty(clientId) && !isNullOrEmpty(clientSecret)) {
|
||||
return new GitHubOAuthAuthenticator(
|
||||
clientId, clientSecret, redirectUris, trimmedOauthEndpoint, authUri, tokenUri);
|
||||
}
|
||||
}
|
||||
return new NoopOAuthAuthenticator();
|
||||
}
|
||||
|
||||
static class NoopOAuthAuthenticator extends OAuthAuthenticator {
|
||||
@Override
|
||||
public String getOAuthProvider() {
|
||||
return "Noop";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEndpointUrl() {
|
||||
return "Noop";
|
||||
}
|
||||
super(
|
||||
gitHubClientIdPath,
|
||||
gitHubClientSecretPath,
|
||||
redirectUris,
|
||||
oauthEndpoint,
|
||||
authUri,
|
||||
tokenUri,
|
||||
PROVIDER_NAME);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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.security.oauth;
|
||||
|
||||
import java.io.IOException;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import org.eclipse.che.commons.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Provides implementation of GitHub {@link OAuthAuthenticator} based on available configuration.
|
||||
*
|
||||
* @author Pavol Baran
|
||||
*/
|
||||
@Singleton
|
||||
public class GitHubOAuthAuthenticatorProviderSecond
|
||||
extends AbstractGitHubOAuthAuthenticatorProvider {
|
||||
private static final String PROVIDER_NAME = "github_2";
|
||||
|
||||
@Inject
|
||||
public GitHubOAuthAuthenticatorProviderSecond(
|
||||
@Nullable @Named("che.oauth2.github.clientid_filepath_2") String gitHubClientIdPath,
|
||||
@Nullable @Named("che.oauth2.github.clientsecret_filepath_2") String gitHubClientSecretPath,
|
||||
@Nullable @Named("che.oauth.github.redirecturis") String[] redirectUris,
|
||||
@Nullable @Named("che.integration.github.oauth_endpoint_2") String oauthEndpoint,
|
||||
@Nullable @Named("che.oauth.github.authuri_2") String authUri,
|
||||
@Nullable @Named("che.oauth.github.tokenuri_2") String tokenUri)
|
||||
throws IOException {
|
||||
super(
|
||||
gitHubClientIdPath,
|
||||
gitHubClientSecretPath,
|
||||
redirectUris,
|
||||
oauthEndpoint,
|
||||
authUri,
|
||||
tokenUri,
|
||||
PROVIDER_NAME);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2012-2021 Red Hat, Inc.
|
||||
* Copyright (c) 2012-2023 Red Hat, Inc.
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
|
|
@ -27,5 +27,6 @@ public class GithubModule extends AbstractModule {
|
|||
Multibinder<OAuthAuthenticator> oAuthAuthenticators =
|
||||
Multibinder.newSetBinder(binder(), OAuthAuthenticator.class);
|
||||
oAuthAuthenticators.addBinding().toProvider(GitHubOAuthAuthenticatorProvider.class);
|
||||
oAuthAuthenticators.addBinding().toProvider(GitHubOAuthAuthenticatorProviderSecond.class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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
|
||||
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<artifactId>che-master-parent</artifactId>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<version>7.76.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>che-core-api-factory-github-common</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Che Core :: API :: Factory Resolver Github Common</name>
|
||||
<properties>
|
||||
<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>jakarta.inject</groupId>
|
||||
<artifactId>jakarta.inject-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.validation</groupId>
|
||||
<artifactId>jakarta.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>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-dto</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-factory</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-factory-shared</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-workspace</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<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>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
* 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.github;
|
||||
|
||||
import static java.util.Collections.emptyMap;
|
||||
import static org.eclipse.che.api.factory.shared.Constants.URL_PARAMETER_NAME;
|
||||
import static org.eclipse.che.dto.server.DtoFactory.newDto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.Map;
|
||||
import org.eclipse.che.api.core.ApiException;
|
||||
import org.eclipse.che.api.core.BadRequestException;
|
||||
import org.eclipse.che.api.factory.server.BaseFactoryParameterResolver;
|
||||
import org.eclipse.che.api.factory.server.FactoryParametersResolver;
|
||||
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.RemoteFactoryUrl;
|
||||
import org.eclipse.che.api.factory.server.urlfactory.URLFactoryBuilder;
|
||||
import org.eclipse.che.api.factory.shared.dto.*;
|
||||
import org.eclipse.che.api.workspace.server.devfile.URLFetcher;
|
||||
import org.eclipse.che.api.workspace.shared.dto.ProjectConfigDto;
|
||||
import org.eclipse.che.api.workspace.shared.dto.devfile.ProjectDto;
|
||||
import org.eclipse.che.security.oauth.AuthorisationRequestManager;
|
||||
|
||||
/**
|
||||
* Provides Factory Parameters resolver for github repositories.
|
||||
*
|
||||
* @author Florent Benoit
|
||||
*/
|
||||
public abstract class AbstractGithubFactoryParametersResolver extends BaseFactoryParameterResolver
|
||||
implements FactoryParametersResolver {
|
||||
|
||||
/** Parser which will allow to check validity of URLs and create objects. */
|
||||
private final AbstractGithubURLParser githubUrlParser;
|
||||
|
||||
private final URLFetcher urlFetcher;
|
||||
|
||||
/** Builder allowing to build objects from github URL. */
|
||||
private final GithubSourceStorageBuilder githubSourceStorageBuilder;
|
||||
|
||||
private final URLFactoryBuilder urlFactoryBuilder;
|
||||
|
||||
/** ProjectDtoMerger */
|
||||
private final ProjectConfigDtoMerger projectConfigDtoMerger;
|
||||
|
||||
private final PersonalAccessTokenManager personalAccessTokenManager;
|
||||
|
||||
private final String providerName;
|
||||
|
||||
public AbstractGithubFactoryParametersResolver(
|
||||
AbstractGithubURLParser githubUrlParser,
|
||||
URLFetcher urlFetcher,
|
||||
GithubSourceStorageBuilder githubSourceStorageBuilder,
|
||||
AuthorisationRequestManager authorisationRequestManager,
|
||||
URLFactoryBuilder urlFactoryBuilder,
|
||||
ProjectConfigDtoMerger projectConfigDtoMerger,
|
||||
PersonalAccessTokenManager personalAccessTokenManager,
|
||||
String providerName) {
|
||||
super(authorisationRequestManager, urlFactoryBuilder, providerName);
|
||||
this.providerName = providerName;
|
||||
this.githubUrlParser = githubUrlParser;
|
||||
this.urlFetcher = urlFetcher;
|
||||
this.githubSourceStorageBuilder = githubSourceStorageBuilder;
|
||||
this.urlFactoryBuilder = urlFactoryBuilder;
|
||||
this.projectConfigDtoMerger = projectConfigDtoMerger;
|
||||
this.personalAccessTokenManager = personalAccessTokenManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this resolver can be used with the given parameters.
|
||||
*
|
||||
* @param factoryParameters map of parameters dedicated to factories
|
||||
* @return true if it will be accepted by the resolver implementation or false if it is not
|
||||
* accepted
|
||||
*/
|
||||
@Override
|
||||
public boolean accept(@NotNull final Map<String, String> factoryParameters) {
|
||||
// Check if url parameter is a github URL
|
||||
return factoryParameters.containsKey(URL_PARAMETER_NAME)
|
||||
&& githubUrlParser.isValid(factoryParameters.get(URL_PARAMETER_NAME));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
return providerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create factory object based on provided parameters
|
||||
*
|
||||
* @param factoryParameters map containing factory data parameters provided through URL
|
||||
* @throws BadRequestException when data are invalid
|
||||
*/
|
||||
@Override
|
||||
public FactoryMetaDto createFactory(@NotNull final Map<String, String> factoryParameters)
|
||||
throws ApiException {
|
||||
// no need to check null value of url parameter as accept() method has performed the check
|
||||
final GithubUrl githubUrl;
|
||||
if (getSkipAuthorisation(factoryParameters)) {
|
||||
githubUrl =
|
||||
githubUrlParser.parseWithoutAuthentication(factoryParameters.get(URL_PARAMETER_NAME));
|
||||
} else {
|
||||
githubUrl = githubUrlParser.parse(factoryParameters.get(URL_PARAMETER_NAME));
|
||||
}
|
||||
|
||||
return createFactory(
|
||||
factoryParameters,
|
||||
githubUrl,
|
||||
new GithubFactoryVisitor(githubUrl),
|
||||
new GithubAuthorizingFileContentProvider(
|
||||
githubUrl, urlFetcher, personalAccessTokenManager));
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitor that puts the default devfile or updates devfile projects into the Github Factory, if
|
||||
* needed.
|
||||
*/
|
||||
private class GithubFactoryVisitor implements FactoryVisitor {
|
||||
|
||||
private final GithubUrl githubUrl;
|
||||
|
||||
private GithubFactoryVisitor(GithubUrl githubUrl) {
|
||||
this.githubUrl = githubUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FactoryDevfileV2Dto visit(FactoryDevfileV2Dto factoryDto) {
|
||||
ScmInfoDto scmInfo =
|
||||
newDto(ScmInfoDto.class)
|
||||
.withScmProviderName(githubUrl.getProviderName())
|
||||
.withRepositoryUrl(githubUrl.repositoryLocation());
|
||||
if (githubUrl.getBranch() != null) {
|
||||
scmInfo.withBranch(githubUrl.getBranch());
|
||||
}
|
||||
return factoryDto.withScmInfo(scmInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FactoryDto visit(FactoryDto factory) {
|
||||
if (factory.getWorkspace() != null) {
|
||||
return projectConfigDtoMerger.merge(
|
||||
factory,
|
||||
() -> {
|
||||
// Compute project configuration
|
||||
return newDto(ProjectConfigDto.class)
|
||||
.withSource(githubSourceStorageBuilder.buildWorkspaceConfigSource(githubUrl))
|
||||
.withName(githubUrl.getRepository())
|
||||
.withPath("/".concat(githubUrl.getRepository()));
|
||||
});
|
||||
} else if (factory.getDevfile() == null) {
|
||||
// initialize default devfile
|
||||
factory.setDevfile(urlFactoryBuilder.buildDefaultDevfile(githubUrl.getRepository()));
|
||||
}
|
||||
|
||||
updateProjects(
|
||||
factory.getDevfile(),
|
||||
() ->
|
||||
newDto(ProjectDto.class)
|
||||
.withSource(githubSourceStorageBuilder.buildDevfileSource(githubUrl))
|
||||
.withName(githubUrl.getRepository()),
|
||||
project -> {
|
||||
final String location = project.getSource().getLocation();
|
||||
if (location.equals(githubUrl.repositoryLocation())) {
|
||||
project.getSource().setBranch(githubUrl.getBranch());
|
||||
}
|
||||
});
|
||||
|
||||
return factory;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteFactoryUrl parseFactoryUrl(String factoryUrl) throws ApiException {
|
||||
if (getSkipAuthorisation(emptyMap())) {
|
||||
return githubUrlParser.parseWithoutAuthentication(factoryUrl);
|
||||
} else {
|
||||
return githubUrlParser.parse(factoryUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
/*
|
||||
* 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.github;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
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 org.eclipse.che.api.auth.shared.dto.OAuthToken;
|
||||
import org.eclipse.che.api.core.*;
|
||||
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.PersonalAccessTokenParams;
|
||||
import org.eclipse.che.api.factory.server.scm.exception.*;
|
||||
import org.eclipse.che.commons.lang.NameGenerator;
|
||||
import org.eclipse.che.commons.lang.Pair;
|
||||
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 abstract class AbstractGithubPersonalAccessTokenFetcher
|
||||
implements PersonalAccessTokenFetcher {
|
||||
|
||||
private static final Logger LOG =
|
||||
LoggerFactory.getLogger(AbstractGithubPersonalAccessTokenFetcher.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 final String providerName;
|
||||
|
||||
/** Collection of OAuth scopes required to make integration with GitHub work. */
|
||||
public static final Set<String> DEFAULT_TOKEN_SCOPES =
|
||||
ImmutableSet.of("repo", "user:email", "read:user", "read:org", "workflow");
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
/**
|
||||
* Constructor used for testing only.
|
||||
*
|
||||
* @param apiEndpoint
|
||||
* @param oAuthAPI
|
||||
* @param githubApiClient
|
||||
*/
|
||||
AbstractGithubPersonalAccessTokenFetcher(
|
||||
String apiEndpoint, OAuthAPI oAuthAPI, GithubApiClient githubApiClient, String providerName) {
|
||||
this.apiEndpoint = apiEndpoint;
|
||||
this.oAuthAPI = oAuthAPI;
|
||||
this.githubApiClient = githubApiClient;
|
||||
this.providerName = providerName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PersonalAccessToken fetchPersonalAccessToken(Subject cheSubject, String scmServerUrl)
|
||||
throws ScmUnauthorizedException, ScmCommunicationException, UnknownScmProviderException {
|
||||
OAuthToken oAuthToken;
|
||||
|
||||
if (githubApiClient == null || !githubApiClient.isConnected(scmServerUrl)) {
|
||||
LOG.debug("not a valid url {} for current fetcher ", scmServerUrl);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
oAuthToken = oAuthAPI.getToken(providerName);
|
||||
String tokenName = NameGenerator.generate(OAUTH_2_PREFIX, 5);
|
||||
String tokenId = NameGenerator.generate("id-", 5);
|
||||
Optional<Pair<Boolean, String>> valid =
|
||||
isValid(
|
||||
new PersonalAccessTokenParams(
|
||||
scmServerUrl, tokenName, tokenId, oAuthToken.getToken(), null));
|
||||
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_SAAS_ENDPOINT
|
||||
+ "' and was '"
|
||||
+ scmServerUrl
|
||||
+ "'");
|
||||
} else if (!valid.get().first) {
|
||||
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 new PersonalAccessToken(
|
||||
scmServerUrl,
|
||||
cheSubject.getUserId(),
|
||||
valid.get().second,
|
||||
tokenName,
|
||||
tokenId,
|
||||
oAuthToken.getToken());
|
||||
} catch (UnauthorizedException e) {
|
||||
throw new ScmUnauthorizedException(
|
||||
cheSubject.getUserName() + " is not authorized in " + providerName + " OAuth provider.",
|
||||
providerName,
|
||||
"2.0",
|
||||
getLocalAuthenticateUrl());
|
||||
} catch (NotFoundException nfe) {
|
||||
throw new UnknownScmProviderException(nfe.getMessage(), scmServerUrl);
|
||||
} catch (ServerException | ForbiddenException | BadRequestException | ConflictException e) {
|
||||
LOG.error(e.getMessage());
|
||||
throw new ScmCommunicationException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public Optional<Boolean> isValid(PersonalAccessToken personalAccessToken) {
|
||||
if (!githubApiClient.isConnected(personalAccessToken.getScmProviderUrl())) {
|
||||
LOG.debug("not a valid url {} for current fetcher ", personalAccessToken.getScmProviderUrl());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
if (personalAccessToken.getScmTokenName() != null
|
||||
&& personalAccessToken.getScmTokenName().startsWith(OAUTH_2_PREFIX)) {
|
||||
String[] scopes = githubApiClient.getTokenScopes(personalAccessToken.getToken()).second;
|
||||
return Optional.of(containsScopes(scopes, DEFAULT_TOKEN_SCOPES));
|
||||
} else {
|
||||
// No REST API for PAT-s in Github found yet. Just try to do some action.
|
||||
GithubUser user = githubApiClient.getUser(personalAccessToken.getToken());
|
||||
if (personalAccessToken.getScmUserName().equals(user.getLogin())) {
|
||||
return Optional.of(Boolean.TRUE);
|
||||
} else {
|
||||
return Optional.of(Boolean.FALSE);
|
||||
}
|
||||
}
|
||||
} catch (ScmItemNotFoundException | ScmCommunicationException | ScmBadRequestException e) {
|
||||
return Optional.of(Boolean.FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Pair<Boolean, String>> isValid(PersonalAccessTokenParams params) {
|
||||
if (!githubApiClient.isConnected(params.getScmProviderUrl())) {
|
||||
LOG.debug("not a valid url {} for current fetcher ", params.getScmProviderUrl());
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
if (params.getScmTokenName() != null && params.getScmTokenName().startsWith(OAUTH_2_PREFIX)) {
|
||||
Pair<String, String[]> pair = githubApiClient.getTokenScopes(params.getToken());
|
||||
return Optional.of(
|
||||
Pair.of(
|
||||
containsScopes(pair.second, DEFAULT_TOKEN_SCOPES) ? Boolean.TRUE : Boolean.FALSE,
|
||||
pair.first));
|
||||
} else {
|
||||
// TODO: add PAT scope validation
|
||||
// No REST API for PAT-s in Github found yet. Just try to do some action.
|
||||
GithubUser user = githubApiClient.getUser(params.getToken());
|
||||
return Optional.of(Pair.of(Boolean.TRUE, user.getLogin()));
|
||||
}
|
||||
} catch (ScmItemNotFoundException | ScmCommunicationException | ScmBadRequestException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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="
|
||||
+ providerName
|
||||
+ "&scope="
|
||||
+ Joiner.on(',').join(DEFAULT_TOKEN_SCOPES)
|
||||
+ "&request_method=POST&signature_method=rsa";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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.github;
|
||||
|
||||
import static org.eclipse.che.api.factory.server.ApiExceptionMapper.toApiException;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.io.IOException;
|
||||
import javax.inject.Inject;
|
||||
import org.eclipse.che.api.core.ApiException;
|
||||
import org.eclipse.che.api.core.NotFoundException;
|
||||
import org.eclipse.che.api.factory.server.ScmFileResolver;
|
||||
import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager;
|
||||
import org.eclipse.che.api.workspace.server.devfile.URLFetcher;
|
||||
import org.eclipse.che.api.workspace.server.devfile.exception.DevfileException;
|
||||
|
||||
/** Github specific SCM file resolver. */
|
||||
public abstract class AbstractGithubScmFileResolver implements ScmFileResolver {
|
||||
|
||||
private final AbstractGithubURLParser githubUrlParser;
|
||||
private final URLFetcher urlFetcher;
|
||||
private final PersonalAccessTokenManager personalAccessTokenManager;
|
||||
|
||||
public AbstractGithubScmFileResolver(
|
||||
AbstractGithubURLParser githubUrlParser,
|
||||
URLFetcher urlFetcher,
|
||||
PersonalAccessTokenManager personalAccessTokenManager) {
|
||||
this.githubUrlParser = githubUrlParser;
|
||||
this.urlFetcher = urlFetcher;
|
||||
this.personalAccessTokenManager = personalAccessTokenManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(@NotNull String repository) {
|
||||
// Check if repository parameter is a github URL
|
||||
return githubUrlParser.isValid(repository);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fileContent(@NotNull String repository, @NotNull String filePath)
|
||||
throws ApiException {
|
||||
final GithubUrl githubUrl = githubUrlParser.parse(repository);
|
||||
try {
|
||||
return fetchContent(githubUrl, filePath, false);
|
||||
} catch (DevfileException exception) {
|
||||
// This catch might mean that the authentication was rejected by user, try to repeat the fetch
|
||||
// without authentication flow.
|
||||
try {
|
||||
return fetchContent(githubUrl, filePath, true);
|
||||
} catch (DevfileException devfileException) {
|
||||
throw toApiException(devfileException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String fetchContent(GithubUrl githubUrl, String filePath, boolean skipAuthentication)
|
||||
throws DevfileException, NotFoundException {
|
||||
try {
|
||||
GithubAuthorizingFileContentProvider contentProvider =
|
||||
new GithubAuthorizingFileContentProvider(
|
||||
githubUrl, urlFetcher, personalAccessTokenManager);
|
||||
return skipAuthentication
|
||||
? contentProvider.fetchContentWithoutAuthentication(filePath)
|
||||
: contentProvider.fetchContent(filePath);
|
||||
} catch (IOException e) {
|
||||
throw new NotFoundException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
/*
|
||||
* 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.github;
|
||||
|
||||
import static com.google.common.base.MoreObjects.firstNonNull;
|
||||
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.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.eclipse.che.api.core.ApiException;
|
||||
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.*;
|
||||
import org.eclipse.che.api.factory.server.urlfactory.DevfileFilenamesProvider;
|
||||
import org.eclipse.che.commons.env.EnvironmentContext;
|
||||
import org.eclipse.che.commons.subject.Subject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Parser of String Github URLs and provide {@link GithubUrl} objects.
|
||||
*
|
||||
* @author Florent Benoit
|
||||
*/
|
||||
public abstract class AbstractGithubURLParser {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractGithubURLParser.class);
|
||||
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;
|
||||
|
||||
private final Pattern githubSSHPattern;
|
||||
|
||||
private final boolean disableSubdomainIsolation;
|
||||
|
||||
private final String providerName;
|
||||
|
||||
/** Constructor used for testing only. */
|
||||
AbstractGithubURLParser(
|
||||
PersonalAccessTokenManager tokenManager,
|
||||
DevfileFilenamesProvider devfileFilenamesProvider,
|
||||
GithubApiClient githubApiClient,
|
||||
String oauthEndpoint,
|
||||
boolean disableSubdomainIsolation,
|
||||
String providerName) {
|
||||
this.tokenManager = tokenManager;
|
||||
this.devfileFilenamesProvider = devfileFilenamesProvider;
|
||||
this.apiClient = githubApiClient;
|
||||
this.oauthEndpoint = oauthEndpoint;
|
||||
this.disableSubdomainIsolation = disableSubdomainIsolation;
|
||||
this.providerName = providerName;
|
||||
|
||||
String endpoint =
|
||||
isNullOrEmpty(oauthEndpoint) ? GITHUB_SAAS_ENDPOINT : trimEnd(oauthEndpoint, '/');
|
||||
|
||||
this.githubPattern =
|
||||
compile(
|
||||
format(
|
||||
"^%s/(?<repoUser>[^/]+)/(?<repoName>[^/]++)((/)|(?:/tree/(?<branchName>.++))|(/pull/(?<pullRequestId>\\d++)))?$",
|
||||
endpoint));
|
||||
this.githubSSHPattern =
|
||||
compile(format("^git@%s:(?<repoUser>.*)/(?<repoName>.*)$", URI.create(endpoint).getHost()));
|
||||
}
|
||||
|
||||
public boolean isValid(@NotNull String url) {
|
||||
String trimmedUrl = trimEnd(url, '/');
|
||||
return githubPattern.matcher(trimmedUrl).matches()
|
||||
|| githubSSHPattern.matcher(trimmedUrl).matches();
|
||||
}
|
||||
|
||||
public GithubUrl parseWithoutAuthentication(String url) throws ApiException {
|
||||
return parse(trimEnd(url, '/'), false);
|
||||
}
|
||||
|
||||
public GithubUrl parse(String url) throws ApiException {
|
||||
return parse(trimEnd(url, '/'), true);
|
||||
}
|
||||
|
||||
private GithubUrl parse(String url, boolean authenticationRequired) throws ApiException {
|
||||
boolean isHTTPSUrl = githubPattern.matcher(url).matches();
|
||||
Matcher matcher = isHTTPSUrl ? githubPattern.matcher(url) : githubSSHPattern.matcher(url);
|
||||
if (!matcher.matches()) {
|
||||
throw new IllegalArgumentException(
|
||||
format("The given url %s is not a valid github URL. ", 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$")) {
|
||||
repoName = repoName.substring(0, repoName.length() - 4);
|
||||
}
|
||||
|
||||
String branchName = null;
|
||||
String pullRequestId = null;
|
||||
if (isHTTPSUrl) {
|
||||
branchName = matcher.group("branchName");
|
||||
pullRequestId = matcher.group("pullRequestId");
|
||||
}
|
||||
|
||||
if (pullRequestId != null) {
|
||||
GithubPullRequest pullRequest =
|
||||
this.getPullRequest(pullRequestId, repoUser, repoName, authenticationRequired);
|
||||
if (pullRequest != null) {
|
||||
String state = pullRequest.getState();
|
||||
if (!"open".equalsIgnoreCase(state)) {
|
||||
throw new IllegalArgumentException(
|
||||
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, state));
|
||||
}
|
||||
|
||||
GithubHead pullRequestHead = pullRequest.getHead();
|
||||
repoUser = pullRequestHead.getUser().getLogin();
|
||||
repoName = pullRequestHead.getRepo().getName();
|
||||
branchName = pullRequestHead.getRef();
|
||||
}
|
||||
}
|
||||
|
||||
String latestCommit = null;
|
||||
GithubCommit commit =
|
||||
this.getLatestCommit(
|
||||
repoUser, repoName, firstNonNull(branchName, "HEAD"), authenticationRequired);
|
||||
if (commit != null) {
|
||||
latestCommit = commit.getSha();
|
||||
}
|
||||
|
||||
return new GithubUrl(providerName)
|
||||
.withUsername(repoUser)
|
||||
.withRepository(repoName)
|
||||
.setIsHTTPSUrl(isHTTPSUrl)
|
||||
.withServerUrl(serverUrl)
|
||||
.withDisableSubdomainIsolation(disableSubdomainIsolation)
|
||||
.withBranch(branchName)
|
||||
.withLatestCommit(latestCommit)
|
||||
.withDevfileFilenames(devfileFilenamesProvider.getConfiguredDevfileFilenames())
|
||||
.withUrl(url);
|
||||
}
|
||||
|
||||
private GithubPullRequest getPullRequest(
|
||||
String pullRequestId, String repoUser, String repoName, boolean authenticationRequired)
|
||||
throws ApiException {
|
||||
try {
|
||||
// prepare token
|
||||
String githubEndpoint =
|
||||
isNullOrEmpty(oauthEndpoint) ? GITHUB_SAAS_ENDPOINT : trimEnd(oauthEndpoint, '/');
|
||||
Subject subject = EnvironmentContext.getCurrent().getSubject();
|
||||
PersonalAccessToken personalAccessToken = null;
|
||||
Optional<PersonalAccessToken> token = tokenManager.get(subject, githubEndpoint);
|
||||
if (token.isPresent()) {
|
||||
personalAccessToken = token.get();
|
||||
} else if (authenticationRequired) {
|
||||
personalAccessToken = tokenManager.fetchAndSave(subject, githubEndpoint);
|
||||
}
|
||||
|
||||
// get pull request
|
||||
return this.apiClient.getPullRequest(
|
||||
pullRequestId,
|
||||
repoUser,
|
||||
repoName,
|
||||
personalAccessToken != null ? personalAccessToken.getToken() : null);
|
||||
} catch (UnknownScmProviderException e) {
|
||||
|
||||
// get pull request without authentication
|
||||
try {
|
||||
return this.apiClient.getPullRequest(pullRequestId, repoUser, repoName, null);
|
||||
} catch (ScmItemNotFoundException
|
||||
| ScmCommunicationException
|
||||
| ScmBadRequestException exception) {
|
||||
LOG.error("Failed to authenticate to GitHub", e);
|
||||
}
|
||||
|
||||
} catch (ScmUnauthorizedException e) {
|
||||
throw toApiException(e);
|
||||
} catch (ScmCommunicationException
|
||||
| UnsatisfiedScmPreconditionException
|
||||
| ScmConfigurationPersistenceException e) {
|
||||
LOG.error("Failed to authenticate to GitHub", e);
|
||||
} catch (ScmItemNotFoundException | ScmBadRequestException e) {
|
||||
LOG.error("Failed retrieve GitHub Pull Request", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private GithubCommit getLatestCommit(
|
||||
String repoUser, String repoName, String branchName, boolean authenticationRequired)
|
||||
throws ApiException {
|
||||
try {
|
||||
// prepare token
|
||||
String githubEndpoint =
|
||||
isNullOrEmpty(oauthEndpoint) ? GITHUB_SAAS_ENDPOINT : trimEnd(oauthEndpoint, '/');
|
||||
Subject subject = EnvironmentContext.getCurrent().getSubject();
|
||||
PersonalAccessToken personalAccessToken = null;
|
||||
Optional<PersonalAccessToken> token = tokenManager.get(subject, githubEndpoint);
|
||||
if (token.isPresent()) {
|
||||
personalAccessToken = token.get();
|
||||
} else if (authenticationRequired) {
|
||||
personalAccessToken = tokenManager.fetchAndSave(subject, githubEndpoint);
|
||||
}
|
||||
|
||||
// get latest commit
|
||||
return this.apiClient.getLatestCommit(
|
||||
repoUser,
|
||||
repoName,
|
||||
branchName,
|
||||
personalAccessToken != null ? personalAccessToken.getToken() : null);
|
||||
} catch (UnknownScmProviderException | ScmUnauthorizedException e) {
|
||||
// get latest commit without authentication
|
||||
try {
|
||||
return this.apiClient.getLatestCommit(repoUser, repoName, branchName, null);
|
||||
} catch (ScmItemNotFoundException
|
||||
| ScmCommunicationException
|
||||
| ScmBadRequestException
|
||||
| URISyntaxException exception) {
|
||||
LOG.error("Failed to authenticate to GitHub", e);
|
||||
}
|
||||
} catch (ScmCommunicationException
|
||||
| UnsatisfiedScmPreconditionException
|
||||
| ScmConfigurationPersistenceException e) {
|
||||
LOG.error("Failed to authenticate to GitHub", e);
|
||||
} catch (ScmItemNotFoundException | ScmBadRequestException | URISyntaxException e) {
|
||||
LOG.error("Failed to retrieve the latest commit", e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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.github;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.util.Set;
|
||||
import org.eclipse.che.api.auth.shared.dto.OAuthToken;
|
||||
import org.eclipse.che.api.factory.server.scm.AbstractGitUserDataFetcher;
|
||||
import org.eclipse.che.api.factory.server.scm.GitUserData;
|
||||
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.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.security.oauth.OAuthAPI;
|
||||
|
||||
/** GitHub user data retriever. */
|
||||
public abstract class AbstractGithubUserDataFetcher extends AbstractGitUserDataFetcher {
|
||||
private final String apiEndpoint;
|
||||
/** GitHub API client. */
|
||||
private final GithubApiClient githubApiClient;
|
||||
|
||||
/** Name of this OAuth provider as found in OAuthAPI. */
|
||||
private final String providerName;
|
||||
/** Collection of OAuth scopes required to make integration with GitHub work. */
|
||||
public static final Set<String> DEFAULT_TOKEN_SCOPES =
|
||||
ImmutableSet.of("repo", "user:email", "read:user");
|
||||
|
||||
/** Constructor used for testing only. */
|
||||
public AbstractGithubUserDataFetcher(
|
||||
String apiEndpoint,
|
||||
OAuthAPI oAuthTokenFetcher,
|
||||
PersonalAccessTokenManager personalAccessTokenManager,
|
||||
GithubApiClient githubApiClient,
|
||||
String providerName) {
|
||||
super(providerName, personalAccessTokenManager, oAuthTokenFetcher);
|
||||
this.providerName = providerName;
|
||||
this.githubApiClient = githubApiClient;
|
||||
this.apiEndpoint = apiEndpoint;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GitUserData fetchGitUserDataWithOAuthToken(OAuthToken oAuthToken)
|
||||
throws ScmItemNotFoundException, ScmCommunicationException, ScmBadRequestException {
|
||||
GithubUser user = githubApiClient.getUser(oAuthToken.getToken());
|
||||
return new GitUserData(user.getName(), user.getEmail());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GitUserData fetchGitUserDataWithPersonalAccessToken(
|
||||
PersonalAccessToken personalAccessToken)
|
||||
throws ScmItemNotFoundException, ScmCommunicationException, ScmBadRequestException {
|
||||
GithubUser user = githubApiClient.getUser(personalAccessToken.getToken());
|
||||
return new GitUserData(user.getName(), user.getEmail());
|
||||
}
|
||||
|
||||
protected String getLocalAuthenticateUrl() {
|
||||
return apiEndpoint
|
||||
+ "/oauth/authenticate?oauth_provider="
|
||||
+ providerName
|
||||
+ "&scope="
|
||||
+ Joiner.on(',').join(DEFAULT_TOKEN_SCOPES)
|
||||
+ "&request_method=POST&signature_method=rsa";
|
||||
}
|
||||
}
|
||||
|
|
@ -12,10 +12,7 @@
|
|||
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.net.HttpURLConnection.*;
|
||||
import static java.time.Duration.ofSeconds;
|
||||
import static org.eclipse.che.commons.lang.StringUtils.trimEnd;
|
||||
|
||||
|
|
@ -29,8 +29,8 @@ import org.eclipse.che.api.factory.server.urlfactory.DefaultFactoryUrl;
|
|||
* @author Florent Benoit
|
||||
*/
|
||||
public class GithubUrl extends DefaultFactoryUrl {
|
||||
|
||||
private final String NAME = "github";
|
||||
// TODO
|
||||
private final String providerName;
|
||||
|
||||
private static final String HOSTNAME = "https://github.com";
|
||||
|
||||
|
|
@ -59,11 +59,13 @@ public class GithubUrl extends DefaultFactoryUrl {
|
|||
* Creation of this instance is made by the parser so user may not need to create a new instance
|
||||
* directly
|
||||
*/
|
||||
protected GithubUrl() {}
|
||||
protected GithubUrl(String providerName) {
|
||||
this.providerName = providerName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
return NAME;
|
||||
return providerName;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -26,14 +26,6 @@
|
|||
<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>
|
||||
|
|
@ -46,10 +38,6 @@
|
|||
<groupId>jakarta.inject</groupId>
|
||||
<artifactId>jakarta.inject-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.validation</groupId>
|
||||
<artifactId>jakarta.validation-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-auth</artifactId>
|
||||
|
|
@ -70,6 +58,10 @@
|
|||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-factory</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-factory-github-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-factory-shared</artifactId>
|
||||
|
|
@ -94,10 +86,6 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -11,30 +11,12 @@
|
|||
*/
|
||||
package org.eclipse.che.api.factory.server.github;
|
||||
|
||||
import static java.util.Collections.emptyMap;
|
||||
import static org.eclipse.che.api.factory.shared.Constants.URL_PARAMETER_NAME;
|
||||
import static org.eclipse.che.dto.server.DtoFactory.newDto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.Map;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import org.eclipse.che.api.core.ApiException;
|
||||
import org.eclipse.che.api.core.BadRequestException;
|
||||
import org.eclipse.che.api.factory.server.BaseFactoryParameterResolver;
|
||||
import org.eclipse.che.api.factory.server.FactoryParametersResolver;
|
||||
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.RemoteFactoryUrl;
|
||||
import org.eclipse.che.api.factory.server.urlfactory.URLFactoryBuilder;
|
||||
import org.eclipse.che.api.factory.shared.dto.FactoryDevfileV2Dto;
|
||||
import org.eclipse.che.api.factory.shared.dto.FactoryDto;
|
||||
import org.eclipse.che.api.factory.shared.dto.FactoryMetaDto;
|
||||
import org.eclipse.che.api.factory.shared.dto.FactoryVisitor;
|
||||
import org.eclipse.che.api.factory.shared.dto.ScmInfoDto;
|
||||
import org.eclipse.che.api.workspace.server.devfile.URLFetcher;
|
||||
import org.eclipse.che.api.workspace.shared.dto.ProjectConfigDto;
|
||||
import org.eclipse.che.api.workspace.shared.dto.devfile.ProjectDto;
|
||||
import org.eclipse.che.security.oauth.AuthorisationRequestManager;
|
||||
|
||||
/**
|
||||
|
|
@ -43,26 +25,10 @@ import org.eclipse.che.security.oauth.AuthorisationRequestManager;
|
|||
* @author Florent Benoit
|
||||
*/
|
||||
@Singleton
|
||||
public class GithubFactoryParametersResolver extends BaseFactoryParameterResolver
|
||||
implements FactoryParametersResolver {
|
||||
public class GithubFactoryParametersResolver extends AbstractGithubFactoryParametersResolver {
|
||||
|
||||
private static final String PROVIDER_NAME = "github";
|
||||
|
||||
/** Parser which will allow to check validity of URLs and create objects. */
|
||||
private final GithubURLParser githubUrlParser;
|
||||
|
||||
private final URLFetcher urlFetcher;
|
||||
|
||||
/** Builder allowing to build objects from github URL. */
|
||||
private final GithubSourceStorageBuilder githubSourceStorageBuilder;
|
||||
|
||||
private final URLFactoryBuilder urlFactoryBuilder;
|
||||
|
||||
/** ProjectDtoMerger */
|
||||
private final ProjectConfigDtoMerger projectConfigDtoMerger;
|
||||
|
||||
private final PersonalAccessTokenManager personalAccessTokenManager;
|
||||
|
||||
@Inject
|
||||
public GithubFactoryParametersResolver(
|
||||
GithubURLParser githubUrlParser,
|
||||
|
|
@ -72,124 +38,14 @@ public class GithubFactoryParametersResolver extends BaseFactoryParameterResolve
|
|||
URLFactoryBuilder urlFactoryBuilder,
|
||||
ProjectConfigDtoMerger projectConfigDtoMerger,
|
||||
PersonalAccessTokenManager personalAccessTokenManager) {
|
||||
super(authorisationRequestManager, urlFactoryBuilder, PROVIDER_NAME);
|
||||
this.githubUrlParser = githubUrlParser;
|
||||
this.urlFetcher = urlFetcher;
|
||||
this.githubSourceStorageBuilder = githubSourceStorageBuilder;
|
||||
this.urlFactoryBuilder = urlFactoryBuilder;
|
||||
this.projectConfigDtoMerger = projectConfigDtoMerger;
|
||||
this.personalAccessTokenManager = personalAccessTokenManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this resolver can be used with the given parameters.
|
||||
*
|
||||
* @param factoryParameters map of parameters dedicated to factories
|
||||
* @return true if it will be accepted by the resolver implementation or false if it is not
|
||||
* accepted
|
||||
*/
|
||||
@Override
|
||||
public boolean accept(@NotNull final Map<String, String> factoryParameters) {
|
||||
// Check if url parameter is a github URL
|
||||
return factoryParameters.containsKey(URL_PARAMETER_NAME)
|
||||
&& githubUrlParser.isValid(factoryParameters.get(URL_PARAMETER_NAME));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
return PROVIDER_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create factory object based on provided parameters
|
||||
*
|
||||
* @param factoryParameters map containing factory data parameters provided through URL
|
||||
* @throws BadRequestException when data are invalid
|
||||
*/
|
||||
@Override
|
||||
public FactoryMetaDto createFactory(@NotNull final Map<String, String> factoryParameters)
|
||||
throws ApiException {
|
||||
// no need to check null value of url parameter as accept() method has performed the check
|
||||
final GithubUrl githubUrl;
|
||||
if (getSkipAuthorisation(factoryParameters)) {
|
||||
githubUrl =
|
||||
githubUrlParser.parseWithoutAuthentication(factoryParameters.get(URL_PARAMETER_NAME));
|
||||
} else {
|
||||
githubUrl = githubUrlParser.parse(factoryParameters.get(URL_PARAMETER_NAME));
|
||||
}
|
||||
|
||||
return createFactory(
|
||||
factoryParameters,
|
||||
githubUrl,
|
||||
new GithubFactoryVisitor(githubUrl),
|
||||
new GithubAuthorizingFileContentProvider(
|
||||
githubUrl, urlFetcher, personalAccessTokenManager));
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitor that puts the default devfile or updates devfile projects into the Github Factory, if
|
||||
* needed.
|
||||
*/
|
||||
private class GithubFactoryVisitor implements FactoryVisitor {
|
||||
|
||||
private final GithubUrl githubUrl;
|
||||
|
||||
private GithubFactoryVisitor(GithubUrl githubUrl) {
|
||||
this.githubUrl = githubUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FactoryDevfileV2Dto visit(FactoryDevfileV2Dto factoryDto) {
|
||||
ScmInfoDto scmInfo =
|
||||
newDto(ScmInfoDto.class)
|
||||
.withScmProviderName(githubUrl.getProviderName())
|
||||
.withRepositoryUrl(githubUrl.repositoryLocation());
|
||||
if (githubUrl.getBranch() != null) {
|
||||
scmInfo.withBranch(githubUrl.getBranch());
|
||||
}
|
||||
return factoryDto.withScmInfo(scmInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FactoryDto visit(FactoryDto factory) {
|
||||
if (factory.getWorkspace() != null) {
|
||||
return projectConfigDtoMerger.merge(
|
||||
factory,
|
||||
() -> {
|
||||
// Compute project configuration
|
||||
return newDto(ProjectConfigDto.class)
|
||||
.withSource(githubSourceStorageBuilder.buildWorkspaceConfigSource(githubUrl))
|
||||
.withName(githubUrl.getRepository())
|
||||
.withPath("/".concat(githubUrl.getRepository()));
|
||||
});
|
||||
} else if (factory.getDevfile() == null) {
|
||||
// initialize default devfile
|
||||
factory.setDevfile(urlFactoryBuilder.buildDefaultDevfile(githubUrl.getRepository()));
|
||||
}
|
||||
|
||||
updateProjects(
|
||||
factory.getDevfile(),
|
||||
() ->
|
||||
newDto(ProjectDto.class)
|
||||
.withSource(githubSourceStorageBuilder.buildDevfileSource(githubUrl))
|
||||
.withName(githubUrl.getRepository()),
|
||||
project -> {
|
||||
final String location = project.getSource().getLocation();
|
||||
if (location.equals(githubUrl.repositoryLocation())) {
|
||||
project.getSource().setBranch(githubUrl.getBranch());
|
||||
}
|
||||
});
|
||||
|
||||
return factory;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteFactoryUrl parseFactoryUrl(String factoryUrl) throws ApiException {
|
||||
if (getSkipAuthorisation(emptyMap())) {
|
||||
return githubUrlParser.parseWithoutAuthentication(factoryUrl);
|
||||
} else {
|
||||
return githubUrlParser.parse(factoryUrl);
|
||||
}
|
||||
super(
|
||||
githubUrlParser,
|
||||
urlFetcher,
|
||||
githubSourceStorageBuilder,
|
||||
authorisationRequestManager,
|
||||
urlFactoryBuilder,
|
||||
projectConfigDtoMerger,
|
||||
personalAccessTokenManager,
|
||||
PROVIDER_NAME);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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.github;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
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.workspace.server.devfile.URLFetcher;
|
||||
import org.eclipse.che.security.oauth.AuthorisationRequestManager;
|
||||
|
||||
/**
|
||||
* Provides Factory Parameters resolver for github repositories.
|
||||
*
|
||||
* @author Florent Benoit
|
||||
*/
|
||||
@Singleton
|
||||
public class GithubFactoryParametersResolverSecond extends AbstractGithubFactoryParametersResolver {
|
||||
|
||||
private static final String PROVIDER_NAME = "github_2";
|
||||
|
||||
@Inject
|
||||
public GithubFactoryParametersResolverSecond(
|
||||
GithubURLParserSecond githubUrlParser,
|
||||
URLFetcher urlFetcher,
|
||||
GithubSourceStorageBuilder githubSourceStorageBuilder,
|
||||
AuthorisationRequestManager authorisationRequestManager,
|
||||
URLFactoryBuilder urlFactoryBuilder,
|
||||
ProjectConfigDtoMerger projectConfigDtoMerger,
|
||||
PersonalAccessTokenManager personalAccessTokenManager) {
|
||||
super(
|
||||
githubUrlParser,
|
||||
urlFetcher,
|
||||
githubSourceStorageBuilder,
|
||||
authorisationRequestManager,
|
||||
urlFactoryBuilder,
|
||||
projectConfigDtoMerger,
|
||||
personalAccessTokenManager,
|
||||
PROVIDER_NAME);
|
||||
}
|
||||
}
|
||||
|
|
@ -23,8 +23,11 @@ public class GithubModule extends AbstractModule {
|
|||
Multibinder<PersonalAccessTokenFetcher> tokenFetcherMultibinder =
|
||||
Multibinder.newSetBinder(binder(), PersonalAccessTokenFetcher.class);
|
||||
tokenFetcherMultibinder.addBinding().to(GithubPersonalAccessTokenFetcher.class);
|
||||
tokenFetcherMultibinder.addBinding().to(GithubPersonalAccessTokenFetcherSecond.class);
|
||||
|
||||
Multibinder<GitUserDataFetcher> gitUserDataMultibinder =
|
||||
Multibinder.newSetBinder(binder(), GitUserDataFetcher.class);
|
||||
gitUserDataMultibinder.addBinding().to(GithubUserDataFetcher.class);
|
||||
gitUserDataMultibinder.addBinding().to(GithubUserDataFetcherSecond.class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,270 +11,27 @@
|
|||
*/
|
||||
package org.eclipse.che.api.factory.server.github;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
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.PersonalAccessTokenParams;
|
||||
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.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.lang.Pair;
|
||||
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;
|
||||
public class GithubPersonalAccessTokenFetcher extends AbstractGithubPersonalAccessTokenFetcher {
|
||||
|
||||
/** 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", "user:email", "read:user", "read:org", "workflow");
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@Nullable @Named("che.integration.github.oauth_endpoint") String oauthEndpoint,
|
||||
OAuthAPI oAuthAPI) {
|
||||
this(apiEndpoint, oAuthAPI, new GithubApiClient(oauthEndpoint));
|
||||
super(apiEndpoint, oAuthAPI, new GithubApiClient(oauthEndpoint), OAUTH_PROVIDER_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, UnknownScmProviderException {
|
||||
OAuthToken oAuthToken;
|
||||
|
||||
if (githubApiClient == null || !githubApiClient.isConnected(scmServerUrl)) {
|
||||
LOG.debug("not a valid url {} for current fetcher ", scmServerUrl);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
oAuthToken = oAuthAPI.getToken(OAUTH_PROVIDER_NAME);
|
||||
String tokenName = NameGenerator.generate(OAUTH_2_PREFIX, 5);
|
||||
String tokenId = NameGenerator.generate("id-", 5);
|
||||
Optional<Pair<Boolean, String>> valid =
|
||||
isValid(
|
||||
new PersonalAccessTokenParams(
|
||||
scmServerUrl, tokenName, tokenId, oAuthToken.getToken(), null));
|
||||
if (valid.isEmpty()) {
|
||||
throw buildScmUnauthorizedException(cheSubject);
|
||||
} else if (!valid.get().first) {
|
||||
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 new PersonalAccessToken(
|
||||
scmServerUrl,
|
||||
cheSubject.getUserId(),
|
||||
valid.get().second,
|
||||
tokenName,
|
||||
tokenId,
|
||||
oAuthToken.getToken());
|
||||
} catch (UnauthorizedException e) {
|
||||
throw buildScmUnauthorizedException(cheSubject);
|
||||
} catch (NotFoundException nfe) {
|
||||
throw new UnknownScmProviderException(nfe.getMessage(), scmServerUrl);
|
||||
} catch (ServerException | ForbiddenException | BadRequestException | ConflictException e) {
|
||||
LOG.error(e.getMessage());
|
||||
throw new ScmCommunicationException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private ScmUnauthorizedException buildScmUnauthorizedException(Subject cheSubject) {
|
||||
return new ScmUnauthorizedException(
|
||||
cheSubject.getUserName()
|
||||
+ " is not authorized in "
|
||||
+ OAUTH_PROVIDER_NAME
|
||||
+ " OAuth provider.",
|
||||
OAUTH_PROVIDER_NAME,
|
||||
"2.0",
|
||||
getLocalAuthenticateUrl());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public Optional<Boolean> isValid(PersonalAccessToken personalAccessToken) {
|
||||
if (!githubApiClient.isConnected(personalAccessToken.getScmProviderUrl())) {
|
||||
LOG.debug("not a valid url {} for current fetcher ", personalAccessToken.getScmProviderUrl());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
if (personalAccessToken.getScmTokenName() != null
|
||||
&& personalAccessToken.getScmTokenName().startsWith(OAUTH_2_PREFIX)) {
|
||||
String[] scopes = githubApiClient.getTokenScopes(personalAccessToken.getToken()).second;
|
||||
return Optional.of(containsScopes(scopes, DEFAULT_TOKEN_SCOPES));
|
||||
} else {
|
||||
// No REST API for PAT-s in Github found yet. Just try to do some action.
|
||||
GithubUser user = githubApiClient.getUser(personalAccessToken.getToken());
|
||||
if (personalAccessToken.getScmUserName().equals(user.getLogin())) {
|
||||
return Optional.of(Boolean.TRUE);
|
||||
} else {
|
||||
return Optional.of(Boolean.FALSE);
|
||||
}
|
||||
}
|
||||
} catch (ScmItemNotFoundException | ScmCommunicationException | ScmBadRequestException e) {
|
||||
return Optional.of(Boolean.FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Pair<Boolean, String>> isValid(PersonalAccessTokenParams params) {
|
||||
if (!githubApiClient.isConnected(params.getScmProviderUrl())) {
|
||||
LOG.debug("not a valid url {} for current fetcher ", params.getScmProviderUrl());
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
if (params.getScmTokenName() != null && params.getScmTokenName().startsWith(OAUTH_2_PREFIX)) {
|
||||
Pair<String, String[]> pair = githubApiClient.getTokenScopes(params.getToken());
|
||||
return Optional.of(
|
||||
Pair.of(
|
||||
containsScopes(pair.second, DEFAULT_TOKEN_SCOPES) ? Boolean.TRUE : Boolean.FALSE,
|
||||
pair.first));
|
||||
} else {
|
||||
// TODO: add PAT scope validation
|
||||
// No REST API for PAT-s in Github found yet. Just try to do some action.
|
||||
GithubUser user = githubApiClient.getUser(params.getToken());
|
||||
return Optional.of(Pair.of(Boolean.TRUE, user.getLogin()));
|
||||
}
|
||||
} catch (ScmItemNotFoundException | ScmCommunicationException | ScmBadRequestException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
+ "&scope="
|
||||
+ Joiner.on(',').join(DEFAULT_TOKEN_SCOPES)
|
||||
+ "&request_method=POST&signature_method=rsa";
|
||||
@Named("che.api") String apiEndpoint, OAuthAPI oAuthAPI, GithubApiClient githubApiClient) {
|
||||
super(apiEndpoint, oAuthAPI, githubApiClient, OAUTH_PROVIDER_NAME);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.github;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import org.eclipse.che.commons.annotation.Nullable;
|
||||
import org.eclipse.che.security.oauth.OAuthAPI;
|
||||
|
||||
/** GitHub OAuth token retriever. */
|
||||
public class GithubPersonalAccessTokenFetcherSecond
|
||||
extends AbstractGithubPersonalAccessTokenFetcher {
|
||||
|
||||
/** Name of this OAuth provider as found in OAuthAPI. */
|
||||
private static final String OAUTH_PROVIDER_NAME = "github_2";
|
||||
|
||||
@Inject
|
||||
public GithubPersonalAccessTokenFetcherSecond(
|
||||
@Named("che.api") String apiEndpoint,
|
||||
@Nullable @Named("che.integration.github.oauth_endpoint_2") String oauthEndpoint,
|
||||
OAuthAPI oAuthAPI) {
|
||||
super(apiEndpoint, oAuthAPI, new GithubApiClient(oauthEndpoint), OAUTH_PROVIDER_NAME);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,69 +11,18 @@
|
|||
*/
|
||||
package org.eclipse.che.api.factory.server.github;
|
||||
|
||||
import static org.eclipse.che.api.factory.server.ApiExceptionMapper.toApiException;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.io.IOException;
|
||||
import javax.inject.Inject;
|
||||
import org.eclipse.che.api.core.ApiException;
|
||||
import org.eclipse.che.api.core.NotFoundException;
|
||||
import org.eclipse.che.api.factory.server.ScmFileResolver;
|
||||
import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager;
|
||||
import org.eclipse.che.api.workspace.server.devfile.URLFetcher;
|
||||
import org.eclipse.che.api.workspace.server.devfile.exception.DevfileException;
|
||||
|
||||
/** Github specific SCM file resolver. */
|
||||
public class GithubScmFileResolver implements ScmFileResolver {
|
||||
|
||||
private final GithubURLParser githubUrlParser;
|
||||
private final URLFetcher urlFetcher;
|
||||
private final PersonalAccessTokenManager personalAccessTokenManager;
|
||||
public class GithubScmFileResolver extends AbstractGithubScmFileResolver {
|
||||
|
||||
@Inject
|
||||
public GithubScmFileResolver(
|
||||
GithubURLParser githubUrlParser,
|
||||
URLFetcher urlFetcher,
|
||||
PersonalAccessTokenManager personalAccessTokenManager) {
|
||||
this.githubUrlParser = githubUrlParser;
|
||||
this.urlFetcher = urlFetcher;
|
||||
this.personalAccessTokenManager = personalAccessTokenManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(@NotNull String repository) {
|
||||
// Check if repository parameter is a github URL
|
||||
return githubUrlParser.isValid(repository);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fileContent(@NotNull String repository, @NotNull String filePath)
|
||||
throws ApiException {
|
||||
final GithubUrl githubUrl = githubUrlParser.parse(repository);
|
||||
try {
|
||||
return fetchContent(githubUrl, filePath, false);
|
||||
} catch (DevfileException exception) {
|
||||
// This catch might mean that the authentication was rejected by user, try to repeat the fetch
|
||||
// without authentication flow.
|
||||
try {
|
||||
return fetchContent(githubUrl, filePath, true);
|
||||
} catch (DevfileException devfileException) {
|
||||
throw toApiException(devfileException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String fetchContent(GithubUrl githubUrl, String filePath, boolean skipAuthentication)
|
||||
throws DevfileException, NotFoundException {
|
||||
try {
|
||||
GithubAuthorizingFileContentProvider contentProvider =
|
||||
new GithubAuthorizingFileContentProvider(
|
||||
githubUrl, urlFetcher, personalAccessTokenManager);
|
||||
return skipAuthentication
|
||||
? contentProvider.fetchContentWithoutAuthentication(filePath)
|
||||
: contentProvider.fetchContent(filePath);
|
||||
} catch (IOException e) {
|
||||
throw new NotFoundException(e.getMessage());
|
||||
}
|
||||
super(githubUrlParser, urlFetcher, personalAccessTokenManager);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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.github;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager;
|
||||
import org.eclipse.che.api.workspace.server.devfile.URLFetcher;
|
||||
|
||||
/** Github specific SCM file resolver. */
|
||||
public class GithubScmFileResolverSecond extends AbstractGithubScmFileResolver {
|
||||
|
||||
@Inject
|
||||
public GithubScmFileResolverSecond(
|
||||
GithubURLParserSecond githubUrlParser,
|
||||
URLFetcher urlFetcher,
|
||||
PersonalAccessTokenManager personalAccessTokenManager) {
|
||||
super(githubUrlParser, urlFetcher, personalAccessTokenManager);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,39 +11,12 @@
|
|||
*/
|
||||
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 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.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
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;
|
||||
import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager;
|
||||
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.ScmConfigurationPersistenceException;
|
||||
import org.eclipse.che.api.factory.server.scm.exception.ScmItemNotFoundException;
|
||||
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
|
||||
import org.eclipse.che.api.factory.server.scm.exception.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;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Parser of String Github URLs and provide {@link GithubUrl} objects.
|
||||
|
|
@ -51,22 +24,10 @@ import org.slf4j.LoggerFactory;
|
|||
* @author Florent Benoit
|
||||
*/
|
||||
@Singleton
|
||||
public class GithubURLParser {
|
||||
public class GithubURLParser extends AbstractGithubURLParser {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GithubURLParser.class);
|
||||
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;
|
||||
|
||||
private final Pattern githubSSHPattern;
|
||||
|
||||
private final boolean disableSubdomainIsolation;
|
||||
/** Name of this OAuth provider as found in OAuthAPI. */
|
||||
private static final String OAUTH_PROVIDER_NAME = "github";
|
||||
|
||||
@Inject
|
||||
public GithubURLParser(
|
||||
|
|
@ -75,204 +36,27 @@ public class GithubURLParser {
|
|||
@Nullable @Named("che.integration.github.oauth_endpoint") String oauthEndpoint,
|
||||
@Named("che.integration.github.disable_subdomain_isolation")
|
||||
boolean disableSubdomainIsolation) {
|
||||
this(
|
||||
super(
|
||||
tokenManager,
|
||||
devfileFilenamesProvider,
|
||||
new GithubApiClient(oauthEndpoint),
|
||||
oauthEndpoint,
|
||||
disableSubdomainIsolation);
|
||||
disableSubdomainIsolation,
|
||||
OAUTH_PROVIDER_NAME);
|
||||
}
|
||||
|
||||
/** Constructor used for testing only. */
|
||||
GithubURLParser(
|
||||
PersonalAccessTokenManager tokenManager,
|
||||
DevfileFilenamesProvider devfileFilenamesProvider,
|
||||
GithubApiClient githubApiClient,
|
||||
String oauthEndpoint,
|
||||
boolean disableSubdomainIsolation) {
|
||||
this.tokenManager = tokenManager;
|
||||
this.devfileFilenamesProvider = devfileFilenamesProvider;
|
||||
this.apiClient = githubApiClient;
|
||||
this.oauthEndpoint = oauthEndpoint;
|
||||
this.disableSubdomainIsolation = disableSubdomainIsolation;
|
||||
|
||||
String endpoint =
|
||||
isNullOrEmpty(oauthEndpoint) ? GITHUB_SAAS_ENDPOINT : trimEnd(oauthEndpoint, '/');
|
||||
|
||||
this.githubPattern =
|
||||
compile(
|
||||
format(
|
||||
"^%s/(?<repoUser>[^/]+)/(?<repoName>[^/]++)((/)|(?:/tree/(?<branchName>.++))|(/pull/(?<pullRequestId>\\d++)))?$",
|
||||
endpoint));
|
||||
this.githubSSHPattern =
|
||||
compile(format("^git@%s:(?<repoUser>.*)/(?<repoName>.*)$", URI.create(endpoint).getHost()));
|
||||
}
|
||||
|
||||
public boolean isValid(@NotNull String url) {
|
||||
String trimmedUrl = trimEnd(url, '/');
|
||||
return githubPattern.matcher(trimmedUrl).matches()
|
||||
|| githubSSHPattern.matcher(trimmedUrl).matches();
|
||||
}
|
||||
|
||||
public GithubUrl parseWithoutAuthentication(String url) throws ApiException {
|
||||
return parse(trimEnd(url, '/'), false);
|
||||
}
|
||||
|
||||
public GithubUrl parse(String url) throws ApiException {
|
||||
return parse(trimEnd(url, '/'), true);
|
||||
}
|
||||
|
||||
private GithubUrl parse(String url, boolean authenticationRequired) throws ApiException {
|
||||
boolean isHTTPSUrl = githubPattern.matcher(url).matches();
|
||||
Matcher matcher = isHTTPSUrl ? githubPattern.matcher(url) : githubSSHPattern.matcher(url);
|
||||
if (!matcher.matches()) {
|
||||
throw new IllegalArgumentException(
|
||||
format("The given url %s is not a valid github URL. ", 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$")) {
|
||||
repoName = repoName.substring(0, repoName.length() - 4);
|
||||
}
|
||||
|
||||
String branchName = null;
|
||||
String pullRequestId = null;
|
||||
if (isHTTPSUrl) {
|
||||
branchName = matcher.group("branchName");
|
||||
pullRequestId = matcher.group("pullRequestId");
|
||||
}
|
||||
|
||||
if (pullRequestId != null) {
|
||||
GithubPullRequest pullRequest =
|
||||
this.getPullRequest(pullRequestId, repoUser, repoName, authenticationRequired);
|
||||
if (pullRequest != null) {
|
||||
String state = pullRequest.getState();
|
||||
if (!"open".equalsIgnoreCase(state)) {
|
||||
throw new IllegalArgumentException(
|
||||
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, state));
|
||||
}
|
||||
|
||||
GithubHead pullRequestHead = pullRequest.getHead();
|
||||
repoUser = pullRequestHead.getUser().getLogin();
|
||||
repoName = pullRequestHead.getRepo().getName();
|
||||
branchName = pullRequestHead.getRef();
|
||||
}
|
||||
}
|
||||
|
||||
String latestCommit = null;
|
||||
GithubCommit commit =
|
||||
this.getLatestCommit(
|
||||
repoUser, repoName, firstNonNull(branchName, "HEAD"), authenticationRequired);
|
||||
if (commit != null) {
|
||||
latestCommit = commit.getSha();
|
||||
}
|
||||
|
||||
return new GithubUrl()
|
||||
.withUsername(repoUser)
|
||||
.withRepository(repoName)
|
||||
.setIsHTTPSUrl(isHTTPSUrl)
|
||||
.withServerUrl(serverUrl)
|
||||
.withDisableSubdomainIsolation(disableSubdomainIsolation)
|
||||
.withBranch(branchName)
|
||||
.withLatestCommit(latestCommit)
|
||||
.withDevfileFilenames(devfileFilenamesProvider.getConfiguredDevfileFilenames())
|
||||
.withUrl(url);
|
||||
}
|
||||
|
||||
private GithubPullRequest getPullRequest(
|
||||
String pullRequestId, String repoUser, String repoName, boolean authenticationRequired)
|
||||
throws ApiException {
|
||||
try {
|
||||
// prepare token
|
||||
String githubEndpoint =
|
||||
isNullOrEmpty(oauthEndpoint) ? GITHUB_SAAS_ENDPOINT : trimEnd(oauthEndpoint, '/');
|
||||
Subject subject = EnvironmentContext.getCurrent().getSubject();
|
||||
PersonalAccessToken personalAccessToken = null;
|
||||
Optional<PersonalAccessToken> token = tokenManager.get(subject, githubEndpoint);
|
||||
if (token.isPresent()) {
|
||||
personalAccessToken = token.get();
|
||||
} else if (authenticationRequired) {
|
||||
personalAccessToken = tokenManager.fetchAndSave(subject, githubEndpoint);
|
||||
}
|
||||
|
||||
// get pull request
|
||||
return this.apiClient.getPullRequest(
|
||||
pullRequestId,
|
||||
repoUser,
|
||||
repoName,
|
||||
personalAccessToken != null ? personalAccessToken.getToken() : null);
|
||||
} catch (UnknownScmProviderException e) {
|
||||
|
||||
// get pull request without authentication
|
||||
try {
|
||||
return this.apiClient.getPullRequest(pullRequestId, repoUser, repoName, null);
|
||||
} catch (ScmItemNotFoundException
|
||||
| ScmCommunicationException
|
||||
| ScmBadRequestException exception) {
|
||||
LOG.error("Failed to authenticate to GitHub", e);
|
||||
}
|
||||
|
||||
} catch (ScmUnauthorizedException e) {
|
||||
throw toApiException(e);
|
||||
} catch (ScmCommunicationException
|
||||
| UnsatisfiedScmPreconditionException
|
||||
| ScmConfigurationPersistenceException e) {
|
||||
LOG.error("Failed to authenticate to GitHub", e);
|
||||
} catch (ScmItemNotFoundException | ScmBadRequestException e) {
|
||||
LOG.error("Failed retrieve GitHub Pull Request", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private GithubCommit getLatestCommit(
|
||||
String repoUser, String repoName, String branchName, boolean authenticationRequired)
|
||||
throws ApiException {
|
||||
try {
|
||||
// prepare token
|
||||
String githubEndpoint =
|
||||
isNullOrEmpty(oauthEndpoint) ? GITHUB_SAAS_ENDPOINT : trimEnd(oauthEndpoint, '/');
|
||||
Subject subject = EnvironmentContext.getCurrent().getSubject();
|
||||
PersonalAccessToken personalAccessToken = null;
|
||||
Optional<PersonalAccessToken> token = tokenManager.get(subject, githubEndpoint);
|
||||
if (token.isPresent()) {
|
||||
personalAccessToken = token.get();
|
||||
} else if (authenticationRequired) {
|
||||
personalAccessToken = tokenManager.fetchAndSave(subject, githubEndpoint);
|
||||
}
|
||||
|
||||
// get latest commit
|
||||
return this.apiClient.getLatestCommit(
|
||||
repoUser,
|
||||
repoName,
|
||||
branchName,
|
||||
personalAccessToken != null ? personalAccessToken.getToken() : null);
|
||||
} catch (UnknownScmProviderException | ScmUnauthorizedException e) {
|
||||
// get latest commit without authentication
|
||||
try {
|
||||
return this.apiClient.getLatestCommit(repoUser, repoName, branchName, null);
|
||||
} catch (ScmItemNotFoundException
|
||||
| ScmCommunicationException
|
||||
| ScmBadRequestException
|
||||
| URISyntaxException exception) {
|
||||
LOG.error("Failed to authenticate to GitHub", e);
|
||||
}
|
||||
} catch (ScmCommunicationException
|
||||
| UnsatisfiedScmPreconditionException
|
||||
| ScmConfigurationPersistenceException e) {
|
||||
LOG.error("Failed to authenticate to GitHub", e);
|
||||
} catch (ScmItemNotFoundException | ScmBadRequestException | URISyntaxException e) {
|
||||
LOG.error("Failed to retrieve the latest commit", e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
super(
|
||||
tokenManager,
|
||||
devfileFilenamesProvider,
|
||||
githubApiClient,
|
||||
oauthEndpoint,
|
||||
disableSubdomainIsolation,
|
||||
OAUTH_PROVIDER_NAME);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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.github;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager;
|
||||
import org.eclipse.che.api.factory.server.urlfactory.DevfileFilenamesProvider;
|
||||
import org.eclipse.che.commons.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Parser of String Github URLs and provide {@link GithubUrl} objects.
|
||||
*
|
||||
* @author Florent Benoit
|
||||
*/
|
||||
@Singleton
|
||||
public class GithubURLParserSecond extends AbstractGithubURLParser {
|
||||
/** Name of this OAuth provider as found in OAuthAPI. */
|
||||
private static final String OAUTH_PROVIDER_NAME = "github_2";
|
||||
|
||||
@Inject
|
||||
public GithubURLParserSecond(
|
||||
PersonalAccessTokenManager tokenManager,
|
||||
DevfileFilenamesProvider devfileFilenamesProvider,
|
||||
@Nullable @Named("che.integration.github.oauth_endpoint_2") String oauthEndpoint,
|
||||
@Named("che.integration.github.disable_subdomain_isolation_2")
|
||||
boolean disableSubdomainIsolation) {
|
||||
super(
|
||||
tokenManager,
|
||||
devfileFilenamesProvider,
|
||||
new GithubApiClient(oauthEndpoint),
|
||||
oauthEndpoint,
|
||||
disableSubdomainIsolation,
|
||||
OAUTH_PROVIDER_NAME);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,80 +11,41 @@
|
|||
*/
|
||||
package org.eclipse.che.api.factory.server.github;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
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.factory.server.scm.AbstractGitUserDataFetcher;
|
||||
import org.eclipse.che.api.factory.server.scm.GitUserData;
|
||||
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.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.security.oauth.OAuthAPI;
|
||||
|
||||
/** GitHub user data retriever. */
|
||||
public class GithubUserDataFetcher extends AbstractGitUserDataFetcher {
|
||||
private final String apiEndpoint;
|
||||
/** GitHub API client. */
|
||||
private final GithubApiClient githubApiClient;
|
||||
|
||||
public class GithubUserDataFetcher extends AbstractGithubUserDataFetcher {
|
||||
/** 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", "user:email", "read:user");
|
||||
|
||||
@Inject
|
||||
public GithubUserDataFetcher(
|
||||
@Named("che.api") String apiEndpoint,
|
||||
@Nullable @Named("che.integration.github.oauth_endpoint") String oauthEndpoint,
|
||||
OAuthAPI oAuthTokenFetcher,
|
||||
PersonalAccessTokenManager personalAccessTokenManager) {
|
||||
this(
|
||||
super(
|
||||
apiEndpoint,
|
||||
oAuthTokenFetcher,
|
||||
personalAccessTokenManager,
|
||||
new GithubApiClient(oauthEndpoint));
|
||||
new GithubApiClient(oauthEndpoint),
|
||||
OAUTH_PROVIDER_NAME);
|
||||
}
|
||||
|
||||
/** Constructor used for testing only. */
|
||||
public GithubUserDataFetcher(
|
||||
GithubUserDataFetcher(
|
||||
String apiEndpoint,
|
||||
OAuthAPI oAuthTokenFetcher,
|
||||
PersonalAccessTokenManager personalAccessTokenManager,
|
||||
GithubApiClient githubApiClient) {
|
||||
super(OAUTH_PROVIDER_NAME, personalAccessTokenManager, oAuthTokenFetcher);
|
||||
this.githubApiClient = githubApiClient;
|
||||
this.apiEndpoint = apiEndpoint;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GitUserData fetchGitUserDataWithOAuthToken(OAuthToken oAuthToken)
|
||||
throws ScmItemNotFoundException, ScmCommunicationException, ScmBadRequestException {
|
||||
GithubUser user = githubApiClient.getUser(oAuthToken.getToken());
|
||||
return new GitUserData(user.getName(), user.getEmail());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GitUserData fetchGitUserDataWithPersonalAccessToken(
|
||||
PersonalAccessToken personalAccessToken)
|
||||
throws ScmItemNotFoundException, ScmCommunicationException, ScmBadRequestException {
|
||||
GithubUser user = githubApiClient.getUser(personalAccessToken.getToken());
|
||||
return new GitUserData(user.getName(), user.getEmail());
|
||||
}
|
||||
|
||||
protected String getLocalAuthenticateUrl() {
|
||||
return apiEndpoint
|
||||
+ "/oauth/authenticate?oauth_provider="
|
||||
+ OAUTH_PROVIDER_NAME
|
||||
+ "&scope="
|
||||
+ Joiner.on(',').join(DEFAULT_TOKEN_SCOPES)
|
||||
+ "&request_method=POST&signature_method=rsa";
|
||||
super(
|
||||
apiEndpoint,
|
||||
oAuthTokenFetcher,
|
||||
personalAccessTokenManager,
|
||||
githubApiClient,
|
||||
OAUTH_PROVIDER_NAME);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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.github;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager;
|
||||
import org.eclipse.che.commons.annotation.Nullable;
|
||||
import org.eclipse.che.security.oauth.OAuthAPI;
|
||||
|
||||
/** GitHub user data retriever. */
|
||||
public class GithubUserDataFetcherSecond extends AbstractGithubUserDataFetcher {
|
||||
/** Name of this OAuth provider as found in OAuthAPI. */
|
||||
private static final String OAUTH_PROVIDER_NAME = "github_2";
|
||||
|
||||
@Inject
|
||||
public GithubUserDataFetcherSecond(
|
||||
@Named("che.api") String apiEndpoint,
|
||||
@Nullable @Named("che.integration.github.oauth_endpoint_2") String oauthEndpoint,
|
||||
OAuthAPI oAuthTokenFetcher,
|
||||
PersonalAccessTokenManager personalAccessTokenManager) {
|
||||
super(
|
||||
apiEndpoint,
|
||||
oAuthTokenFetcher,
|
||||
personalAccessTokenManager,
|
||||
new GithubApiClient(oauthEndpoint),
|
||||
OAUTH_PROVIDER_NAME);
|
||||
}
|
||||
}
|
||||
|
|
@ -45,7 +45,7 @@ public class GithubAuthorizingFileContentProviderTest {
|
|||
URLFetcher urlFetcher = mock(URLFetcher.class);
|
||||
|
||||
GithubUrl githubUrl =
|
||||
new GithubUrl()
|
||||
new GithubUrl("github")
|
||||
.withUsername("eclipse")
|
||||
.withRepository("che")
|
||||
.withBranch("main")
|
||||
|
|
@ -71,7 +71,7 @@ public class GithubAuthorizingFileContentProviderTest {
|
|||
String raw_url = "https://raw.githubusercontent.com/foo/bar/branch-name/devfile.yaml";
|
||||
|
||||
GithubUrl githubUrl =
|
||||
new GithubUrl()
|
||||
new GithubUrl("github")
|
||||
.withUsername("eclipse")
|
||||
.withRepository("che")
|
||||
.withBranch("main")
|
||||
|
|
@ -92,7 +92,7 @@ public class GithubAuthorizingFileContentProviderTest {
|
|||
public void shouldThrowNotFoundForPublicRepos() throws Exception {
|
||||
String url = "https://raw.githubusercontent.com/foo/bar/branch-name/devfile.yaml";
|
||||
|
||||
GithubUrl githubUrl = new GithubUrl().withUsername("eclipse").withRepository("che");
|
||||
GithubUrl githubUrl = new GithubUrl("github").withUsername("eclipse").withRepository("che");
|
||||
|
||||
URLFetcher urlFetcher = Mockito.mock(URLFetcher.class);
|
||||
FileContentProvider fileContentProvider =
|
||||
|
|
@ -109,7 +109,7 @@ public class GithubAuthorizingFileContentProviderTest {
|
|||
@Test(expectedExceptions = DevfileException.class)
|
||||
public void shouldThrowDevfileException() throws Exception {
|
||||
String url = "https://raw.githubusercontent.com/foo/bar/branch-name/devfile.yaml";
|
||||
GithubUrl githubUrl = new GithubUrl().withUsername("eclipse").withRepository("che");
|
||||
GithubUrl githubUrl = new GithubUrl("github").withUsername("eclipse").withRepository("che");
|
||||
|
||||
URLFetcher urlFetcher = Mockito.mock(URLFetcher.class);
|
||||
FileContentProvider fileContentProvider =
|
||||
|
|
@ -128,7 +128,7 @@ public class GithubAuthorizingFileContentProviderTest {
|
|||
String raw_url = "https://ghserver.com/foo/bar/branch-name/devfile.yaml";
|
||||
|
||||
URLFetcher urlFetcher = Mockito.mock(URLFetcher.class);
|
||||
GithubUrl githubUrl = new GithubUrl().withUsername("eclipse").withRepository("che");
|
||||
GithubUrl githubUrl = new GithubUrl("github").withUsername("eclipse").withRepository("che");
|
||||
FileContentProvider fileContentProvider =
|
||||
new GithubAuthorizingFileContentProvider(githubUrl, urlFetcher, personalAccessTokenManager);
|
||||
var personalAccessToken = new PersonalAccessToken(raw_url, "che", "my-token");
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ public class GithubFactoryParametersResolverTest {
|
|||
@Captor private ArgumentCaptor<RemoteFactoryUrl> factoryUrlArgumentCaptor;
|
||||
|
||||
/** Instance of resolver that will be tested. */
|
||||
private GithubFactoryParametersResolver githubFactoryParametersResolver;
|
||||
private AbstractGithubFactoryParametersResolver abstractGithubFactoryParametersResolver;
|
||||
|
||||
@BeforeMethod
|
||||
protected void init() throws Exception {
|
||||
|
|
@ -110,7 +110,7 @@ public class GithubFactoryParametersResolverTest {
|
|||
new GithubURLParser(
|
||||
personalAccessTokenManager, devfileFilenamesProvider, githubApiClient, null, false);
|
||||
|
||||
githubFactoryParametersResolver =
|
||||
abstractGithubFactoryParametersResolver =
|
||||
new GithubFactoryParametersResolver(
|
||||
githubUrlParser,
|
||||
urlFetcher,
|
||||
|
|
@ -119,14 +119,14 @@ public class GithubFactoryParametersResolverTest {
|
|||
urlFactoryBuilder,
|
||||
projectConfigDtoMerger,
|
||||
personalAccessTokenManager);
|
||||
assertNotNull(this.githubFactoryParametersResolver);
|
||||
assertNotNull(this.abstractGithubFactoryParametersResolver);
|
||||
}
|
||||
|
||||
/** Check missing parameter name can't be accepted by this resolver */
|
||||
@Test
|
||||
public void checkMissingParameter() {
|
||||
Map<String, String> parameters = singletonMap("foo", "this is a foo bar");
|
||||
boolean accept = githubFactoryParametersResolver.accept(parameters);
|
||||
boolean accept = abstractGithubFactoryParametersResolver.accept(parameters);
|
||||
// shouldn't be accepted
|
||||
assertFalse(accept);
|
||||
}
|
||||
|
|
@ -135,7 +135,7 @@ public class GithubFactoryParametersResolverTest {
|
|||
@Test
|
||||
public void checkInvalidAcceptUrl() {
|
||||
Map<String, String> parameters = singletonMap(URL_PARAMETER_NAME, "http://www.eclipse.org/che");
|
||||
boolean accept = githubFactoryParametersResolver.accept(parameters);
|
||||
boolean accept = abstractGithubFactoryParametersResolver.accept(parameters);
|
||||
// shouldn't be accepted
|
||||
assertFalse(accept);
|
||||
}
|
||||
|
|
@ -145,7 +145,7 @@ public class GithubFactoryParametersResolverTest {
|
|||
public void checkValidAcceptUrl() {
|
||||
Map<String, String> parameters =
|
||||
singletonMap(URL_PARAMETER_NAME, "https://github.com/codenvy/codenvy.git");
|
||||
boolean accept = githubFactoryParametersResolver.accept(parameters);
|
||||
boolean accept = abstractGithubFactoryParametersResolver.accept(parameters);
|
||||
// shouldn't be accepted
|
||||
assertTrue(accept);
|
||||
}
|
||||
|
|
@ -168,7 +168,7 @@ public class GithubFactoryParametersResolverTest {
|
|||
|
||||
Map<String, String> params = ImmutableMap.of(URL_PARAMETER_NAME, githubUrl);
|
||||
// when
|
||||
FactoryDto factory = (FactoryDto) githubFactoryParametersResolver.createFactory(params);
|
||||
FactoryDto factory = (FactoryDto) abstractGithubFactoryParametersResolver.createFactory(params);
|
||||
// then
|
||||
verify(urlFactoryBuilder).buildDefaultDevfile(eq("che"));
|
||||
assertEquals(factory, computedFactory);
|
||||
|
|
@ -193,7 +193,7 @@ public class GithubFactoryParametersResolverTest {
|
|||
"https://github.com/eclipse/che",
|
||||
ERROR_QUERY_NAME,
|
||||
"access_denied");
|
||||
githubFactoryParametersResolver.createFactory(params);
|
||||
abstractGithubFactoryParametersResolver.createFactory(params);
|
||||
// then
|
||||
verify(urlFactoryBuilder)
|
||||
.createFactoryFromDevfile(
|
||||
|
|
@ -210,7 +210,7 @@ public class GithubFactoryParametersResolverTest {
|
|||
.thenReturn(new GithubCommit().withSha("test-sha"));
|
||||
|
||||
// when
|
||||
githubFactoryParametersResolver.createFactory(
|
||||
abstractGithubFactoryParametersResolver.createFactory(
|
||||
ImmutableMap.of(URL_PARAMETER_NAME, "https://github.com/eclipse/che"));
|
||||
// then
|
||||
verify(urlFactoryBuilder)
|
||||
|
|
@ -237,7 +237,7 @@ public class GithubFactoryParametersResolverTest {
|
|||
|
||||
Map<String, String> params = ImmutableMap.of(URL_PARAMETER_NAME, githubUrl);
|
||||
// when
|
||||
FactoryDto factory = (FactoryDto) githubFactoryParametersResolver.createFactory(params);
|
||||
FactoryDto factory = (FactoryDto) abstractGithubFactoryParametersResolver.createFactory(params);
|
||||
// then
|
||||
assertNotNull(factory.getDevfile());
|
||||
assertNull(factory.getWorkspace());
|
||||
|
|
@ -268,7 +268,7 @@ public class GithubFactoryParametersResolverTest {
|
|||
|
||||
Map<String, String> params = ImmutableMap.of(URL_PARAMETER_NAME, githubUrl);
|
||||
// when
|
||||
FactoryDto factory = (FactoryDto) githubFactoryParametersResolver.createFactory(params);
|
||||
FactoryDto factory = (FactoryDto) abstractGithubFactoryParametersResolver.createFactory(params);
|
||||
// then
|
||||
assertNotNull(factory.getDevfile());
|
||||
SourceDto source = factory.getDevfile().getProjects().get(0).getSource();
|
||||
|
|
@ -299,7 +299,7 @@ public class GithubFactoryParametersResolverTest {
|
|||
|
||||
Map<String, String> params = ImmutableMap.of(URL_PARAMETER_NAME, githubUrl);
|
||||
// when
|
||||
FactoryDto factory = (FactoryDto) githubFactoryParametersResolver.createFactory(params);
|
||||
FactoryDto factory = (FactoryDto) abstractGithubFactoryParametersResolver.createFactory(params);
|
||||
// then
|
||||
assertNotNull(factory.getDevfile());
|
||||
SourceDto source = factory.getDevfile().getProjects().get(0).getSource();
|
||||
|
|
@ -323,7 +323,7 @@ public class GithubFactoryParametersResolverTest {
|
|||
Map<String, String> params = ImmutableMap.of(URL_PARAMETER_NAME, githubUrl);
|
||||
// when
|
||||
FactoryDevfileV2Dto factory =
|
||||
(FactoryDevfileV2Dto) githubFactoryParametersResolver.createFactory(params);
|
||||
(FactoryDevfileV2Dto) abstractGithubFactoryParametersResolver.createFactory(params);
|
||||
// then
|
||||
ScmInfo scmInfo = factory.getScmInfo();
|
||||
assertNotNull(scmInfo);
|
||||
|
|
@ -343,7 +343,7 @@ public class GithubFactoryParametersResolverTest {
|
|||
.thenReturn(Optional.of(generateDevfileFactory()));
|
||||
|
||||
// when
|
||||
githubFactoryParametersResolver.createFactory(params);
|
||||
abstractGithubFactoryParametersResolver.createFactory(params);
|
||||
|
||||
// then
|
||||
verify(urlFactoryBuilder)
|
||||
|
|
@ -359,7 +359,7 @@ public class GithubFactoryParametersResolverTest {
|
|||
// given
|
||||
githubUrlParser = mock(GithubURLParser.class);
|
||||
|
||||
githubFactoryParametersResolver =
|
||||
abstractGithubFactoryParametersResolver =
|
||||
new GithubFactoryParametersResolver(
|
||||
githubUrlParser,
|
||||
urlFetcher,
|
||||
|
|
@ -370,7 +370,7 @@ public class GithubFactoryParametersResolverTest {
|
|||
personalAccessTokenManager);
|
||||
when(authorisationRequestManager.isStored(eq("github"))).thenReturn(true);
|
||||
// when
|
||||
githubFactoryParametersResolver.parseFactoryUrl("url");
|
||||
abstractGithubFactoryParametersResolver.parseFactoryUrl("url");
|
||||
// then
|
||||
verify(githubUrlParser).parseWithoutAuthentication("url");
|
||||
verify(githubUrlParser, never()).parse("url");
|
||||
|
|
@ -381,7 +381,7 @@ public class GithubFactoryParametersResolverTest {
|
|||
// given
|
||||
githubUrlParser = mock(GithubURLParser.class);
|
||||
|
||||
githubFactoryParametersResolver =
|
||||
abstractGithubFactoryParametersResolver =
|
||||
new GithubFactoryParametersResolver(
|
||||
githubUrlParser,
|
||||
urlFetcher,
|
||||
|
|
@ -392,7 +392,7 @@ public class GithubFactoryParametersResolverTest {
|
|||
personalAccessTokenManager);
|
||||
when(authorisationRequestManager.isStored(eq("github"))).thenReturn(false);
|
||||
// when
|
||||
githubFactoryParametersResolver.parseFactoryUrl("url");
|
||||
abstractGithubFactoryParametersResolver.parseFactoryUrl("url");
|
||||
// then
|
||||
verify(githubUrlParser).parse("url");
|
||||
verify(githubUrlParser, never()).parseWithoutAuthentication("url");
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ public class GithubUrlTest {
|
|||
public void testRawFileLocationWithDefaultBranchName() {
|
||||
String file = ".che/che-theia-plugins.yaml";
|
||||
|
||||
GithubUrl url = new GithubUrl().withUsername("eclipse").withRepository("che");
|
||||
GithubUrl url = new GithubUrl("github").withUsername("eclipse").withRepository("che");
|
||||
|
||||
assertEquals(
|
||||
url.rawFileLocation(file),
|
||||
|
|
@ -151,7 +151,7 @@ public class GithubUrlTest {
|
|||
String file = ".che/che-theia-plugins.yaml";
|
||||
|
||||
GithubUrl url =
|
||||
new GithubUrl().withUsername("eclipse").withRepository("che").withBranch("main");
|
||||
new GithubUrl("github").withUsername("eclipse").withRepository("che").withBranch("main");
|
||||
|
||||
assertEquals(
|
||||
url.rawFileLocation(file),
|
||||
|
|
@ -163,7 +163,7 @@ public class GithubUrlTest {
|
|||
String file = ".che/che-theia-plugins.yaml";
|
||||
|
||||
GithubUrl url =
|
||||
new GithubUrl()
|
||||
new GithubUrl("github")
|
||||
.withUsername("eclipse")
|
||||
.withRepository("che")
|
||||
.withBranch("main")
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
<module>che-core-api-auth-azure-devops</module>
|
||||
<module>che-core-api-auth-bitbucket</module>
|
||||
<module>che-core-api-auth-github</module>
|
||||
<module>che-core-api-auth-github-common</module>
|
||||
<module>che-core-api-auth-gitlab</module>
|
||||
<module>che-core-api-auth-openshift</module>
|
||||
<module>che-core-api-workspace-shared</module>
|
||||
|
|
@ -44,6 +45,7 @@
|
|||
<module>che-core-api-factory-shared</module>
|
||||
<module>che-core-api-factory</module>
|
||||
<module>che-core-api-factory-github</module>
|
||||
<module>che-core-api-factory-github-common</module>
|
||||
<module>che-core-api-factory-gitlab</module>
|
||||
<module>che-core-api-factory-bitbucket</module>
|
||||
<module>che-core-api-factory-bitbucket-server</module>
|
||||
|
|
|
|||
Loading…
Reference in New Issue