Obtain and persist Bitbucket personal access token as k8s secret (#18726)

* Obtain and persist Bitbucket personal access token as k8s secret

Signed-off-by: Sergii Kabashniuk <skabashniuk@redhat.com>
7.28.x
Sergii Kabashniuk 2021-02-11 16:17:13 +02:00 committed by GitHub
parent d51e1d0edf
commit 0d0a68fc00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 2702 additions and 14 deletions

View File

@ -111,6 +111,10 @@
<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-bitbucket</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-auth-openshift</artifactId>

View File

@ -165,6 +165,7 @@ public class WsMasterModule extends AbstractModule {
bind(org.eclipse.che.api.user.server.ProfileService.class);
bind(org.eclipse.che.api.user.server.PreferencesService.class);
bind(org.eclipse.che.security.oauth.OAuthAuthenticationService.class);
bind(org.eclipse.che.security.oauth1.OAuthAuthenticationService.class);
install(new DevfileModule());
@ -256,6 +257,7 @@ public class WsMasterModule extends AbstractModule {
install(new FactoryModuleBuilder().build(JwtProxyConfigBuilderFactory.class));
install(new FactoryModuleBuilder().build(PassThroughProxyProvisionerFactory.class));
installDefaultSecureServerExposer(infrastructure);
install(new org.eclipse.che.security.oauth1.BitbucketModule());
if (Boolean.valueOf(System.getenv("CHE_MULTIUSER"))) {
configureMultiUserMode(persistenceProperties, infrastructure);

View File

@ -184,6 +184,16 @@ che.oauth.openshift.clientsecret=NULL
che.oauth.openshift.oauth_endpoint= NULL
che.oauth.openshift.verify_token_url= NULL
# Configuration of Bitbucket Server OAuth1 client. Used to obtain Personal access tokens.
# Location of the file with Bitbucket Server application consumer key (equivalent to a username).
che.oauth1.bitbucket.consumerkeypath=NULL
# Location of the file with Bitbucket Server application private key
che.oauth1.bitbucket.privatekeypath=NULL
# Bitbucket Server URL. To work correctly with factories the same URL
# has to be part of `che.integration.bitbucket.server_endpoints` too.
che.oauth1.bitbucket.endpoint=NULL
### Internal
# Che extensions can be scheduled executions on a time basis.

View File

@ -68,11 +68,6 @@
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8-standalone</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>

View File

@ -24,8 +24,9 @@ public class KeycloakServletModule extends ServletModule {
+ "(?!/keycloak/(OIDC|oidc)[^\\/]+$)"
// not contains /docs/ (for swagger)
+ "(?!.*(/docs/))"
// not ends with '/oauth/callback/' or '/keycloak/settings/' or '/system/state'
+ "(?!.*(/keycloak/settings/?|/oauth/callback/?|/system/state/?)$)"
// not ends with '/oauth/callback/' or '/oauth/1.0/callback/' or '/keycloak/settings/' or
// '/system/state'
+ "(?!.*(/keycloak/settings/?|/oauth/callback/?|/oauth/1.0/callback/?|/system/state/?)$)"
// all other
+ ".*";

View File

@ -700,6 +700,11 @@
<artifactId>che-core-api-auth</artifactId>
<version>${che.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-auth-bitbucket</artifactId>
<version>${che.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-auth-github</artifactId>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2012-2018 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.27.0-SNAPSHOT</version>
</parent>
<artifactId>che-core-api-auth-bitbucket</artifactId>
<packaging>jar</packaging>
<name>Che Core :: API :: Authentication Bitbucket</name>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
</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-commons-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2012-2018 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.oauth1;
import com.google.inject.AbstractModule;
import com.google.inject.multibindings.Multibinder;
/**
* Setup BitbucketServerOAuthAuthenticator in guice container.
*
* @author Sergii Kabashniuk
*/
public class BitbucketModule extends AbstractModule {
@Override
protected void configure() {
Multibinder<OAuthAuthenticator> oAuthAuthenticators =
Multibinder.newSetBinder(binder(), OAuthAuthenticator.class);
oAuthAuthenticators.addBinding().toProvider(BitbucketServerOAuthAuthenticatorProvider.class);
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2012-2018 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.oauth1;
import com.google.inject.Singleton;
/**
* OAuth1 authentication for Bitbucket Server account.
*
* @author Igor Vinokur
*/
@Singleton
public class BitbucketServerOAuthAuthenticator extends OAuthAuthenticator {
public static final String AUTHENTICATOR_NAME = "bitbucket-server";
public BitbucketServerOAuthAuthenticator(
String consumerKey, String privateKey, String bitbucketEndpoint, String apiEndpoint) {
super(
consumerKey,
bitbucketEndpoint + "/plugins/servlet/oauth/request-token",
bitbucketEndpoint + "/plugins/servlet/oauth/access-token",
bitbucketEndpoint + "/plugins/servlet/oauth/authorize",
apiEndpoint + "/oauth/1.0/callback",
null,
privateKey);
}
@Override
public final String getOAuthProvider() {
return AUTHENTICATOR_NAME;
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) 2012-2018 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.oauth1;
import static com.google.common.base.Strings.isNullOrEmpty;
import com.google.inject.name.Named;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.eclipse.che.commons.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class BitbucketServerOAuthAuthenticatorProvider implements Provider<OAuthAuthenticator> {
private static final Logger LOG =
LoggerFactory.getLogger(BitbucketServerOAuthAuthenticatorProvider.class);
private final OAuthAuthenticator authenticator;
@Inject
public BitbucketServerOAuthAuthenticatorProvider(
@Nullable @Named("che.oauth1.bitbucket.consumerkeypath") String consumerKeyPath,
@Nullable @Named("che.oauth1.bitbucket.privatekeypath") String privateKeyPath,
@Nullable @Named("che.oauth1.bitbucket.endpoint") String bitbucketEndpoint,
@Named("che.api") String apiEndpoint)
throws IOException {
authenticator =
getOAuthAuthenticator(consumerKeyPath, privateKeyPath, bitbucketEndpoint, apiEndpoint);
LOG.debug("{} Bitbucket OAuthAuthenticator is used.", authenticator);
}
@Override
public OAuthAuthenticator get() {
return authenticator;
}
private static OAuthAuthenticator getOAuthAuthenticator(
String consumerKeyPath, String privateKeyPath, String bitbucketEndpoint, String apiEndpoint)
throws IOException {
if (!isNullOrEmpty(bitbucketEndpoint)
&& !isNullOrEmpty(consumerKeyPath)
&& !isNullOrEmpty(privateKeyPath)) {
String consumerKey = Files.readString(Path.of(consumerKeyPath));
String privateKey = Files.readString(Path.of(privateKeyPath));
if (!isNullOrEmpty(consumerKey) && !isNullOrEmpty(privateKey)) {
return new BitbucketServerOAuthAuthenticator(
consumerKey, privateKey, bitbucketEndpoint, apiEndpoint);
}
}
return new NoopOAuthAuthenticator();
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2012-2018 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.oauth1;
import java.net.URL;
/**
* Dummy implementation of @{@link OAuthAuthenticator} used in the case if no Bitbucket Server
* integration is configured.
*/
public class NoopOAuthAuthenticator extends OAuthAuthenticator {
protected NoopOAuthAuthenticator() {
super(null, null, null, null, null, null, null);
}
@Override
String getOAuthProvider() {
return "Noop";
}
@Override
String getAuthenticateUrl(URL requestUrl, String requestMethod, String signatureMethod)
throws OAuthAuthenticationException {
throw new RuntimeException(
"The fallback noop authenticator cannot be used for authentication. Make sure OAuth is properly configured.");
}
@Override
String callback(URL requestUrl) throws OAuthAuthenticationException {
throw new RuntimeException(
"The fallback noop authenticator cannot be used for authentication. Make sure OAuth is properly configured.");
}
@Override
String computeAuthorizationHeader(String userId, String requestMethod, String requestUrl)
throws OAuthAuthenticationException {
throw new RuntimeException(
"The fallback noop authenticator cannot be used for authentication. Make sure OAuth is properly configured.");
}
}

View File

@ -0,0 +1,83 @@
/*
* Copyright (c) 2012-2018 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.oauth1;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
public class BitbucketServerOAuthAuthenticatorProviderTest {
private File cfgFile;
private File emptyFile;
@BeforeClass
public void setup() throws IOException {
cfgFile = File.createTempFile("BitbucketServerOAuthAuthenticatorProviderTest-", "-cfg");
Files.asCharSink(cfgFile, Charset.defaultCharset()).write("tmp-data");
cfgFile.deleteOnExit();
emptyFile = File.createTempFile("BitbucketServerOAuthAuthenticatorProviderTest-", "-empty");
emptyFile.deleteOnExit();
}
@Test(dataProvider = "noopConfig")
public void shouldProvideNoopOAuthAuthenticatorIfSomeConfigurationIsNotSet(
String consumerKeyPath, String privateKeyPath, String bitbucketEndpoint) throws IOException {
// given
BitbucketServerOAuthAuthenticatorProvider provider =
new BitbucketServerOAuthAuthenticatorProvider(
consumerKeyPath, privateKeyPath, bitbucketEndpoint, "http://che.server.com");
// when
OAuthAuthenticator actual = provider.get();
// then
assertNotNull(actual);
assertTrue(NoopOAuthAuthenticator.class.isAssignableFrom(actual.getClass()));
}
@Test
public void shouldBeAbleToConfigureValidBitbucketServerOAuthAuthenticator() throws IOException {
// given
BitbucketServerOAuthAuthenticatorProvider provider =
new BitbucketServerOAuthAuthenticatorProvider(
cfgFile.getPath(), cfgFile.getPath(), "http://bitubucket.com", "http://che.server.com");
// when
OAuthAuthenticator actual = provider.get();
// then
assertNotNull(actual);
assertTrue(BitbucketServerOAuthAuthenticator.class.isAssignableFrom(actual.getClass()));
}
@DataProvider(name = "noopConfig")
public Object[][] noopConfig() {
return new Object[][] {
{null, null, null},
{cfgFile.getPath(), null, null},
{null, cfgFile.getPath(), null},
{cfgFile.getPath(), cfgFile.getPath(), null},
{emptyFile.getPath(), null, null},
{null, emptyFile.getPath(), null},
{emptyFile.getPath(), emptyFile.getPath(), null},
{cfgFile.getPath(), emptyFile.getPath(), null},
{emptyFile.getPath(), cfgFile.getPath(), null},
{emptyFile.getPath(), emptyFile.getPath(), "http://bitubucket.com"},
{cfgFile.getPath(), emptyFile.getPath(), "http://bitubucket.com"},
{emptyFile.getPath(), cfgFile.getPath(), "http://bitubucket.com"},
{null, null, "http://bitubucket.com"}
};
}
}

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2012-2018 Red Hat, Inc.
This program and the accompanying materials are made
available under the terms of the Eclipse Public License 2.0
which is available at https://www.eclipse.org/legal/epl-2.0/
SPDX-License-Identifier: EPL-2.0
Contributors:
Red Hat, Inc. - initial API and implementation
-->
<configuration>
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="stdout"/>
</root>
</configuration>

View File

@ -28,6 +28,7 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import org.eclipse.che.api.core.BadRequestException;
import org.eclipse.che.api.core.rest.Service;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -82,17 +83,16 @@ public class OAuthAuthenticationService extends Service {
@Path("signature")
public String signature(
@QueryParam("oauth_provider") String providerName,
@QueryParam("user_id") String userId,
@QueryParam("request_url") String requestUrl,
@QueryParam("request_method") String requestMethod)
throws OAuthAuthenticationException, BadRequestException {
requiredNotNull(providerName, "Provider name");
requiredNotNull(userId, "User Id");
requiredNotNull(requestUrl, "Request url");
requiredNotNull(requestMethod, "Request method");
return getAuthenticator(providerName)
.computeAuthorizationHeader(userId, requestMethod, requestUrl);
.computeAuthorizationHeader(
EnvironmentContext.getCurrent().getSubject().getUserId(), requestMethod, requestUrl);
}
private OAuthAuthenticator getAuthenticator(String oauthProviderName) throws BadRequestException {

View File

@ -39,6 +39,7 @@ import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
import org.eclipse.che.api.auth.shared.dto.OAuthToken;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.commons.env.EnvironmentContext;
/**
* Authentication service which allows get access token from OAuth provider site.
@ -105,7 +106,24 @@ public abstract class OAuthAuthenticator {
throws OAuthAuthenticationException {
try {
final GenericUrl callbackUrl = new GenericUrl(redirectUri);
callbackUrl.put(STATE_PARAM_KEY, requestUrl.getQuery());
String userId = getParameterFromState(requestUrl.getQuery(), USER_ID_PARAM_KEY);
String currentUserId = EnvironmentContext.getCurrent().getSubject().getUserId();
if (userId != null) {
if (currentUserId.equals(userId)) {
callbackUrl.put(STATE_PARAM_KEY, requestUrl.getQuery());
} else {
throw new OAuthAuthenticationException(
"Provided query parameter "
+ USER_ID_PARAM_KEY
+ "="
+ userId
+ " does not match the current user id: "
+ currentUserId);
}
} else {
callbackUrl.put(
STATE_PARAM_KEY, requestUrl.getQuery() + "&" + USER_ID_PARAM_KEY + "=" + currentUserId);
}
OAuthGetTemporaryToken temporaryToken;
if (requestMethod != null && "post".equalsIgnoreCase(requestMethod)) {

View File

@ -26,6 +26,18 @@
<findbugs.failonerror>false</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-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
@ -42,6 +54,18 @@
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-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-bitbucket</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-core</artifactId>
@ -70,15 +94,28 @@
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-annotations</artifactId>
</dependency>
<dependency>
<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>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8-standalone</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-json</artifactId>

View File

@ -13,7 +13,9 @@ package org.eclipse.che.api.factory.server.bitbucket;
import com.google.inject.AbstractModule;
import com.google.inject.multibindings.Multibinder;
import org.eclipse.che.api.factory.server.bitbucket.server.BitbucketServerApiClient;
import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenFetcher;
import org.eclipse.che.security.oauth1.BitbucketServerApiProvider;
public class BitbucketServerModule extends AbstractModule {
@Override
@ -21,5 +23,6 @@ public class BitbucketServerModule extends AbstractModule {
Multibinder<PersonalAccessTokenFetcher> tokenFetcherMultibinder =
Multibinder.newSetBinder(binder(), PersonalAccessTokenFetcher.class);
tokenFetcherMultibinder.addBinding().to(BitbucketServerPersonalAccessTokenFetcher.class);
bind(BitbucketServerApiClient.class).toProvider(BitbucketServerApiProvider.class);
}
}

View File

@ -11,9 +11,28 @@
*/
package org.eclipse.che.api.factory.server.bitbucket;
import static java.lang.String.format;
import static java.lang.String.valueOf;
import com.google.common.collect.ImmutableSet;
import java.net.URL;
import java.util.List;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Named;
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.scm.PersonalAccessToken;
import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenFetcher;
import org.eclipse.che.api.factory.server.scm.exception.ScmBadRequestException;
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
import org.eclipse.che.api.factory.server.scm.exception.ScmItemNotFoundException;
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Bitbucket implementation for {@link PersonalAccessTokenFetcher}. Right now returns {@code null}
@ -21,8 +40,61 @@ import org.eclipse.che.commons.subject.Subject;
* class.
*/
public class BitbucketServerPersonalAccessTokenFetcher implements PersonalAccessTokenFetcher {
private static final Logger LOG =
LoggerFactory.getLogger(BitbucketServerPersonalAccessTokenFetcher.class);
private static final String TOKEN_NAME_TEMPLATE = "che-token-<%s>-<%s>";
private final BitbucketServerApiClient bitbucketServerApiClient;
private final URL apiEndpoint;
@Inject
public BitbucketServerPersonalAccessTokenFetcher(
BitbucketServerApiClient bitbucketServerApiClient, @Named("che.api") URL apiEndpoint) {
this.bitbucketServerApiClient = bitbucketServerApiClient;
this.apiEndpoint = apiEndpoint;
}
@Override
public PersonalAccessToken fetchPersonalAccessToken(Subject cheUser, String scmServerUrl) {
return null;
public PersonalAccessToken fetchPersonalAccessToken(Subject cheUser, String scmServerUrl)
throws ScmUnauthorizedException, ScmCommunicationException {
if (!bitbucketServerApiClient.isConnected(scmServerUrl)) {
LOG.debug("not a valid url {} for current fetcher ", scmServerUrl);
return null;
}
final String tokenName =
format(TOKEN_NAME_TEMPLATE, cheUser.getUserId(), apiEndpoint.getHost());
try {
BitbucketUser user =
bitbucketServerApiClient.getUser(EnvironmentContext.getCurrent().getSubject());
LOG.debug("Current bitbucket user {} ", user);
// cleanup existed
List<BitbucketPersonalAccessToken> existingTokens =
bitbucketServerApiClient
.getPersonalAccessTokens(user.getSlug())
.stream()
.filter(p -> p.getName().equals(tokenName))
.collect(Collectors.toList());
for (BitbucketPersonalAccessToken existedToken : existingTokens) {
LOG.debug("Deleting existed che token {} {}", existedToken.getId(), existedToken.getName());
bitbucketServerApiClient.deletePersonalAccessTokens(user.getSlug(), existedToken.getId());
}
BitbucketPersonalAccessToken token =
bitbucketServerApiClient.createPersonalAccessTokens(
user.getSlug(), tokenName, ImmutableSet.of("PROJECT_WRITE", "REPO_WRITE"));
LOG.debug("Token created = {} for {}", token.getId(), token.getUser());
return new PersonalAccessToken(
scmServerUrl,
EnvironmentContext.getCurrent().getSubject().getUserId(),
user.getName(),
valueOf(user.getId()),
token.getName(),
valueOf(token.getId()),
token.getToken());
} catch (ScmBadRequestException | ScmItemNotFoundException e) {
throw new ScmCommunicationException(e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2012-2018 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.bitbucket.server;
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
/** Compute the Authorization header to sign the OAuth 1 request. */
public interface AuthorizationHeaderSupplier {
String computeAuthorizationHeader(final String requestMethod, final String requestUrl)
throws ScmUnauthorizedException, ScmCommunicationException;
}

View File

@ -0,0 +1,148 @@
/*
* Copyright (c) 2012-2018 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.bitbucket.server;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.Objects;
import java.util.Set;
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public class BitbucketPersonalAccessToken {
private long id;
private long createdDate;
private long lastAuthenticated;
private String name;
private String token;
private BitbucketUser user;
private Set<String> permissions;
public BitbucketPersonalAccessToken(String name, Set<String> permissions) {
this.name = name;
this.permissions = permissions;
}
public BitbucketPersonalAccessToken() {}
public BitbucketPersonalAccessToken(
long id,
long createdDate,
long lastAuthenticated,
String name,
String token,
BitbucketUser user,
Set<String> permissions) {
this.id = id;
this.createdDate = createdDate;
this.lastAuthenticated = lastAuthenticated;
this.name = name;
this.token = token;
this.user = user;
this.permissions = permissions;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public long getCreatedDate() {
return createdDate;
}
public void setCreatedDate(long createdDate) {
this.createdDate = createdDate;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public BitbucketUser getUser() {
return user;
}
public void setUser(BitbucketUser user) {
this.user = user;
}
public Set<String> getPermissions() {
return permissions;
}
public void setPermissions(Set<String> permissions) {
this.permissions = permissions;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public long getLastAuthenticated() {
return lastAuthenticated;
}
public void setLastAuthenticated(long lastAuthenticated) {
this.lastAuthenticated = lastAuthenticated;
}
@Override
public String toString() {
return "BitbucketPersonalAccessToken{"
+ "id="
+ id
+ ", createdDate="
+ createdDate
+ ", lastAuthenticated="
+ lastAuthenticated
+ ", name='"
+ name
+ '\''
+ ", token='"
+ token
+ '\''
+ ", user="
+ user
+ ", permissions="
+ permissions
+ '}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BitbucketPersonalAccessToken that = (BitbucketPersonalAccessToken) o;
return id == that.id
&& createdDate == that.createdDate
&& lastAuthenticated == that.lastAuthenticated
&& Objects.equals(name, that.name)
&& Objects.equals(token, that.token)
&& Objects.equals(user, that.user)
&& Objects.equals(permissions, that.permissions);
}
@Override
public int hashCode() {
return Objects.hash(id, createdDate, lastAuthenticated, name, token, user, permissions);
}
}

View File

@ -0,0 +1,107 @@
/*
* Copyright (c) 2012-2018 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.bitbucket.server;
import java.util.List;
import java.util.Set;
import org.eclipse.che.api.factory.server.scm.exception.ScmBadRequestException;
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
import org.eclipse.che.api.factory.server.scm.exception.ScmItemNotFoundException;
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
import org.eclipse.che.commons.subject.Subject;
/** Bitbucket Server API client. */
public interface BitbucketServerApiClient {
/**
* @param bitbucketServerUrl
* @return - true if client is connected to the given bitbucket server.
*/
boolean isConnected(String bitbucketServerUrl);
/**
* @param cheUser - Che user.
* @return - {@link BitbucketUser} that is linked with given {@link Subject}
* @throws ScmUnauthorizedException - in case if {@link Subject} is not linked to any {@link
* BitbucketUser}
*/
BitbucketUser getUser(Subject cheUser) throws ScmUnauthorizedException, ScmCommunicationException;
/**
* @param slug
* @return - Retrieve the {@link BitbucketUser} matching the supplied userSlug.
* @throws ScmItemNotFoundException
* @throws ScmUnauthorizedException
* @throws ScmCommunicationException
*/
BitbucketUser getUser(String slug)
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException;
/**
* @return Retrieve a list of {@link BitbucketUser}. Only authenticated users may call this
* resource.
* @throws ScmBadRequestException
* @throws ScmUnauthorizedException
* @throws ScmCommunicationException
*/
List<BitbucketUser> getUsers()
throws ScmBadRequestException, ScmUnauthorizedException, ScmCommunicationException;
/**
* @return Retrieve a list of {@link BitbucketUser}, optionally run through provided filters. Only
* authenticated users may call this resource.
* @throws ScmBadRequestException
* @throws ScmUnauthorizedException
* @throws ScmCommunicationException
*/
List<BitbucketUser> getUsers(String filter)
throws ScmBadRequestException, ScmUnauthorizedException, ScmCommunicationException;
/**
* Modify an access token for the user according to the given request. Any fields not specified
* will not be altered
*
* @param userSlug
* @param tokenId - the token id
* @throws ScmItemNotFoundException
* @throws ScmUnauthorizedException
* @throws ScmCommunicationException
*/
void deletePersonalAccessTokens(String userSlug, Long tokenId)
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException;
/**
* Create an access token for the user according to the given request.
*
* @param userSlug
* @param tokenName
* @param permissions
* @return
* @throws ScmBadRequestException
* @throws ScmUnauthorizedException
* @throws ScmCommunicationException
*/
BitbucketPersonalAccessToken createPersonalAccessTokens(
String userSlug, String tokenName, Set<String> permissions)
throws ScmBadRequestException, ScmUnauthorizedException, ScmCommunicationException;
/**
* Get all access tokens associated with the given user
*
* @param userSlug
* @return
* @throws ScmItemNotFoundException
* @throws ScmUnauthorizedException
* @throws ScmBadRequestException
* @throws ScmCommunicationException
*/
List<BitbucketPersonalAccessToken> getPersonalAccessTokens(String userSlug)
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException;
}

View File

@ -0,0 +1,146 @@
/*
* Copyright (c) 2012-2018 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.bitbucket.server;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.Objects;
@JsonIgnoreProperties(value = "links")
public class BitbucketUser {
private String displayName;
private String name;
private long id;
private String type;
private boolean isActive;
private String slug;
private String emailAddress;
public BitbucketUser(
String displayName,
String name,
long id,
String type,
boolean isActive,
String slug,
String emailAddress) {
this.displayName = displayName;
this.name = name;
this.id = id;
this.type = type;
this.isActive = isActive;
this.slug = slug;
this.emailAddress = emailAddress;
}
public BitbucketUser() {}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public boolean isActive() {
return isActive;
}
public void setActive(boolean active) {
isActive = active;
}
public String getSlug() {
return slug;
}
public void setSlug(String slug) {
this.slug = slug;
}
public String getEmailAddress() {
return emailAddress;
}
public void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BitbucketUser that = (BitbucketUser) o;
return id == that.id
&& isActive == that.isActive
&& Objects.equals(displayName, that.displayName)
&& Objects.equals(name, that.name)
&& Objects.equals(type, that.type)
&& Objects.equals(slug, that.slug)
&& Objects.equals(emailAddress, that.emailAddress);
}
@Override
public int hashCode() {
return Objects.hash(displayName, name, id, type, isActive, slug, emailAddress);
}
@Override
public String toString() {
return "BitbucketUser{"
+ "displayName='"
+ displayName
+ '\''
+ ", name='"
+ name
+ '\''
+ ", id="
+ id
+ ", type='"
+ type
+ '\''
+ ", isActive="
+ isActive
+ ", slug='"
+ slug
+ '\''
+ ", emailAddress='"
+ emailAddress
+ '\''
+ '}';
}
}

View File

@ -0,0 +1,365 @@
/*
* Copyright (c) 2012-2018 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.bitbucket.server;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
import static java.time.Duration.ofSeconds;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import com.google.common.io.CharStreams;
import com.google.common.net.HttpHeaders;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.ws.rs.core.MediaType;
import org.eclipse.che.api.factory.server.scm.exception.ScmBadRequestException;
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
import org.eclipse.che.api.factory.server.scm.exception.ScmItemNotFoundException;
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler;
import org.eclipse.che.commons.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of @{@link BitbucketServerApiClient} that is using @{@link HttpClient} to
* communicate with Bitbucket Server.
*/
public class HttpBitbucketServerApiClient implements BitbucketServerApiClient {
private static final ObjectMapper OM = new ObjectMapper();
private static final Logger LOG = LoggerFactory.getLogger(HttpBitbucketServerApiClient.class);
private static final Duration DEFAULT_HTTP_TIMEOUT = ofSeconds(10);
private final URI serverUri;
private final AuthorizationHeaderSupplier headerProvider;
private final HttpClient httpClient;
public HttpBitbucketServerApiClient(
String serverUrl, AuthorizationHeaderSupplier authorizationHeaderSupplier) {
this.serverUri = URI.create(serverUrl);
this.headerProvider = authorizationHeaderSupplier;
this.httpClient =
HttpClient.newBuilder()
.executor(
Executors.newCachedThreadPool(
new ThreadFactoryBuilder()
.setUncaughtExceptionHandler(LoggingUncaughtExceptionHandler.getInstance())
.setNameFormat(HttpBitbucketServerApiClient.class.getName() + "-%d")
.setDaemon(true)
.build()))
.connectTimeout(DEFAULT_HTTP_TIMEOUT)
.build();
}
@Override
public boolean isConnected(String bitbucketServerUrl) {
return serverUri.equals(URI.create(bitbucketServerUrl));
}
@Override
public BitbucketUser getUser(Subject cheUser)
throws ScmUnauthorizedException, ScmCommunicationException {
try {
// Since Bitbucket server API doesn't provide a way to get an account profile currently
// authenticated user we will try to find it and by iterating over the list available to the
// current user Bitbucket users and attempting to get their personal access tokens. To speed
// up this process first of all we will search among users that contain(somewhere in Bitbucket
// user
// entity) Che's user username. At the second step, we will search against all visible(to the
// current Che's user) bitbucket users that are not included in the first list.
Set<String> usersByName =
getUsers(cheUser.getUserName())
.stream()
.map(BitbucketUser::getSlug)
.collect(Collectors.toSet());
Optional<BitbucketUser> currentUser = findCurrentUser(usersByName);
if (currentUser.isPresent()) {
return currentUser.get();
}
Set<String> usersAllExceptByName =
getUsers()
.stream()
.map(BitbucketUser::getSlug)
.filter(s -> !usersByName.contains(s))
.collect(Collectors.toSet());
currentUser = findCurrentUser(usersAllExceptByName);
if (currentUser.isPresent()) {
return currentUser.get();
}
} catch (ScmBadRequestException | ScmItemNotFoundException scmBadRequestException) {
throw new ScmCommunicationException(
scmBadRequestException.getMessage(), scmBadRequestException);
}
throw new ScmUnauthorizedException(
"Current user not found. That is possible only if user are not authorized against "
+ serverUri);
}
@Override
public BitbucketUser getUser(String slug)
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
URI uri = serverUri.resolve("/rest/api/1.0/users/" + slug);
HttpRequest request =
HttpRequest.newBuilder(uri)
.headers(
"Authorization", headerProvider.computeAuthorizationHeader("GET", uri.toString()))
.timeout(DEFAULT_HTTP_TIMEOUT)
.build();
try {
LOG.trace("executeRequest={}", request);
return executeRequest(
httpClient,
request,
inputStream -> {
try {
return OM.readValue(inputStream, BitbucketUser.class);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
} catch (ScmBadRequestException e) {
throw new ScmCommunicationException(e.getMessage(), e);
}
}
@Override
public List<BitbucketUser> getUsers()
throws ScmBadRequestException, ScmUnauthorizedException, ScmCommunicationException {
try {
return doGetItems(BitbucketUser.class, "/rest/api/1.0/users", null);
} catch (ScmItemNotFoundException e) {
throw new ScmCommunicationException(e.getMessage(), e);
}
}
@Override
public List<BitbucketUser> getUsers(String filter)
throws ScmBadRequestException, ScmUnauthorizedException, ScmCommunicationException {
try {
return doGetItems(BitbucketUser.class, "/rest/api/1.0/users", filter);
} catch (ScmItemNotFoundException e) {
throw new ScmCommunicationException(e.getMessage(), e);
}
}
@Override
public void deletePersonalAccessTokens(String userSlug, Long tokenId)
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
URI uri = serverUri.resolve("/rest/access-tokens/1.0/users/" + userSlug + "/" + tokenId);
HttpRequest request =
HttpRequest.newBuilder(uri)
.DELETE()
.headers(
HttpHeaders.AUTHORIZATION,
headerProvider.computeAuthorizationHeader("DELETE", uri.toString()),
HttpHeaders.ACCEPT,
MediaType.APPLICATION_JSON,
HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_JSON)
.timeout(DEFAULT_HTTP_TIMEOUT)
.build();
try {
LOG.trace("executeRequest={}", request);
executeRequest(
httpClient,
request,
inputStream -> {
try {
return OM.readValue(inputStream, String.class);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
} catch (ScmBadRequestException e) {
throw new ScmCommunicationException(e.getMessage(), e);
}
}
@Override
public BitbucketPersonalAccessToken createPersonalAccessTokens(
String userSlug, String tokenName, Set<String> permissions)
throws ScmBadRequestException, ScmUnauthorizedException, ScmCommunicationException {
URI uri = serverUri.resolve("/rest/access-tokens/1.0/users/" + userSlug);
try {
HttpRequest request =
HttpRequest.newBuilder(uri)
.PUT(
HttpRequest.BodyPublishers.ofString(
OM.writeValueAsString(
new BitbucketPersonalAccessToken(tokenName, permissions))))
.headers(
HttpHeaders.AUTHORIZATION,
headerProvider.computeAuthorizationHeader("PUT", uri.toString()),
HttpHeaders.ACCEPT,
MediaType.APPLICATION_JSON,
HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_JSON)
.timeout(DEFAULT_HTTP_TIMEOUT)
.build();
LOG.trace("executeRequest={}", request);
return executeRequest(
httpClient,
request,
inputStream -> {
try {
return OM.readValue(inputStream, BitbucketPersonalAccessToken.class);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
} catch (ScmItemNotFoundException | JsonProcessingException e) {
throw new ScmCommunicationException(e.getMessage(), e);
}
}
@Override
public List<BitbucketPersonalAccessToken> getPersonalAccessTokens(String userSlug)
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
try {
return doGetItems(
BitbucketPersonalAccessToken.class, "/rest/access-tokens/1.0/users/" + userSlug, null);
} catch (ScmBadRequestException e) {
throw new ScmCommunicationException(e.getMessage(), e);
}
}
/**
* This method is testing provided collection of user's `slug`s if contains the `slug` of the
* currently authenticated user and return it. The major method to test that condition is to get
* the list of personal access tokens. Current Che user that is associated with Bitbucket user
* should not be able to get someone else list of personal access tokens except his own.
*
* @param userSlugs set of user's `slug`s to test if it contains currently authenticated user.
* @return Bitbucket user from the given set that is associated with the current user. Or
* Optional.empty if the given set doesn't contain that user.
* @throws ScmCommunicationException can happen if communication between che server and bitbucket
* server is failed.
* @throws ScmUnauthorizedException can happen if currently authenticated che user is not
* associated with bitbucket server.
* @throws ScmItemNotFoundException can happen if provided `slug` to test is not associated with
* any user on Bitbucket server
*/
private Optional<BitbucketUser> findCurrentUser(Set<String> userSlugs)
throws ScmCommunicationException, ScmUnauthorizedException, ScmItemNotFoundException {
for (String userSlug : userSlugs) {
BitbucketUser user = getUser(userSlug);
try {
getPersonalAccessTokens(userSlug);
return Optional.of(user);
} catch (ScmItemNotFoundException | ScmUnauthorizedException e) {
// ok
}
}
return Optional.empty();
}
private <T> List<T> doGetItems(Class<T> tClass, String api, String filter)
throws ScmUnauthorizedException, ScmCommunicationException, ScmBadRequestException,
ScmItemNotFoundException {
List<T> result = new ArrayList<>();
Page<T> currentPage = doGetPage(tClass, api, 0, 25, filter);
result.addAll(currentPage.getValues());
while (!currentPage.isLastPage()) {
currentPage = doGetPage(tClass, api, currentPage.getNextPageStart(), 25, filter);
result.addAll(currentPage.getValues());
}
return result;
}
private <T> Page<T> doGetPage(Class<T> tClass, String api, int start, int limit, String filter)
throws ScmUnauthorizedException, ScmBadRequestException, ScmCommunicationException,
ScmItemNotFoundException {
String suffix = api + "?start=" + start + "&limit=" + limit;
if (!Strings.isNullOrEmpty(filter)) {
suffix += "&filter=" + filter;
}
URI uri = serverUri.resolve(suffix);
HttpRequest request =
HttpRequest.newBuilder(uri)
.headers(
"Authorization", headerProvider.computeAuthorizationHeader("GET", uri.toString()))
.timeout(DEFAULT_HTTP_TIMEOUT)
.build();
LOG.trace("executeRequest={}", request);
final JavaType typeReference =
TypeFactory.defaultInstance().constructParametricType(Page.class, tClass);
return executeRequest(
httpClient,
request,
inputStream -> {
try {
return OM.readValue(inputStream, typeReference);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
private <T> T executeRequest(
HttpClient httpClient, HttpRequest request, Function<InputStream, T> bodyConverter)
throws ScmBadRequestException, ScmItemNotFoundException, ScmCommunicationException,
ScmUnauthorizedException {
try {
HttpResponse<InputStream> response =
httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
LOG.trace("executeRequest={} response {}", request, response.statusCode());
if (response.statusCode() == 200) {
return bodyConverter.apply(response.body());
} else if (response.statusCode() == 204) {
return null;
} else {
String body = CharStreams.toString(new InputStreamReader(response.body(), Charsets.UTF_8));
switch (response.statusCode()) {
case HTTP_BAD_REQUEST:
throw new ScmBadRequestException(body);
case HTTP_UNAUTHORIZED:
throw new ScmUnauthorizedException(body);
case HTTP_NOT_FOUND:
throw new ScmItemNotFoundException(body);
default:
throw new ScmCommunicationException(
"Unexpected status code " + response.statusCode() + " " + response.toString());
}
}
} catch (IOException | InterruptedException | UncheckedIOException e) {
throw new ScmCommunicationException(e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,79 @@
/*
* Copyright (c) 2012-2018 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.bitbucket.server;
import java.util.List;
import java.util.Set;
import org.eclipse.che.api.factory.server.scm.exception.ScmBadRequestException;
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
import org.eclipse.che.api.factory.server.scm.exception.ScmItemNotFoundException;
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
import org.eclipse.che.commons.subject.Subject;
/**
* Implementation of @{@link BitbucketServerApiClient} that is going to be deployed in container in
* case if no integration with Bitbucket server is needed.
*/
public class NoopBitbucketServerApiClient implements BitbucketServerApiClient {
@Override
public boolean isConnected(String bitbucketServerUrl) {
return false;
}
@Override
public BitbucketUser getUser(Subject cheUser)
throws ScmUnauthorizedException, ScmCommunicationException {
throw new RuntimeException(
"The fallback noop api client cannot be used for real operation. Make sure Bitbucket OAuth1 is properly configured.");
}
@Override
public BitbucketUser getUser(String slug)
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.");
}
@Override
public List<BitbucketUser> getUsers()
throws ScmBadRequestException, ScmUnauthorizedException, ScmCommunicationException {
throw new RuntimeException(
"The fallback noop api client cannot be used for real operation. Make sure Bitbucket OAuth1 is properly configured.");
}
@Override
public List<BitbucketUser> getUsers(String filter)
throws ScmBadRequestException, ScmUnauthorizedException, ScmCommunicationException {
throw new RuntimeException(
"The fallback noop api client cannot be used for real operation. Make sure Bitbucket OAuth1 is properly configured.");
}
@Override
public void deletePersonalAccessTokens(String userSlug, Long tokenId)
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.");
}
@Override
public BitbucketPersonalAccessToken createPersonalAccessTokens(
String userSlug, String tokenName, Set<String> permissions)
throws ScmBadRequestException, ScmUnauthorizedException, ScmCommunicationException {
throw new RuntimeException("Invalid usage of BitbucketServerApi");
}
@Override
public List<BitbucketPersonalAccessToken> getPersonalAccessTokens(String userSlug)
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
throw new RuntimeException("Invalid usage of BitbucketServerApi");
}
}

View File

@ -0,0 +1,121 @@
/*
* Copyright (c) 2012-2018 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.bitbucket.server;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Objects;
/**
* Bitbucket's paging object. Combines collections of items with some metadata.
*
* <p>See more
*
* <p>https://docs.atlassian.com/bitbucket-server/rest/5.6.1/bitbucket-rest.html
*
* @param <T>
*/
public class Page<T> {
private int start;
private int size;
private int limit;
@JsonProperty(value = "isLastPage")
private boolean isLastPage;
private int nextPageStart;
List<T> values;
public int getStart() {
return start;
}
public void setStart(int start) {
this.start = start;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public int getLimit() {
return limit;
}
public void setLimit(int limit) {
this.limit = limit;
}
public boolean isLastPage() {
return isLastPage;
}
public void setLastPage(boolean lastPage) {
isLastPage = lastPage;
}
public List<T> getValues() {
return values;
}
public void setValues(List<T> values) {
this.values = values;
}
public int getNextPageStart() {
return nextPageStart;
}
public void setNextPageStart(int nextPageStart) {
this.nextPageStart = nextPageStart;
}
@Override
public String toString() {
return "Page{"
+ "start="
+ start
+ ", size="
+ size
+ ", limit="
+ limit
+ ", isLastPage="
+ isLastPage
+ ", nextPageStart="
+ nextPageStart
+ ", values="
+ values
+ '}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Page<?> page = (Page<?>) o;
return start == page.start
&& size == page.size
&& limit == page.limit
&& isLastPage == page.isLastPage
&& nextPageStart == page.nextPageStart
&& Objects.equals(values, page.values);
}
@Override
public int hashCode() {
return Objects.hash(start, size, limit, isLastPage, nextPageStart, values);
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright (c) 2012-2018 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.oauth1;
import static com.google.common.base.Strings.isNullOrEmpty;
import java.util.Optional;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.eclipse.che.api.factory.server.bitbucket.server.BitbucketServerApiClient;
import org.eclipse.che.api.factory.server.bitbucket.server.HttpBitbucketServerApiClient;
import org.eclipse.che.api.factory.server.bitbucket.server.NoopBitbucketServerApiClient;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.inject.ConfigurationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class BitbucketServerApiProvider implements Provider<BitbucketServerApiClient> {
private static final Logger LOG = LoggerFactory.getLogger(BitbucketServerApiProvider.class);
private final BitbucketServerApiClient bitbucketServerApiClient;
@Inject
public BitbucketServerApiProvider(
@Nullable @Named("che.integration.bitbucket.server_endpoints") String bitbucketEndpoints,
@Nullable @Named("che.oauth1.bitbucket.endpoint") String bitbucketOauth1Endpoint,
Set<OAuthAuthenticator> authenticators) {
bitbucketServerApiClient = doGet(bitbucketEndpoints, bitbucketOauth1Endpoint, authenticators);
LOG.debug("Bitbucket server api is used {}", bitbucketServerApiClient);
}
@Override
public BitbucketServerApiClient get() {
return bitbucketServerApiClient;
}
private static BitbucketServerApiClient doGet(
String bitbucketEndpoints,
String bitbucketOauth1Endpoint,
Set<OAuthAuthenticator> authenticators) {
if (isNullOrEmpty(bitbucketOauth1Endpoint)) {
return new NoopBitbucketServerApiClient();
} else {
if (isNullOrEmpty(bitbucketEndpoints)) {
throw new ConfigurationException(
"`che.integration.bitbucket.server_endpoints` bitbucket configuration is missing."
+ " It should contain values from 'che.oauth1.bitbucket.endpoint'");
} else {
if (bitbucketEndpoints.contains(bitbucketOauth1Endpoint)) {
Optional<OAuthAuthenticator> authenticator =
authenticators
.stream()
.filter(
a ->
a.getOAuthProvider()
.equals(BitbucketServerOAuthAuthenticator.AUTHENTICATOR_NAME))
.filter(
a -> BitbucketServerOAuthAuthenticator.class.isAssignableFrom(a.getClass()))
.findFirst();
if (authenticator.isEmpty()) {
throw new ConfigurationException(
"'che.oauth1.bitbucket.endpoint' is set but BitbucketServerOAuthAuthenticator is not deployed correctly");
}
return new HttpBitbucketServerApiClient(
bitbucketOauth1Endpoint,
new BitbucketServerOAuth1AuthorizationHeaderSupplier(
(BitbucketServerOAuthAuthenticator) authenticator.get()));
} else {
throw new ConfigurationException(
"`che.integration.bitbucket.server_endpoints` must contain `"
+ bitbucketOauth1Endpoint
+ "` value");
}
}
}
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright (c) 2012-2018 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.oauth1;
import com.google.common.base.Strings;
import javax.inject.Inject;
import org.eclipse.che.api.factory.server.bitbucket.server.AuthorizationHeaderSupplier;
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.subject.Subject;
/**
* Implementation of @{@link AuthorizationHeaderSupplier} that is used @{@link
* BitbucketServerOAuthAuthenticator} to compute authorization headers.
*/
public class BitbucketServerOAuth1AuthorizationHeaderSupplier
implements AuthorizationHeaderSupplier {
private final BitbucketServerOAuthAuthenticator authenticator;
@Inject
public BitbucketServerOAuth1AuthorizationHeaderSupplier(
BitbucketServerOAuthAuthenticator authenticator) {
this.authenticator = authenticator;
}
@Override
public String computeAuthorizationHeader(String requestMethod, String requestUrl)
throws ScmUnauthorizedException, ScmCommunicationException {
try {
Subject subject = EnvironmentContext.getCurrent().getSubject();
String authorizationHeader =
authenticator.computeAuthorizationHeader(subject.getUserId(), requestMethod, requestUrl);
if (Strings.isNullOrEmpty(authorizationHeader)) {
throw new ScmUnauthorizedException(
subject.getUserName()
+ " is not authorized in "
+ authenticator.getOAuthProvider()
+ " OAuth1 provider");
}
return authorizationHeader;
} catch (OAuthAuthenticationException e) {
throw new ScmCommunicationException(e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,193 @@
/*
* Copyright (c) 2012-2018 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.bitbucket;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
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.scm.PersonalAccessToken;
import org.eclipse.che.api.factory.server.scm.exception.ScmBadRequestException;
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
import org.eclipse.che.api.factory.server.scm.exception.ScmItemNotFoundException;
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.subject.Subject;
import org.eclipse.che.commons.subject.SubjectImpl;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(MockitoTestNGListener.class)
public class BitbucketServerPersonalAccessTokenFetcherTest {
String someNotBitbucketURL = "https://notabitbucket.com";
String someBitbucketURL = "https://some.bitbucketserver.com";
Subject subject;
@Mock BitbucketServerApiClient bitbucketServerApiClient;
BitbucketUser bitbucketUser;
BitbucketServerPersonalAccessTokenFetcher fetcher;
BitbucketPersonalAccessToken bitbucketPersonalAccessToken;
BitbucketPersonalAccessToken bitbucketPersonalAccessToken2;
BitbucketPersonalAccessToken bitbucketPersonalAccessToken3;
@BeforeMethod
public void setup() throws MalformedURLException {
URL apiEndpoint = new URL("https://che.server.com");
subject = new SubjectImpl("another_user", "user987", "token111", false);
bitbucketUser =
new BitbucketUser("User", "user", 32423523, "NORMAL", true, "user", "user@users.com");
bitbucketPersonalAccessToken =
new BitbucketPersonalAccessToken(
234234,
234345345,
23534534,
"che-token-<user987>-<che.server.com>",
"2340590skdf3<0>945i0923i4jasoidfj934ui50",
bitbucketUser,
ImmutableSet.of("PROJECT_WRITE", "REPO_WRITE"));
bitbucketPersonalAccessToken2 =
new BitbucketPersonalAccessToken(
3647456,
234345345,
23534534,
"che-token-<user987>-<che.server.com>",
"34545<0>945i0923i4jasoidfj934ui50",
bitbucketUser,
ImmutableSet.of("REPO_READ"));
bitbucketPersonalAccessToken3 =
new BitbucketPersonalAccessToken(
132423,
234345345,
23534534,
"che-token-<user987>-<che.server.com>",
"3456\\<0>945//i0923i4jasoidfj934ui50",
bitbucketUser,
ImmutableSet.of("PROJECT_READ", "REPO_READ"));
fetcher = new BitbucketServerPersonalAccessTokenFetcher(bitbucketServerApiClient, apiEndpoint);
EnvironmentContext context = new EnvironmentContext();
context.setSubject(subject);
EnvironmentContext.setCurrent(context);
}
@Test
public void shouldSkipToFetchUnknownUrls()
throws ScmUnauthorizedException, ScmCommunicationException {
// given
when(bitbucketServerApiClient.isConnected(eq(someNotBitbucketURL))).thenReturn(false);
// when
PersonalAccessToken result = fetcher.fetchPersonalAccessToken(subject, someNotBitbucketURL);
// then
assertNull(result);
}
@Test(
dataProvider = "expectedExceptions",
expectedExceptions = {ScmUnauthorizedException.class, ScmCommunicationException.class})
public void shouldRethrowBasicExceptionsOnGetUserStep(Class<? extends Throwable> exception)
throws ScmUnauthorizedException, ScmCommunicationException {
// given
when(bitbucketServerApiClient.isConnected(eq(someNotBitbucketURL))).thenReturn(true);
doThrow(exception).when(bitbucketServerApiClient).getUser(eq(subject));
// when
fetcher.fetchPersonalAccessToken(subject, someNotBitbucketURL);
}
@Test
public void shouldBeAbleToFetchPersonalAccessToken()
throws ScmUnauthorizedException, ScmCommunicationException, ScmItemNotFoundException,
ScmBadRequestException {
// given
when(bitbucketServerApiClient.isConnected(eq(someBitbucketURL))).thenReturn(true);
when(bitbucketServerApiClient.getUser(eq(subject))).thenReturn(bitbucketUser);
when(bitbucketServerApiClient.getPersonalAccessTokens(eq(bitbucketUser.getSlug())))
.thenReturn(Collections.emptyList());
when(bitbucketServerApiClient.createPersonalAccessTokens(
eq(bitbucketUser.getSlug()),
eq("che-token-<user987>-<che.server.com>"),
eq(ImmutableSet.of("PROJECT_WRITE", "REPO_WRITE"))))
.thenReturn(bitbucketPersonalAccessToken);
// when
PersonalAccessToken result = fetcher.fetchPersonalAccessToken(subject, someBitbucketURL);
// then
assertNotNull(result);
}
@Test
public void shouldDeleteExistedCheTokenBeforeCreatingNew()
throws ScmUnauthorizedException, ScmCommunicationException, ScmItemNotFoundException,
ScmBadRequestException {
when(bitbucketServerApiClient.isConnected(eq(someBitbucketURL))).thenReturn(true);
when(bitbucketServerApiClient.getUser(eq(subject))).thenReturn(bitbucketUser);
when(bitbucketServerApiClient.getPersonalAccessTokens(eq(bitbucketUser.getSlug())))
.thenReturn(ImmutableList.of(bitbucketPersonalAccessToken, bitbucketPersonalAccessToken2));
when(bitbucketServerApiClient.createPersonalAccessTokens(
eq(bitbucketUser.getSlug()),
eq("che-token-<user987>-<che.server.com>"),
eq(ImmutableSet.of("PROJECT_WRITE", "REPO_WRITE"))))
.thenReturn(bitbucketPersonalAccessToken3);
// when
PersonalAccessToken result = fetcher.fetchPersonalAccessToken(subject, someBitbucketURL);
// then
assertNotNull(result);
verify(bitbucketServerApiClient)
.deletePersonalAccessTokens(
eq(bitbucketUser.getSlug()), eq(bitbucketPersonalAccessToken.getId()));
verify(bitbucketServerApiClient)
.deletePersonalAccessTokens(
eq(bitbucketUser.getSlug()), eq(bitbucketPersonalAccessToken2.getId()));
}
@Test(expectedExceptions = {ScmCommunicationException.class})
public void shouldRethrowUnExceptionsOnCreatePersonalAccessTokens()
throws ScmUnauthorizedException, ScmCommunicationException, ScmItemNotFoundException,
ScmBadRequestException {
// given
when(bitbucketServerApiClient.isConnected(eq(someBitbucketURL))).thenReturn(true);
when(bitbucketServerApiClient.getUser(eq(subject))).thenReturn(bitbucketUser);
when(bitbucketServerApiClient.getPersonalAccessTokens(eq(bitbucketUser.getSlug())))
.thenReturn(Collections.emptyList());
doThrow(ScmBadRequestException.class)
.when(bitbucketServerApiClient)
.createPersonalAccessTokens(
eq(bitbucketUser.getSlug()),
eq("che-token-<user987>-<che.server.com>"),
eq(ImmutableSet.of("PROJECT_WRITE", "REPO_WRITE")));
// when
fetcher.fetchPersonalAccessToken(subject, someBitbucketURL);
}
@DataProvider
public static Object[][] expectedExceptions() {
return new Object[][] {{ScmUnauthorizedException.class}, {ScmCommunicationException.class}};
}
@DataProvider
public static Object[][] unExpectedExceptions() {
return new Object[][] {{ScmBadRequestException.class}, {ScmItemNotFoundException.class}};
}
}

View File

@ -0,0 +1,222 @@
/*
* Copyright (c) 2012-2018 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.bitbucket;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.ok;
import static com.github.tomakehurst.wiremock.client.WireMock.put;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.common.Slf4jNotifier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.HttpHeaders;
import java.util.List;
import java.util.stream.Collectors;
import javax.ws.rs.core.MediaType;
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.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.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class HttpBitbucketServerApiClientTest {
private final String AUTHORIZATION_TOKEN =
"OAuth oauth_consumer_key=\"key123321\", oauth_nonce=\"6c0eace252f8dcda\","
+ " oauth_signature=\"dPCm521TAF56FfGxabBAZDs9YTNeCg%2BiRK49afoJve8Mxk5ILlfkZKH693udqOig5k5ydeVxX%2FTso%2Flxx1pv2bqdbCqj3Nq82do1hJN5eTDLSvbHfGvjFuOGRobHTHwP6oJkaBSafjMUY8i8Vnz6hLfxToPj2ktd6ug4nKc1WGg%3D\", "
+ "oauth_signature_method=\"RSA-SHA1\", oauth_timestamp=\"1609250025\", "
+ "oauth_token=\"JmpyDe9sgYNn6pYHP6eGLaIU0vxdKLCJ\", oauth_version=\"1.0\"";
WireMockServer wireMockServer;
WireMock wireMock;
BitbucketServerApiClient bitbucketServer;
@BeforeMethod
void start() {
int httpPort = getHttpPort();
wireMockServer =
new WireMockServer(wireMockConfig().notifier(new Slf4jNotifier(false)).port(httpPort));
wireMockServer.start();
WireMock.configureFor("localhost", httpPort);
wireMock = new WireMock("localhost", httpPort);
bitbucketServer =
new HttpBitbucketServerApiClient(
wireMockServer.url("/"), (requestMethod, requestUrl) -> AUTHORIZATION_TOKEN);
}
@AfterMethod
void stop() {
wireMockServer.stop();
}
int getHttpPort() {
return 3301;
}
@Test
public void testGetUser()
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
stubFor(
get(urlEqualTo("/rest/api/1.0/users/ksmster"))
.withHeader(HttpHeaders.AUTHORIZATION, equalTo(AUTHORIZATION_TOKEN))
.willReturn(
aResponse()
.withHeader("Content-Type", "application/json; charset=utf-8")
.withBodyFile("bitbucket/rest/api/1.0/users/ksmster/response.json")));
BitbucketUser user = bitbucketServer.getUser("ksmster");
assertNotNull(user);
}
@Test
public void testGetUsers()
throws ScmCommunicationException, ScmBadRequestException, ScmUnauthorizedException {
stubFor(
get(urlPathEqualTo("/rest/api/1.0/users"))
.withHeader(HttpHeaders.AUTHORIZATION, equalTo(AUTHORIZATION_TOKEN))
.withQueryParam("start", equalTo("0"))
.withQueryParam("limit", equalTo("25"))
.willReturn(
aResponse()
.withHeader("Content-Type", "application/json; charset=utf-8")
.withBodyFile("bitbucket/rest/api/1.0/users/response_s0_l25.json")));
stubFor(
get(urlPathEqualTo("/rest/api/1.0/users"))
.withHeader(HttpHeaders.AUTHORIZATION, equalTo(AUTHORIZATION_TOKEN))
.withQueryParam("start", equalTo("3"))
.withQueryParam("limit", equalTo("25"))
.willReturn(
aResponse()
.withHeader("Content-Type", "application/json; charset=utf-8")
.withBodyFile("bitbucket/rest/api/1.0/users/response_s3_l25.json")));
stubFor(
get(urlPathEqualTo("/rest/api/1.0/users"))
.withHeader(HttpHeaders.AUTHORIZATION, equalTo(AUTHORIZATION_TOKEN))
.withQueryParam("start", equalTo("6"))
.withQueryParam("limit", equalTo("25"))
.willReturn(
aResponse()
.withHeader("Content-Type", "application/json; charset=utf-8")
.withBodyFile("bitbucket/rest/api/1.0/users/response_s6_l25.json")));
stubFor(
get(urlPathEqualTo("/rest/api/1.0/users"))
.withHeader(HttpHeaders.AUTHORIZATION, equalTo(AUTHORIZATION_TOKEN))
.withQueryParam("start", equalTo("9"))
.withQueryParam("limit", equalTo("25"))
.willReturn(
aResponse()
.withHeader("Content-Type", "application/json; charset=utf-8")
.withBodyFile("bitbucket/rest/api/1.0/users/response_s9_l25.json")));
List<String> page =
bitbucketServer
.getUsers()
.stream()
.map(BitbucketUser::getSlug)
.collect(Collectors.toList());
assertEquals(
page,
ImmutableList.of(
"admin",
"ksmster",
"skabashn",
"user1",
"user2",
"user3",
"user4",
"user5",
"user6",
"user7"));
}
@Test
public void testGetUsersFiltered()
throws ScmCommunicationException, ScmBadRequestException, ScmUnauthorizedException {
stubFor(
get(urlPathEqualTo("/rest/api/1.0/users"))
.withHeader(HttpHeaders.AUTHORIZATION, equalTo(AUTHORIZATION_TOKEN))
.withQueryParam("start", equalTo("0"))
.withQueryParam("limit", equalTo("25"))
.willReturn(
aResponse()
.withHeader("Content-Type", "application/json; charset=utf-8")
.withBodyFile("bitbucket/rest/api/1.0/users/filtered/response.json")));
List<String> page =
bitbucketServer
.getUsers("ksmster")
.stream()
.map(BitbucketUser::getSlug)
.collect(Collectors.toList());
assertEquals(page, ImmutableList.of("admin", "ksmster"));
}
@Test
public void testGetPersonalAccessTokens()
throws ScmCommunicationException, ScmBadRequestException, ScmItemNotFoundException,
ScmUnauthorizedException {
stubFor(
get(urlPathEqualTo("/rest/access-tokens/1.0/users/ksmster"))
.withHeader(HttpHeaders.AUTHORIZATION, equalTo(AUTHORIZATION_TOKEN))
.withQueryParam("start", equalTo("0"))
.withQueryParam("limit", equalTo("25"))
.willReturn(
aResponse()
.withHeader("Content-Type", "application/json; charset=utf-8")
.withBodyFile("bitbucket/rest/access-tokens/1.0/users/ksmster/response.json")));
List<String> page =
bitbucketServer
.getPersonalAccessTokens("ksmster")
.stream()
.map(BitbucketPersonalAccessToken::getName)
.collect(Collectors.toList());
assertEquals(page, ImmutableList.of("che", "t2"));
}
@Test
public void shouldBeAbleToCreatePAT()
throws ScmCommunicationException, ScmBadRequestException, ScmUnauthorizedException {
// given
stubFor(
put(urlPathEqualTo("/rest/access-tokens/1.0/users/ksmster"))
.withHeader(HttpHeaders.AUTHORIZATION, equalTo(AUTHORIZATION_TOKEN))
.withHeader(HttpHeaders.ACCEPT, equalTo(MediaType.APPLICATION_JSON))
.withHeader(HttpHeaders.CONTENT_TYPE, equalTo(MediaType.APPLICATION_JSON))
.withHeader(HttpHeaders.CONTENT_LENGTH, equalTo("63"))
.willReturn(
ok().withBodyFile("bitbucket/rest/access-tokens/1.0/users/ksmster/newtoken.json")));
// when
BitbucketPersonalAccessToken result =
bitbucketServer.createPersonalAccessTokens(
"ksmster", "myToKen", ImmutableSet.of("PROJECT_WRITE", "REPO_WRITE"));
// then
assertNotNull(result);
assertEquals(result.getToken(), "MTU4OTEwNTMyOTA5Ohc88HcY8k7gWOzl2mP5TtdtY5Qs");
}
}

View File

@ -0,0 +1,123 @@
/*
* Copyright (c) 2012-2018 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.oauth1;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import org.eclipse.che.api.factory.server.bitbucket.server.BitbucketServerApiClient;
import org.eclipse.che.api.factory.server.bitbucket.server.HttpBitbucketServerApiClient;
import org.eclipse.che.api.factory.server.bitbucket.server.NoopBitbucketServerApiClient;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.inject.ConfigurationException;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
public class BitbucketServerApiClientProviderTest {
BitbucketServerOAuthAuthenticator oAuthAuthenticator;
@BeforeClass
public void setUp() {
oAuthAuthenticator =
new BitbucketServerOAuthAuthenticator(
"df", "private", " https://bitbucket2.server.com", " https://che.server.com");
}
@Test
public void shouldBeAbleToCreateBitbucketServerApi() {
// given
BitbucketServerApiProvider bitbucketServerApiProvider =
new BitbucketServerApiProvider(
"https://bitbucket.server.com, https://bitbucket2.server.com",
"https://bitbucket.server.com",
ImmutableSet.of(oAuthAuthenticator));
// when
BitbucketServerApiClient actual = bitbucketServerApiProvider.get();
// then
assertNotNull(actual);
assertTrue(HttpBitbucketServerApiClient.class.isAssignableFrom(actual.getClass()));
}
@Test(dataProvider = "noopConfig")
public void shouldProvideNoopOAuthAuthenticatorIfSomeConfigurationIsNotSet(
@Nullable String bitbucketEndpoints,
@Nullable String bitbucketOauth1Endpoint,
Set<OAuthAuthenticator> authenticators)
throws IOException {
// given
BitbucketServerApiProvider bitbucketServerApiProvider =
new BitbucketServerApiProvider(bitbucketEndpoints, bitbucketOauth1Endpoint, authenticators);
// when
BitbucketServerApiClient actual = bitbucketServerApiProvider.get();
// then
assertNotNull(actual);
assertTrue(NoopBitbucketServerApiClient.class.isAssignableFrom(actual.getClass()));
}
@Test(
expectedExceptions = ConfigurationException.class,
expectedExceptionsMessageRegExp =
"`che.integration.bitbucket.server_endpoints` bitbucket configuration is missing. It should contain values from 'che.oauth1.bitbucket.endpoint'")
public void shouldFailToBuildIfEndpointsAreMisconfigured() {
// given
// when
BitbucketServerApiProvider bitbucketServerApiProvider =
new BitbucketServerApiProvider(
"", "https://bitbucket.server.com", ImmutableSet.of(oAuthAuthenticator));
}
@Test(
expectedExceptions = ConfigurationException.class,
expectedExceptionsMessageRegExp =
"'che.oauth1.bitbucket.endpoint' is set but BitbucketServerOAuthAuthenticator is not deployed correctly")
public void shouldFailToBuildIfEndpointsAreMisconfigured2() {
// given
// when
BitbucketServerApiProvider bitbucketServerApiProvider =
new BitbucketServerApiProvider(
"https://bitbucket.server.com, https://bitbucket2.server.com",
"https://bitbucket.server.com",
Collections.emptySet());
}
@Test(
expectedExceptions = ConfigurationException.class,
expectedExceptionsMessageRegExp =
"`che.integration.bitbucket.server_endpoints` must contain `https://bitbucket.server.com` value")
public void shouldFailToBuildIfEndpointsAreMisconfigured3() {
// given
// when
BitbucketServerApiProvider bitbucketServerApiProvider =
new BitbucketServerApiProvider(
"https://bitbucket3.server.com, https://bitbucket2.server.com",
"https://bitbucket.server.com",
ImmutableSet.of(oAuthAuthenticator));
}
@DataProvider(name = "noopConfig")
public Object[][] noopConfig() {
return new Object[][] {
{null, null, null},
{"https://bitbucket.server.com, https://bitbucket2.server.com", null, null},
{
"https://bitbucket.server.com, https://bitbucket2.server.com",
null,
ImmutableSet.of(oAuthAuthenticator)
}
};
}
}

View File

@ -0,0 +1,95 @@
/*
* Copyright (c) 2012-2018 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.oauth1;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.subject.Subject;
import org.eclipse.che.commons.subject.SubjectImpl;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(MockitoTestNGListener.class)
public class BitbucketServerOAuth1AuthorizationHeaderSupplierTest {
@Mock BitbucketServerOAuthAuthenticator authenticator;
@InjectMocks BitbucketServerOAuth1AuthorizationHeaderSupplier supplier;
Subject subject = new SubjectImpl("user", "234234", "t234234", false);
@BeforeMethod
public void setUp() {
EnvironmentContext.getCurrent().setSubject(subject);
}
@Test
public void shouldBeAbleToComputeAuthorizationHeader()
throws ScmUnauthorizedException, OAuthAuthenticationException, ScmCommunicationException {
// given
when(authenticator.computeAuthorizationHeader(
eq(subject.getUserId()), eq("POST"), eq("/api/user")))
.thenReturn("signature");
// when
String actual = supplier.computeAuthorizationHeader("POST", "/api/user");
// then
assertEquals(actual, "signature");
}
@Test(
expectedExceptions = ScmUnauthorizedException.class,
expectedExceptionsMessageRegExp =
"user is not authorized in bitbucket-server OAuth1 provider")
public void shouldThrowScmUnauthorizedExceptionIfHeaderIsNull()
throws OAuthAuthenticationException, ScmUnauthorizedException, ScmCommunicationException {
// given
when(authenticator.computeAuthorizationHeader(
eq(subject.getUserId()), eq("POST"), eq("/api/user")))
.thenReturn(null);
// when
supplier.computeAuthorizationHeader("POST", "/api/user");
}
@Test(
expectedExceptions = ScmUnauthorizedException.class,
expectedExceptionsMessageRegExp =
"user is not authorized in bitbucket-server OAuth1 provider")
public void shouldThrowScmUnauthorizedExceptionIfHeaderIsEmpty()
throws OAuthAuthenticationException, ScmUnauthorizedException, ScmCommunicationException {
// given
when(authenticator.computeAuthorizationHeader(
eq(subject.getUserId()), eq("POST"), eq("/api/user")))
.thenReturn("");
// when
supplier.computeAuthorizationHeader("POST", "/api/user");
}
@Test(
expectedExceptions = ScmCommunicationException.class,
expectedExceptionsMessageRegExp = "this is a message")
public void shouldThrowScmUnauthorizedExceptionOnOAuthAuthenticationException()
throws OAuthAuthenticationException, ScmUnauthorizedException, ScmCommunicationException {
// given
when(authenticator.computeAuthorizationHeader(
eq(subject.getUserId()), eq("POST"), eq("/api/user")))
.thenThrow(new OAuthAuthenticationException("this is a message"));
// when
supplier.computeAuthorizationHeader("POST", "/api/user");
}
}

View File

@ -0,0 +1,26 @@
{
"id": "158910532909",
"createdDate": 1609249808751,
"name": "che5",
"permissions": [
"PROJECT_WRITE",
"REPO_WRITE"
],
"user": {
"name": "ksmster",
"emailAddress": "ksmster@gmail.com",
"id": 2,
"displayName": "ksmster",
"active": true,
"slug": "ksmster",
"type": "NORMAL",
"links": {
"self": [
{
"href": "https://bitbucket-bitbucket.apps.cluster-devtools-c5ac.devtools-c5ac.example.opentlc.com/users/ksmster"
}
]
}
},
"token": "MTU4OTEwNTMyOTA5Ohc88HcY8k7gWOzl2mP5TtdtY5Qs"
}

View File

@ -0,0 +1,58 @@
{
"size": 2,
"limit": 25,
"isLastPage": true,
"values": [
{
"id": "898123953680",
"createdDate": 1609227270831,
"name": "che",
"permissions": [
"PROJECT_WRITE",
"REPO_WRITE"
],
"user": {
"name": "ksmster",
"emailAddress": "ksmster@gmail.com",
"id": 2,
"displayName": "ksmster",
"active": true,
"slug": "ksmster",
"type": "NORMAL",
"links": {
"self": [
{
"href": "https://bitbucket-bitbucket.apps.cluster-devtools-c5ac.devtools-c5ac.example.opentlc.com/users/ksmster"
}
]
}
}
},
{
"id": "080920112506",
"createdDate": 1609227263410,
"name": "t2",
"permissions": [
"REPO_ADMIN",
"PROJECT_WRITE"
],
"user": {
"name": "ksmster",
"emailAddress": "ksmster@gmail.com",
"id": 2,
"displayName": "ksmster",
"active": true,
"slug": "ksmster",
"type": "NORMAL",
"links": {
"self": [
{
"href": "https://bitbucket-bitbucket.apps.cluster-devtools-c5ac.devtools-c5ac.example.opentlc.com/users/ksmster"
}
]
}
}
}
],
"start": 0
}

View File

@ -0,0 +1,40 @@
{
"size": 2,
"limit": 25,
"isLastPage": true,
"values": [
{
"name": "admin",
"emailAddress": "admin@ksmster.com",
"id": 1,
"displayName": "Admin",
"active": true,
"slug": "admin",
"type": "NORMAL",
"links": {
"self": [
{
"href": "https://bitbucket-bitbucket.apps.cluster-devtools-c5ac.devtools-c5ac.example.opentlc.com/users/admin"
}
]
}
},
{
"name": "ksmster",
"emailAddress": "ksmster@gmail.com",
"id": 2,
"displayName": "ksmster",
"active": true,
"slug": "ksmster",
"type": "NORMAL",
"links": {
"self": [
{
"href": "https://bitbucket-bitbucket.apps.cluster-devtools-c5ac.devtools-c5ac.example.opentlc.com/users/ksmster"
}
]
}
}
],
"start": 0
}

View File

@ -0,0 +1,16 @@
{
"name": "ksmster",
"emailAddress": "ksmster@gmail.com",
"id": 2,
"displayName": "Sergii Kabashniuk",
"active": true,
"slug": "ksmster",
"type": "NORMAL",
"links": {
"self": [
{
"href": "https://bitbucket-bitbucket.apps.cluster-devtools-c9ac.devtools-c9ac.example.opentlc.com/users/ksmster"
}
]
}
}

View File

@ -0,0 +1,56 @@
{
"size": 3,
"limit": 25,
"isLastPage": true,
"values": [
{
"name": "admin",
"emailAddress": "admin@ksmster.com",
"id": 1,
"displayName": "Admin",
"active": true,
"slug": "admin",
"type": "NORMAL",
"links": {
"self": [
{
"href": "https://bitbucket-bitbucket.apps.cluster-devtools-c9ac.devtools-c9ac.example.opentlc.com/users/admin"
}
]
}
},
{
"name": "ksmster",
"emailAddress": "ksmster@gmail.com",
"id": 2,
"displayName": "Sergii Kabashniuk",
"active": true,
"slug": "ksmster",
"type": "NORMAL",
"links": {
"self": [
{
"href": "https://bitbucket-bitbucket.apps.cluster-devtools-c9ac.devtools-c9ac.example.opentlc.com/users/ksmster"
}
]
}
},
{
"name": "skabashn",
"emailAddress": "skabashniuk@redhat.com",
"id": 3,
"displayName": "Kabashn",
"active": true,
"slug": "skabashn",
"type": "NORMAL",
"links": {
"self": [
{
"href": "https://bitbucket-bitbucket.apps.cluster-devtools-c9ac.devtools-c9ac.example.opentlc.com/users/skabashn"
}
]
}
}
],
"start": 0
}

View File

@ -0,0 +1,57 @@
{
"size": 3,
"limit": 3,
"isLastPage": false,
"values": [
{
"name": "admin",
"emailAddress": "admin@ksmster.com",
"id": 1,
"displayName": "Admin",
"active": true,
"slug": "admin",
"type": "NORMAL",
"links": {
"self": [
{
"href": "https://bitbucket-bitbucket.apps.cluster-devtools-c9ac.devtools-c9ac.example.opentlc.com/users/admin"
}
]
}
},
{
"name": "ksmster",
"emailAddress": "ksmster@gmail.com",
"id": 2,
"displayName": "Sergii Kabashniuk",
"active": true,
"slug": "ksmster",
"type": "NORMAL",
"links": {
"self": [
{
"href": "https://bitbucket-bitbucket.apps.cluster-devtools-c9ac.devtools-c9ac.example.opentlc.com/users/ksmster"
}
]
}
},
{
"name": "skabashn",
"emailAddress": "skabashniuk@redhat.com",
"id": 3,
"displayName": "Kabashn",
"active": true,
"slug": "skabashn",
"type": "NORMAL",
"links": {
"self": [
{
"href": "https://bitbucket-bitbucket.apps.cluster-devtools-c9ac.devtools-c9ac.example.opentlc.com/users/skabashn"
}
]
}
}
],
"start": 0,
"nextPageStart": 3
}

View File

@ -0,0 +1,57 @@
{
"size": 3,
"limit": 3,
"isLastPage": false,
"values": [
{
"name": "user1",
"emailAddress": "user1@gmail.com",
"id": 52,
"displayName": "User1",
"active": true,
"slug": "user1",
"type": "NORMAL",
"links": {
"self": [
{
"href": "https://bitbucket-bitbucket.apps.cluster-devtools-c9ac.devtools-c9ac.example.opentlc.com/users/user1"
}
]
}
},
{
"name": "user2",
"emailAddress": "user2@gmail.com",
"id": 53,
"displayName": "user2",
"active": true,
"slug": "user2",
"type": "NORMAL",
"links": {
"self": [
{
"href": "https://bitbucket-bitbucket.apps.cluster-devtools-c9ac.devtools-c9ac.example.opentlc.com/users/user2"
}
]
}
},
{
"name": "user3@gmail.com",
"emailAddress": "user3@gmail.com",
"id": 54,
"displayName": "user3",
"active": true,
"slug": "user3",
"type": "NORMAL",
"links": {
"self": [
{
"href": "https://bitbucket-bitbucket.apps.cluster-devtools-c9ac.devtools-c9ac.example.opentlc.com/users/user3_gmail.com"
}
]
}
}
],
"start": 3,
"nextPageStart": 6
}

View File

@ -0,0 +1,57 @@
{
"size": 3,
"limit": 3,
"isLastPage": false,
"values": [
{
"name": "user4",
"emailAddress": "user4@gmail.com",
"id": 55,
"displayName": "user4",
"active": true,
"slug": "user4",
"type": "NORMAL",
"links": {
"self": [
{
"href": "https://bitbucket-bitbucket.apps.cluster-devtools-c9ac.devtools-c9ac.example.opentlc.com/users/user4"
}
]
}
},
{
"name": "user5",
"emailAddress": "user5@gmail.com",
"id": 56,
"displayName": "user5",
"active": true,
"slug": "user5",
"type": "NORMAL",
"links": {
"self": [
{
"href": "https://bitbucket-bitbucket.apps.cluster-devtools-c9ac.devtools-c9ac.example.opentlc.com/users/user5"
}
]
}
},
{
"name": "user6",
"emailAddress": "user6@gmail.com",
"id": 57,
"displayName": "user6",
"active": true,
"slug": "user6",
"type": "NORMAL",
"links": {
"self": [
{
"href": "https://bitbucket-bitbucket.apps.cluster-devtools-c9ac.devtools-c9ac.example.opentlc.com/users/user6"
}
]
}
}
],
"start": 6,
"nextPageStart": 9
}

View File

@ -0,0 +1,24 @@
{
"size": 1,
"limit": 3,
"isLastPage": true,
"values": [
{
"name": "user7",
"emailAddress": "user7@gmail.com",
"id": 58,
"displayName": "user7",
"active": true,
"slug": "user7",
"type": "NORMAL",
"links": {
"self": [
{
"href": "https://bitbucket-bitbucket.apps.cluster-devtools-c9ac.devtools-c9ac.example.opentlc.com/users/user7"
}
]
}
}
],
"start": 9
}

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2012-2018 Red Hat, Inc.
This program and the accompanying materials are made
available under the terms of the Eclipse Public License 2.0
which is available at https://www.eclipse.org/legal/epl-2.0/
SPDX-License-Identifier: EPL-2.0
Contributors:
Red Hat, Inc. - initial API and implementation
-->
<configuration>
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n%nopex</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="stdout"/>
</root>
</configuration>

View File

@ -21,7 +21,8 @@ public interface PersonalAccessTokenFetcher {
*
* @param cheUser
* @param scmServerUrl
* @return - personal access token.
* @return - personal access token. Must return {@code null} if scmServerUrl is not applicable for
* the current fetcher.
* @throws ScmUnauthorizedException - in case if user are not authorized che server to create new
* token. Further user interaction is needed before calling next time this method.
* @throws ScmCommunicationException - Some unexpected problem occurred during communication with

View File

@ -26,6 +26,7 @@
<modules>
<module>che-core-api-auth-shared</module>
<module>che-core-api-auth</module>
<module>che-core-api-auth-bitbucket</module>
<module>che-core-api-auth-github</module>
<module>che-core-api-auth-openshift</module>
<module>che-core-api-workspace-shared</module>