Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for PATs in GitHub Enterprise server #603

Merged
merged 7 commits into from
Nov 21, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -205,21 +205,29 @@ public Optional<Boolean> isValid(PersonalAccessToken personalAccessToken) {

@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();
GithubApiClient apiClient;
if (githubApiClient.isConnected(params.getScmProviderUrl())) {
// The url from the token has the same url as the api client, no need to create a new one.
apiClient = githubApiClient;
tolusha marked this conversation as resolved.
Show resolved Hide resolved
} else {
if ("github".equals(params.getScmTokenName())) {
apiClient = new GithubApiClient(params.getScmProviderUrl());
} else {
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());
Pair<String, String[]> pair = apiClient.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());
GithubUser user = apiClient.getUser(params.getToken());
return Optional.of(Pair.of(Boolean.TRUE, user.getLogin()));
}
} catch (ScmItemNotFoundException | ScmCommunicationException | ScmBadRequestException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
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.net.HttpURLConnection.HTTP_UNAUTHORIZED;
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;
Expand Down Expand Up @@ -46,15 +47,19 @@ public abstract class AbstractGithubURLParser {
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 String githubPatternTemplate =
"^%s/(?<repoUser>[^/]+)/(?<repoName>[^/]++)((/)|(?:/tree/(?<branchName>.++))|(/pull/(?<pullRequestId>\\d++)))?$";

private final Pattern githubSSHPattern;

private final String githubSSHPatternTemplate = "^git@%s:(?<repoUser>.*)/(?<repoName>.*)$";

private final boolean disableSubdomainIsolation;

private final String providerName;
Expand All @@ -70,26 +75,80 @@ public abstract class AbstractGithubURLParser {
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.githubPattern = compile(format(githubPatternTemplate, endpoint));
this.githubSSHPattern =
compile(format("^git@%s:(?<repoUser>.*)/(?<repoName>.*)$", URI.create(endpoint).getHost()));
compile(format(githubSSHPatternTemplate, URI.create(endpoint).getHost()));
}

public boolean isValid(@NotNull String url) {
String trimmedUrl = trimEnd(url, '/');
return githubPattern.matcher(trimmedUrl).matches()
|| githubSSHPattern.matcher(trimmedUrl).matches();
|| githubSSHPattern.matcher(trimmedUrl).matches()
// If the GitHub URL is not configured, try to find it in a manually added user namespace
// token.
|| isUserTokenPresent(trimmedUrl)
// Try to call an API request to see if the URL matches GitHub.
|| isApiRequestRelevant(trimmedUrl);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need extra checks here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The url might correspond to a GitHub server which is not configured so the url is unpredictable. We make a test request in case if the project is public so we could use it even without a PAT. I'll add some descriptions here.


private boolean isUserTokenPresent(String repositoryUrl) {
Optional<String> serverUrlOptional = getServerUrl(repositoryUrl);
if (serverUrlOptional.isPresent()) {
String serverUrl = serverUrlOptional.get();
try {
Optional<PersonalAccessToken> token =
tokenManager.get(EnvironmentContext.getCurrent().getSubject(), serverUrl);
if (token.isPresent()) {
PersonalAccessToken accessToken = token.get();
return accessToken.getScmTokenName().equals(providerName);
}
} catch (ScmConfigurationPersistenceException
| ScmUnauthorizedException
| ScmCommunicationException exception) {
return false;
}
}
return false;
}

private boolean isApiRequestRelevant(String repositoryUrl) {
Optional<String> serverUrlOptional = getServerUrl(repositoryUrl);
if (serverUrlOptional.isPresent()) {
GithubApiClient GithubApiClient = new GithubApiClient(serverUrlOptional.get());
try {
// If the user request catches the unauthorised error, it means that the provided url
// belongs to GitHub.
GithubApiClient.getUser("");
} catch (ScmCommunicationException e) {
return e.getStatusCode() == HTTP_UNAUTHORIZED;
} catch (ScmItemNotFoundException | ScmBadRequestException | IllegalArgumentException e) {
return false;
}
}
return false;
}

private Optional<String> getServerUrl(String repositoryUrl) {
// If the given repository url is an SSH url, generate the base url from the pattern:
// https://<hostname extracted from the SSH url>.
if (repositoryUrl.startsWith("git@")) {
String substring = repositoryUrl.substring(4);
return Optional.of("https://" + substring.substring(0, substring.indexOf(":")));
}
// Otherwise, extract the base url from the given repository url by cutting the url after the
// first slash.
Matcher serverUrlMatcher = compile("[^/|:]/").matcher(repositoryUrl);
if (serverUrlMatcher.find()) {
return Optional.of(
repositoryUrl.substring(0, repositoryUrl.indexOf(serverUrlMatcher.group()) + 1));
}
return Optional.empty();
}

public GithubUrl parseWithoutAuthentication(String url) throws ApiException {
Expand All @@ -100,18 +159,27 @@ public GithubUrl parse(String url) throws ApiException {
return parse(trimEnd(url, '/'), true);
}

private IllegalArgumentException buildIllegalArgumentException(String url) {
return new IllegalArgumentException(
format("The given url %s is not a valid github URL. ", url));
}

private GithubUrl parse(String url, boolean authenticationRequired) throws ApiException {
Matcher matcher;
boolean isHTTPSUrl = githubPattern.matcher(url).matches();
Matcher matcher = isHTTPSUrl ? githubPattern.matcher(url) : githubSSHPattern.matcher(url);
if (isHTTPSUrl) {
matcher = githubPattern.matcher(url);
} else if (githubSSHPattern.matcher(url).matches()) {
matcher = githubSSHPattern.matcher(url);
} else {
matcher = getPatternMatcherByUrl(url).orElseThrow(() -> buildIllegalArgumentException(url));
isHTTPSUrl = url.startsWith("http");
}
if (!matcher.matches()) {
throw new IllegalArgumentException(
format("The given url %s is not a valid github URL. ", url));
throw buildIllegalArgumentException(url);
}

String serverUrl =
isNullOrEmpty(oauthEndpoint) || trimEnd(oauthEndpoint, '/').equals(GITHUB_SAAS_ENDPOINT)
? null
: trimEnd(oauthEndpoint, '/');
String serverUrl = getServerUrl(url).orElseThrow(() -> buildIllegalArgumentException(url));
String repoUser = matcher.group("repoUser");
String repoName = matcher.group("repoName");
if (repoName.matches("^[\\w-][\\w.-]*?\\.git$")) {
Expand All @@ -127,7 +195,7 @@ private GithubUrl parse(String url, boolean authenticationRequired) throws ApiEx

if (pullRequestId != null) {
GithubPullRequest pullRequest =
this.getPullRequest(pullRequestId, repoUser, repoName, authenticationRequired);
this.getPullRequest(serverUrl, pullRequestId, repoUser, repoName, authenticationRequired);
if (pullRequest != null) {
String state = pullRequest.getState();
if (!"open".equalsIgnoreCase(state)) {
Expand All @@ -146,8 +214,12 @@ private GithubUrl parse(String url, boolean authenticationRequired) throws ApiEx

String latestCommit = null;
GithubCommit commit =
this.getLatestCommit(
repoUser, repoName, firstNonNull(branchName, "HEAD"), authenticationRequired);
getLatestCommit(
serverUrl,
repoUser,
repoName,
firstNonNull(branchName, "HEAD"),
authenticationRequired);
if (commit != null) {
latestCommit = commit.getSha();
}
Expand All @@ -165,12 +237,14 @@ private GithubUrl parse(String url, boolean authenticationRequired) throws ApiEx
}

private GithubPullRequest getPullRequest(
String pullRequestId, String repoUser, String repoName, boolean authenticationRequired)
String githubEndpoint,
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);
Expand All @@ -180,8 +254,13 @@ private GithubPullRequest getPullRequest(
personalAccessToken = tokenManager.fetchAndSave(subject, githubEndpoint);
}

GithubApiClient apiClient =
this.apiClient.isConnected(githubEndpoint)
? this.apiClient
: new GithubApiClient(githubEndpoint);

// get pull request
return this.apiClient.getPullRequest(
return apiClient.getPullRequest(
pullRequestId,
repoUser,
repoName,
Expand All @@ -190,7 +269,7 @@ private GithubPullRequest getPullRequest(

// get pull request without authentication
try {
return this.apiClient.getPullRequest(pullRequestId, repoUser, repoName, null);
return apiClient.getPullRequest(pullRequestId, repoUser, repoName, null);
} catch (ScmItemNotFoundException
| ScmCommunicationException
| ScmBadRequestException exception) {
Expand All @@ -211,12 +290,17 @@ private GithubPullRequest getPullRequest(
}

private GithubCommit getLatestCommit(
String repoUser, String repoName, String branchName, boolean authenticationRequired)
throws ApiException {
String githubEndpoint,
String repoUser,
String repoName,
String branchName,
boolean authenticationRequired) {
GithubApiClient apiClient =
this.apiClient.isConnected(githubEndpoint)
? this.apiClient
: new GithubApiClient(githubEndpoint);
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);
Expand All @@ -227,15 +311,15 @@ private GithubCommit getLatestCommit(
}

// get latest commit
return this.apiClient.getLatestCommit(
return 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);
return apiClient.getLatestCommit(repoUser, repoName, branchName, null);
} catch (ScmItemNotFoundException
| ScmCommunicationException
| ScmBadRequestException
Expand All @@ -253,4 +337,21 @@ private GithubCommit getLatestCommit(

return null;
}

private Optional<Matcher> getPatternMatcherByUrl(String url) {
URI uri =
URI.create(
url.matches(format(githubSSHPatternTemplate, ".*"))
? "ssh://" + url.replace(":", "/")
: url);
String scheme = uri.getScheme();
String host = uri.getHost();
Matcher matcher = compile(format(githubPatternTemplate, scheme + "://" + host)).matcher(url);
if (matcher.matches()) {
return Optional.of(matcher);
} else {
matcher = compile(format(githubSSHPatternTemplate, host)).matcher(url);
return matcher.matches() ? Optional.of(matcher) : Optional.empty();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
*/
package org.eclipse.che.api.factory.server.github;

import static com.google.common.base.Strings.isNullOrEmpty;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import java.util.Set;
Expand All @@ -36,6 +38,9 @@ public abstract class AbstractGithubUserDataFetcher extends AbstractGitUserDataF
public static final Set<String> DEFAULT_TOKEN_SCOPES =
ImmutableSet.of("repo", "user:email", "read:user");

private static final String NO_USERNAME_AND_EMAIL_ERROR_MESSAGE =
"User name and email is not found in the GitHub profile.";

/** Constructor used for testing only. */
public AbstractGithubUserDataFetcher(
String apiEndpoint,
Expand All @@ -53,15 +58,25 @@ public AbstractGithubUserDataFetcher(
protected GitUserData fetchGitUserDataWithOAuthToken(OAuthToken oAuthToken)
throws ScmItemNotFoundException, ScmCommunicationException, ScmBadRequestException {
GithubUser user = githubApiClient.getUser(oAuthToken.getToken());
return new GitUserData(user.getName(), user.getEmail());
if (!isNullOrEmpty(user.getName()) && !isNullOrEmpty(user.getEmail())) {
tolusha marked this conversation as resolved.
Show resolved Hide resolved
return new GitUserData(user.getName(), user.getEmail());
}
throw new ScmItemNotFoundException(NO_USERNAME_AND_EMAIL_ERROR_MESSAGE);
}

@Override
protected GitUserData fetchGitUserDataWithPersonalAccessToken(
PersonalAccessToken personalAccessToken)
throws ScmItemNotFoundException, ScmCommunicationException, ScmBadRequestException {
GithubUser user = githubApiClient.getUser(personalAccessToken.getToken());
return new GitUserData(user.getName(), user.getEmail());
GithubApiClient apiClient =
githubApiClient.isConnected(personalAccessToken.getScmProviderUrl())
? githubApiClient
: new GithubApiClient(personalAccessToken.getScmProviderUrl());
GithubUser user = apiClient.getUser(personalAccessToken.getToken());
if (!isNullOrEmpty(user.getName()) && !isNullOrEmpty(user.getEmail())) {
return new GitUserData(user.getName(), user.getEmail());
}
throw new ScmItemNotFoundException(NO_USERNAME_AND_EMAIL_ERROR_MESSAGE);
}

protected String getLocalAuthenticateUrl() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ public String rawFileLocation(String fileName) {

return new StringJoiner("/")
.add(
isNullOrEmpty(serverUrl)
HOSTNAME.equals(serverUrl)
? "https://raw.githubusercontent.com"
: disableSubdomainIsolation
? serverUrl + "/raw"
Expand All @@ -208,7 +208,8 @@ public String rawFileLocation(String fileName) {

@Override
public String getHostName() {
return isNullOrEmpty(serverUrl) ? HOSTNAME : serverUrl;
// TODO: rework this method to return hostname in format https://<hostname>/user/repo
return serverUrl;
}

/**
Expand All @@ -218,12 +219,7 @@ public String getHostName() {
*/
protected String repositoryLocation() {
if (isHTTPSUrl) {
return (isNullOrEmpty(serverUrl) ? HOSTNAME : serverUrl)
+ "/"
+ this.username
+ "/"
+ this.repository
+ ".git";
return serverUrl + "/" + this.username + "/" + this.repository + ".git";
}
return "git@"
+ getHostName().substring(getHostName().indexOf("://") + 3)
Expand Down
Loading