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

chore: Add an ability to manually configure bitbucket oAuth token #313

Merged
merged 10 commits into from
Jun 29, 2022
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* Copyright (c) 2012-2022 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* Copyright (c) 2012-2022 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
Expand Down Expand Up @@ -49,7 +49,6 @@ public String computeAuthorizationHeader(String userId, String requestMethod, St

@Override
public String getLocalAuthenticateUrl() {
throw new RuntimeException(
"The fallback noop authenticator cannot be used for authentication. Make sure OAuth is properly configured.");
return "Empty URL";
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not to keep throwing RuntimeException ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To avoid the exception when the NoopOAuthAuthenticator is used to create an API client with an empty authenticator:

vinokurig marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* Copyright (c) 2012-2022 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
Expand All @@ -25,6 +25,7 @@
import org.eclipse.che.api.factory.server.bitbucket.server.BitbucketPersonalAccessToken;
import org.eclipse.che.api.factory.server.bitbucket.server.BitbucketServerApiClient;
import org.eclipse.che.api.factory.server.bitbucket.server.BitbucketUser;
import org.eclipse.che.api.factory.server.bitbucket.server.HttpBitbucketServerApiClient;
import org.eclipse.che.api.factory.server.scm.PersonalAccessToken;
import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenFetcher;
import org.eclipse.che.api.factory.server.scm.exception.ScmBadRequestException;
Expand All @@ -33,6 +34,7 @@
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.subject.Subject;
import org.eclipse.che.security.oauth1.NoopOAuthAuthenticator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -104,8 +106,20 @@ public PersonalAccessToken fetchPersonalAccessToken(Subject cheUser, String scmS
public Optional<Boolean> isValid(PersonalAccessToken personalAccessToken)
throws ScmCommunicationException, ScmUnauthorizedException {
if (!bitbucketServerApiClient.isConnected(personalAccessToken.getScmProviderUrl())) {
LOG.debug("not a valid url {} for current fetcher ", personalAccessToken.getScmProviderUrl());
return Optional.empty();
// If BitBucket oAuth is not configured check the manually added user namespace token.
HttpBitbucketServerApiClient apiClient =
Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit lost in the flow, why are we trying to check the token if BitBucket server is not configured?

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 main idea of the PR is to be able to use manually added user namespace secrets with tokeen for the oAuth flow in case when oAuth is not configured by the admin.

new HttpBitbucketServerApiClient(
personalAccessToken.getScmProviderUrl(), new NoopOAuthAuthenticator());
try {
apiClient.getUser(personalAccessToken.getScmUserName(), personalAccessToken.getToken());
return Optional.of(Boolean.TRUE);
} catch (ScmItemNotFoundException
| ScmUnauthorizedException
| ScmCommunicationException exception) {
LOG.debug(
"not a valid url {} for current fetcher ", personalAccessToken.getScmProviderUrl());
return Optional.empty();
}
}
try {
BitbucketPersonalAccessToken bitbucketPersonalAccessToken =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* Copyright (c) 2012-2022 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
Expand All @@ -17,13 +17,20 @@
import jakarta.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.eclipse.che.api.factory.server.scm.PersonalAccessToken;
import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager;
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
import org.eclipse.che.api.factory.server.scm.exception.ScmConfigurationPersistenceException;
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
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.lang.StringUtils;

/**
Expand All @@ -35,15 +42,19 @@
public class BitbucketURLParser {

private final DevfileFilenamesProvider devfileFilenamesProvider;
private final PersonalAccessTokenManager personalAccessTokenManager;
private static final String bitbucketUrlPatternTemplate =
"^(?<host>%s)/scm/(?<project>[^/]++)/(?<repo>[^.]++).git(\\?at=)?(?<branch>[\\w\\d-_]*)";
private final List<Pattern> bitbucketUrlPatterns = new ArrayList<>();
private static final String OAUTH_PROVIDER_NAME = "bitbucket-server";

@Inject
public BitbucketURLParser(
@Nullable @Named("che.integration.bitbucket.server_endpoints") String bitbucketEndpoints,
DevfileFilenamesProvider devfileFilenamesProvider) {
DevfileFilenamesProvider devfileFilenamesProvider,
PersonalAccessTokenManager personalAccessTokenManager) {
this.devfileFilenamesProvider = devfileFilenamesProvider;
this.personalAccessTokenManager = personalAccessTokenManager;
if (bitbucketEndpoints != null) {
for (String bitbucketEndpoint : Splitter.on(",").split(bitbucketEndpoints)) {
String trimmedEndpoint = StringUtils.trimEnd(bitbucketEndpoint, '/');
Expand All @@ -53,9 +64,42 @@ public BitbucketURLParser(
}
}

private boolean isUserTokenExists(String repositoryUrl) {
String serverUrl = getServerUrl(repositoryUrl);
if (Pattern.compile(format(bitbucketUrlPatternTemplate, serverUrl))
.matcher(repositoryUrl)
.matches()) {
try {
Optional<PersonalAccessToken> token =
personalAccessTokenManager.get(EnvironmentContext.getCurrent().getSubject(), serverUrl);
return token.isPresent() && token.get().getScmTokenName().equals(OAUTH_PROVIDER_NAME);
} catch (ScmConfigurationPersistenceException
| ScmUnauthorizedException
| ScmCommunicationException exception) {
return false;
}
}
return false;
}

public boolean isValid(@NotNull String url) {
return !bitbucketUrlPatterns.isEmpty()
&& bitbucketUrlPatterns.stream().anyMatch(pattern -> pattern.matcher(url).matches());
if (!bitbucketUrlPatterns.isEmpty()) {
return bitbucketUrlPatterns.stream().anyMatch(pattern -> pattern.matcher(url).matches());
} else {
// If Bitbucket server URL is not configured try to find it in a manually added user namespace
// token.
return isUserTokenExists(url);
}
}

private String getServerUrl(String repositoryUrl) {
return repositoryUrl.substring(
0,
repositoryUrl.indexOf("/scm") > 0 ? repositoryUrl.indexOf("/scm") : repositoryUrl.length());
}

private Matcher getPatternMatcherByUrl(String url) {
return Pattern.compile(format(bitbucketUrlPatternTemplate, getServerUrl(url))).matcher(url);
}

/**
Expand All @@ -66,6 +110,10 @@ public boolean isValid(@NotNull String url) {
public BitbucketUrl parse(String url) {

if (bitbucketUrlPatterns.isEmpty()) {
Matcher patternMatcher = getPatternMatcherByUrl(url);
if (patternMatcher.matches()) {
return parse(patternMatcher);
}
throw new UnsupportedOperationException(
"The Bitbucket integration is not configured properly and cannot be used at this moment."
+ "Please refer to docs to check the Bitbucket integration instructions");
Expand All @@ -82,6 +130,10 @@ public BitbucketUrl parse(String url) {
format(
"The given url %s is not a valid Bitbucket server URL. Check either URL or server configuration.",
url)));
return parse(matcher);
}

private BitbucketUrl parse(Matcher matcher) {
String host = matcher.group("host");
String project = matcher.group("project");
String repoName = matcher.group("repo");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* Copyright (c) 2012-2022 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
Expand All @@ -17,6 +17,7 @@
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
import org.eclipse.che.api.factory.server.scm.exception.ScmItemNotFoundException;
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.commons.subject.Subject;

/** Bitbucket Server API client. */
Expand All @@ -36,13 +37,14 @@ BitbucketUser getUser(Subject cheUser)
throws ScmUnauthorizedException, ScmCommunicationException, ScmItemNotFoundException;

/**
* @param slug
* @param slug scm username.
* @param token token to override. Pass {@code null} to use token from the authentication flow.
* @return - Retrieve the {@link BitbucketUser} matching the supplied userSlug.
* @throws ScmItemNotFoundException
* @throws ScmUnauthorizedException
* @throws ScmCommunicationException
*/
BitbucketUser getUser(String slug)
BitbucketUser getUser(String slug, @Nullable String token)
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
import org.eclipse.che.api.factory.server.scm.exception.ScmItemNotFoundException;
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler;
import org.eclipse.che.commons.subject.Subject;
Expand Down Expand Up @@ -128,12 +129,16 @@ public BitbucketUser getUser(Subject cheUser)
}

@Override
public BitbucketUser getUser(String slug)
public BitbucketUser getUser(String slug, @Nullable String token)
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
URI uri = serverUri.resolve("/rest/api/1.0/users/" + slug);
HttpRequest request =
HttpRequest.newBuilder(uri)
.headers("Authorization", computeAuthorizationHeader("GET", uri.toString()))
.headers(
"Authorization",
token != null
? "Bearer " + token
: computeAuthorizationHeader("GET", uri.toString()))
.timeout(DEFAULT_HTTP_TIMEOUT)
.build();

Expand Down Expand Up @@ -308,7 +313,7 @@ private Optional<BitbucketUser> findCurrentUser(Set<String> userSlugs)
throws ScmCommunicationException, ScmUnauthorizedException, ScmItemNotFoundException {

for (String userSlug : userSlugs) {
BitbucketUser user = getUser(userSlug);
BitbucketUser user = getUser(userSlug, null);
try {
getPersonalAccessTokens(userSlug);
return Optional.of(user);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* Copyright (c) 2012-2022 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
Expand All @@ -17,6 +17,7 @@
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
import org.eclipse.che.api.factory.server.scm.exception.ScmItemNotFoundException;
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.commons.subject.Subject;

/**
Expand All @@ -37,7 +38,7 @@ public BitbucketUser getUser(Subject cheUser)
}

@Override
public BitbucketUser getUser(String slug)
public BitbucketUser getUser(String slug, @Nullable String token)
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
throw new RuntimeException(
"The fallback noop api client cannot be used for real operation. Make sure Bitbucket OAuth1 is properly configured.");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* Copyright (c) 2012-2022 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
Expand All @@ -19,6 +19,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
Expand Down Expand Up @@ -68,7 +69,10 @@ public class BitbucketServerAuthorizingFactoryParametersResolverTest {
@BeforeMethod
protected void init() {
bitbucketURLParser =
new BitbucketURLParser("http://bitbucket.2mcl.com", devfileFilenamesProvider);
new BitbucketURLParser(
"http://bitbucket.2mcl.com",
devfileFilenamesProvider,
mock(PersonalAccessTokenManager.class));
assertNotNull(this.bitbucketURLParser);
bitbucketServerFactoryParametersResolver =
new BitbucketServerAuthorizingFactoryParametersResolver(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* Copyright (c) 2012-2022 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* Copyright (c) 2012-2022 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
Expand All @@ -14,6 +14,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.testng.Assert.*;

Expand Down Expand Up @@ -47,7 +48,9 @@ public class BitbucketServerScmFileResolverTest {

@BeforeMethod
protected void init() {
bitbucketURLParser = new BitbucketURLParser(SCM_URL, devfileFilenamesProvider);
bitbucketURLParser =
new BitbucketURLParser(
SCM_URL, devfileFilenamesProvider, mock(PersonalAccessTokenManager.class));
assertNotNull(this.bitbucketURLParser);
serverScmFileResolver =
new BitbucketServerScmFileResolver(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* Copyright (c) 2012-2022 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
Expand All @@ -11,9 +11,11 @@
*/
package org.eclipse.che.api.factory.server.bitbucket;

import static org.mockito.Mockito.mock;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;

import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager;
import org.eclipse.che.api.factory.server.urlfactory.DevfileFilenamesProvider;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
Expand All @@ -34,7 +36,9 @@ public class BitbucketURLParserTest {
public void setUp() {
bitbucketURLParser =
new BitbucketURLParser(
"https://bitbucket.2mcl.com,https://bbkt.com", devfileFilenamesProvider);
"https://bitbucket.2mcl.com,https://bbkt.com",
devfileFilenamesProvider,
mock(PersonalAccessTokenManager.class));
}

/** Check URLs are valid with regexp */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* Copyright (c) 2012-2022 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
Expand Down Expand Up @@ -95,7 +95,7 @@ public void testGetUser()
.withHeader("Content-Type", "application/json; charset=utf-8")
.withBodyFile("bitbucket/rest/api/1.0/users/ksmster/response.json")));

BitbucketUser user = bitbucketServer.getUser("ksmster");
BitbucketUser user = bitbucketServer.getUser("ksmster", null);
assertNotNull(user);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,13 @@ public Optional<Boolean> isValid(PersonalAccessToken personalAccessToken) {
GitlabApiClient gitlabApiClient = getApiClient(personalAccessToken.getScmProviderUrl());
if (gitlabApiClient == null
|| !gitlabApiClient.isConnected(personalAccessToken.getScmProviderUrl())) {
LOG.debug(
"not a valid url {} for current fetcher ", personalAccessToken.getScmProviderUrl());
return Optional.empty();
if (personalAccessToken.getScmTokenName().equals(OAUTH_PROVIDER_NAME)) {
gitlabApiClient = new GitlabApiClient(personalAccessToken.getScmProviderUrl());
} else {
LOG.debug(
"not a valid url {} for current fetcher ", personalAccessToken.getScmProviderUrl());
return Optional.empty();
}
}
if (personalAccessToken.getScmTokenName() != null
&& personalAccessToken.getScmTokenName().startsWith(OAUTH_2_PREFIX)) {
Expand Down
Loading