diff --git a/assembly/assembly-wsmaster-war/pom.xml b/assembly/assembly-wsmaster-war/pom.xml
index 19c359d98c9..0abc7e4d82d 100644
--- a/assembly/assembly-wsmaster-war/pom.xml
+++ b/assembly/assembly-wsmaster-war/pom.xml
@@ -111,6 +111,10 @@
org.eclipse.che.core
che-core-api-auth
+
+ org.eclipse.che.core
+ che-core-api-auth-bitbucket
+
org.eclipse.che.core
che-core-api-auth-openshift
diff --git a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java
index d18a0e2026a..5358c04aad1 100644
--- a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java
+++ b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java
@@ -165,6 +165,7 @@ protected void configure() {
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 @@ protected void configure() {
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);
diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties
index e3e1153a513..dd453eeae8d 100644
--- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties
+++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties
@@ -184,6 +184,11 @@ che.oauth.openshift.clientsecret=NULL
che.oauth.openshift.oauth_endpoint= NULL
che.oauth.openshift.verify_token_url= NULL
+che.oauth1.bitbucket.consumerkeypath=NULL
+che.oauth1.bitbucket.privatekeypath=NULL
+che.oauth1.bitbucket.endpoint=NULL
+
+
### Internal
# Che extensions can be scheduled executions on a time basis.
diff --git a/infrastructures/infrastructure-factory/pom.xml b/infrastructures/infrastructure-factory/pom.xml
index 7d0d5c353ab..e122683f5af 100644
--- a/infrastructures/infrastructure-factory/pom.xml
+++ b/infrastructures/infrastructure-factory/pom.xml
@@ -64,13 +64,12 @@
infrastructure-kubernetes
- ch.qos.logback
- logback-classic
- test
+ org.slf4j
+ slf4j-api
- com.github.tomakehurst
- wiremock-jre8-standalone
+ ch.qos.logback
+ logback-classic
test
diff --git a/infrastructures/infrastructure-factory/src/main/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesPersonalAccessTokenManager.java b/infrastructures/infrastructure-factory/src/main/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesPersonalAccessTokenManager.java
index 95ae072ebe0..1816eca7ea0 100644
--- a/infrastructures/infrastructure-factory/src/main/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesPersonalAccessTokenManager.java
+++ b/infrastructures/infrastructure-factory/src/main/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesPersonalAccessTokenManager.java
@@ -15,10 +15,12 @@
import com.google.common.collect.ImmutableMap;
import io.fabric8.kubernetes.api.model.LabelSelector;
import io.fabric8.kubernetes.api.model.LabelSelectorBuilder;
+import io.fabric8.kubernetes.api.model.Namespace;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
@@ -41,10 +43,16 @@
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/** Manages personal access token secrets used for private repositories authentication. */
@Singleton
public class KubernetesPersonalAccessTokenManager implements PersonalAccessTokenManager {
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(KubernetesPersonalAccessTokenManager.class);
+
public static final Map SECRET_LABELS =
ImmutableMap.of(
"app.kubernetes.io/part-of", "che.eclipse.org",
@@ -83,6 +91,7 @@ void save(PersonalAccessToken personalAccessToken)
throws UnsatisfiedScmPreconditionException, ScmConfigurationPersistenceException {
try {
String namespace = getFirstNamespace();
+ LOG.info(" save in namespace {}", namespace);
ObjectMeta meta =
new ObjectMetaBuilder()
.withName(NameGenerator.generate(NAME_PATTERN, 5))
@@ -134,6 +143,9 @@ public Optional get(Subject cheUser, String scmServerUrl)
throws ScmConfigurationPersistenceException {
try {
+ String namespace = getFirstNamespace();
+ LOG.info(" save in namespace {}", namespace);
+
for (KubernetesNamespaceMeta namespaceMeta : namespaceFactory.list()) {
List secrets =
namespaceFactory
@@ -158,6 +170,8 @@ public Optional get(Subject cheUser, String scmServerUrl)
}
} catch (InfrastructureException e) {
throw new ScmConfigurationPersistenceException(e.getMessage(), e);
+ } catch (UnsatisfiedScmPreconditionException e) {
+ return Optional.empty();
}
return Optional.empty();
}
@@ -178,4 +192,26 @@ private String getFirstNamespace()
throw new ScmConfigurationPersistenceException(e.getMessage(), e);
}
}
+
+ private boolean canCreateDeleteSecrets(Namespace namespace) {
+ LOG.info("testing namespace {}", namespace);
+ ObjectMeta meta =
+ new ObjectMetaBuilder().withName(NameGenerator.generate(NAME_PATTERN, "tmp", 5)).build();
+ Secret secret = new SecretBuilder().withMetadata(meta).build();
+ try {
+ KubernetesClient kubernetesClient = clientFactory.create();
+ Secret result =
+ kubernetesClient
+ .secrets()
+ .inNamespace(namespace.getMetadata().getName())
+ .createOrReplace(secret);
+ kubernetesClient.secrets().delete(result);
+ LOG.info(" namespace {} is ok", namespace);
+ return true;
+ } catch (InfrastructureException e) {
+ LOG.error(e.getMessage(), e);
+ }
+ LOG.info(" namespace {} is not ok", namespace);
+ return false;
+ }
}
diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakServletModule.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakServletModule.java
index 93ef4cdb647..0606f21d265 100644
--- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakServletModule.java
+++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakServletModule.java
@@ -25,7 +25,7 @@ public class KeycloakServletModule extends ServletModule {
// not contains /docs/ (for swagger)
+ "(?!.*(/docs/))"
// not ends with '/oauth/callback/' or '/keycloak/settings/' or '/system/state'
- + "(?!.*(/keycloak/settings/?|/oauth/callback/?|/system/state/?)$)"
+ + "(?!.*(/keycloak/settings/?|/oauth/callback/?|/oauth/1.0/callback/?|/system/state/?)$)"
// all other
+ ".*";
diff --git a/pom.xml b/pom.xml
index a9d155af9c7..d7192c684ee 100644
--- a/pom.xml
+++ b/pom.xml
@@ -700,6 +700,11 @@
che-core-api-auth
${che.version}
+
+ org.eclipse.che.core
+ che-core-api-auth-bitbucket
+ ${che.version}
+
org.eclipse.che.core
che-core-api-auth-github
diff --git a/wsmaster/che-core-api-auth-bitbucket/pom.xml b/wsmaster/che-core-api-auth-bitbucket/pom.xml
new file mode 100644
index 00000000000..8d1a6ccb1d8
--- /dev/null
+++ b/wsmaster/che-core-api-auth-bitbucket/pom.xml
@@ -0,0 +1,55 @@
+
+
+
+ 4.0.0
+
+ che-master-parent
+ org.eclipse.che.core
+ 7.25.0-SNAPSHOT
+
+ che-core-api-auth-bitbucket
+ jar
+ Che Core :: API :: Authentication Bitbucket
+
+
+ com.google.guava
+ guava
+
+
+ com.google.inject
+ guice
+
+
+ javax.inject
+ javax.inject
+
+
+ org.eclipse.che.core
+ che-core-api-auth
+
+
+ org.eclipse.che.core
+ che-core-commons-annotations
+
+
+ org.eclipse.che.core
+ che-core-commons-inject
+
+
+ org.slf4j
+ slf4j-api
+
+
+
diff --git a/wsmaster/che-core-api-auth-bitbucket/src/main/java/org/eclipse/che/security/oauth1/BitbucketModule.java b/wsmaster/che-core-api-auth-bitbucket/src/main/java/org/eclipse/che/security/oauth1/BitbucketModule.java
new file mode 100644
index 00000000000..e702863582c
--- /dev/null
+++ b/wsmaster/che-core-api-auth-bitbucket/src/main/java/org/eclipse/che/security/oauth1/BitbucketModule.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+import org.eclipse.che.inject.DynaModule;
+
+/**
+ * Setup BitbucketServerOAuthAuthenticator in guice container.
+ *
+ * @author Sergii Kabashniuk
+ */
+@DynaModule
+public class BitbucketModule extends AbstractModule {
+ @Override
+ protected void configure() {
+ Multibinder oAuthAuthenticators =
+ Multibinder.newSetBinder(binder(), OAuthAuthenticator.class);
+ oAuthAuthenticators.addBinding().toProvider(BitbucketServerOAuthAuthenticatorProvider.class);
+ }
+}
diff --git a/wsmaster/che-core-api-auth-bitbucket/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerOAuthAuthenticator.java b/wsmaster/che-core-api-auth-bitbucket/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerOAuthAuthenticator.java
new file mode 100644
index 00000000000..1860f005f8e
--- /dev/null
+++ b/wsmaster/che-core-api-auth-bitbucket/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerOAuthAuthenticator.java
@@ -0,0 +1,46 @@
+/*
+ * 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.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+/**
+ * OAuth1 authentication for Bitbucket Server account.
+ *
+ * @author Igor Vinokur
+ */
+@Singleton
+public class BitbucketServerOAuthAuthenticator extends OAuthAuthenticator {
+
+ @Inject
+ public BitbucketServerOAuthAuthenticator(
+ @Named("che.oauth1.bitbucket.consumerkey") String consumerKey,
+ @Named("che.oauth1.bitbucket.privatekey") String privateKey,
+ @Named("che.oauth1.bitbucket.endpoint") String bitbucketEndpoint,
+ @Named("che.api") 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 "bitbucket-server";
+ }
+}
diff --git a/wsmaster/che-core-api-auth-bitbucket/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerOAuthAuthenticatorProvider.java b/wsmaster/che-core-api-auth-bitbucket/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerOAuthAuthenticatorProvider.java
new file mode 100644
index 00000000000..3d384108fbc
--- /dev/null
+++ b/wsmaster/che-core-api-auth-bitbucket/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerOAuthAuthenticatorProvider.java
@@ -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 {
+ private final OAuthAuthenticator authenticator;
+ private static final Logger LOG =
+ LoggerFactory.getLogger(BitbucketServerOAuthAuthenticatorProvider.class);
+
+ @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.info("authenticator={}", authenticator);
+ }
+
+ @Override
+ public OAuthAuthenticator get() {
+ return authenticator;
+ }
+
+ private static OAuthAuthenticator getOAuthAuthenticator(
+ String consumerKeyPath, String privateKeyPath, String bitbucketEndpoint, String apiEndpoint)
+ throws IOException {
+ LOG.info("1");
+ if (!isNullOrEmpty(bitbucketEndpoint) && consumerKeyPath != null && privateKeyPath != null) {
+ String consumerKey = Files.readString(Path.of(consumerKeyPath));
+ String privateKey = Files.readString(Path.of(privateKeyPath));
+ LOG.info("consumerKey={} privateKey={}", consumerKey, privateKey);
+ if (!isNullOrEmpty(consumerKey) && !isNullOrEmpty(privateKey)) {
+ return new BitbucketServerOAuthAuthenticator(
+ consumerKey, privateKey, bitbucketEndpoint, apiEndpoint);
+ }
+ }
+
+ return new NoopOAuthAuthenticator();
+ }
+}
diff --git a/wsmaster/che-core-api-auth-bitbucket/src/main/java/org/eclipse/che/security/oauth1/NoopOAuthAuthenticator.java b/wsmaster/che-core-api-auth-bitbucket/src/main/java/org/eclipse/che/security/oauth1/NoopOAuthAuthenticator.java
new file mode 100644
index 00000000000..6c7e20eb6db
--- /dev/null
+++ b/wsmaster/che-core-api-auth-bitbucket/src/main/java/org/eclipse/che/security/oauth1/NoopOAuthAuthenticator.java
@@ -0,0 +1,42 @@
+/*
+ * 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;
+
+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("Invalid usage of NoopOAuthAuthenticator");
+ }
+
+ @Override
+ String callback(URL requestUrl) throws OAuthAuthenticationException {
+ throw new RuntimeException("Invalid usage of NoopOAuthAuthenticator");
+ }
+
+ @Override
+ String computeAuthorizationHeader(String userId, String requestMethod, String requestUrl)
+ throws OAuthAuthenticationException {
+ throw new RuntimeException("Invalid usage of NoopOAuthAuthenticator");
+ }
+}
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/pom.xml b/wsmaster/che-core-api-factory-bitbucket-server/pom.xml
index 7b52e1b3014..3ba0872bc7c 100644
--- a/wsmaster/che-core-api-factory-bitbucket-server/pom.xml
+++ b/wsmaster/che-core-api-factory-bitbucket-server/pom.xml
@@ -26,6 +26,18 @@
false
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
com.google.guava
guava
@@ -42,6 +54,18 @@
javax.validation
validation-api
+
+ javax.ws.rs
+ javax.ws.rs-api
+
+
+ org.eclipse.che.core
+ che-core-api-auth
+
+
+ org.eclipse.che.core
+ che-core-api-auth-bitbucket
+
org.eclipse.che.core
che-core-api-core
@@ -74,11 +98,20 @@
org.eclipse.che.core
che-core-commons-lang
+
+ org.slf4j
+ slf4j-api
+
ch.qos.logback
logback-classic
test
+
+ com.github.tomakehurst
+ wiremock-jre8-standalone
+ test
+
org.eclipse.che.core
che-core-commons-json
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerModule.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerModule.java
index 72a5740d1c5..794e4dac36f 100644
--- a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerModule.java
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerModule.java
@@ -13,7 +13,9 @@
import com.google.inject.AbstractModule;
import com.google.inject.multibindings.Multibinder;
+import org.eclipse.che.api.factory.server.bitbucket.server.BitbucketServerApi;
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 @@ protected void configure() {
Multibinder tokenFetcherMultibinder =
Multibinder.newSetBinder(binder(), PersonalAccessTokenFetcher.class);
tokenFetcherMultibinder.addBinding().to(BitbucketServerPersonalAccessTokenFetcher.class);
+ bind(BitbucketServerApi.class).toProvider(BitbucketServerApiProvider.class);
}
}
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerPersonalAccessTokenFetcher.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerPersonalAccessTokenFetcher.java
index b0759493e2c..112147a2389 100644
--- a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerPersonalAccessTokenFetcher.java
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerPersonalAccessTokenFetcher.java
@@ -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.BitbucketServerApi;
+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,59 @@
* 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 BitbucketServerApi bitbucketServerApi;
+ private final URL apiEndpoint;
+
+ @Inject
+ public BitbucketServerPersonalAccessTokenFetcher(
+ BitbucketServerApi bitbucketServerApi, @Named("che.api") URL apiEndpoint) {
+ this.bitbucketServerApi = bitbucketServerApi;
+ this.apiEndpoint = apiEndpoint;
+ }
+
@Override
- public PersonalAccessToken fetchPersonalAccessToken(Subject cheUser, String scmServerUrl) {
- return null;
+ public PersonalAccessToken fetchPersonalAccessToken(Subject cheUser, String scmServerUrl)
+ throws ScmUnauthorizedException, ScmCommunicationException {
+ if (!bitbucketServerApi.isConnected(scmServerUrl)) {
+ LOG.info("not valid url {} for current fetcher ", scmServerUrl);
+ return null;
+ }
+
+ String tokenName = format(TOKEN_NAME_TEMPLATE, cheUser.getUserId(), apiEndpoint.getHost());
+ try {
+ BitbucketUser user = bitbucketServerApi.getUser(EnvironmentContext.getCurrent().getSubject());
+ LOG.info("Current bitbucket user {} ", user);
+ // cleanup existed
+ List existedTokens =
+ bitbucketServerApi
+ .getPersonalAccessTokens(user.getSlug())
+ .stream()
+ .filter(p -> p.getName().equals(tokenName))
+ .collect(Collectors.toList());
+ for (BitbucketPersonalAccessToken existedToken : existedTokens) {
+ LOG.info("Deleting existed che token {} {}", existedToken.getId(), existedToken.getName());
+ bitbucketServerApi.deletePersonalAccessTokens(user.getSlug(), existedToken.getId());
+ }
+
+ BitbucketPersonalAccessToken token =
+ bitbucketServerApi.createPersonalAccessTokens(
+ user.getSlug(), tokenName, ImmutableSet.of("PROJECT_WRITE", "REPO_WRITE"));
+ LOG.info("Token = {} 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);
+ }
}
}
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketURLParser.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketURLParser.java
index 460bffb7666..faabaa4d1ae 100644
--- a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketURLParser.java
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketURLParser.java
@@ -25,6 +25,8 @@
import org.eclipse.che.api.factory.server.urlfactory.DevfileFilenamesProvider;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Parser of String Bitbucket URLs and provide {@link BitbucketUrl} objects.
@@ -33,7 +35,7 @@
*/
@Singleton
public class BitbucketURLParser {
-
+ private static final Logger LOG = LoggerFactory.getLogger(BitbucketURLParser.class);
private final DevfileFilenamesProvider devfileFilenamesProvider;
private static final String bitbucketUrlPatternTemplate =
"^(?%s)/scm/(?[^/]++)/(?[^.]++).git(\\?at=)?(?[\\w\\d-_]*)";
@@ -43,6 +45,7 @@ public class BitbucketURLParser {
public BitbucketURLParser(
@Nullable @Named("che.integration.bitbucket.server_endpoints") String bitbucketEndpoints,
DevfileFilenamesProvider devfileFilenamesProvider) {
+ LOG.info("bitbucketEndpoints={}", bitbucketEndpoints);
this.devfileFilenamesProvider = devfileFilenamesProvider;
if (bitbucketEndpoints != null) {
for (String bitbucketEndpoint : Splitter.on(",").split(bitbucketEndpoints)) {
@@ -54,6 +57,7 @@ public BitbucketURLParser(
}
public boolean isValid(@NotNull String url) {
+ LOG.info("isValid={}", url);
return !bitbucketUrlPatterns.isEmpty()
&& bitbucketUrlPatterns.stream().anyMatch(pattern -> pattern.matcher(url).matches());
}
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/AuthorizationHeaderProvider.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/AuthorizationHeaderProvider.java
new file mode 100644
index 00000000000..2b461f93b0a
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/AuthorizationHeaderProvider.java
@@ -0,0 +1,20 @@
+/*
+ * 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.ScmUnauthorizedException;
+
+/** Compute the Authorization header to sign the OAuth 1 request. */
+public interface AuthorizationHeaderProvider {
+ String computeAuthorizationHeader(final String requestMethod, final String requestUrl)
+ throws ScmUnauthorizedException;
+}
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/BitbucketPersonalAccessToken.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/BitbucketPersonalAccessToken.java
new file mode 100644
index 00000000000..43f4fe40bd6
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/BitbucketPersonalAccessToken.java
@@ -0,0 +1,128 @@
+/*
+ * 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.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 permissions;
+
+ public BitbucketPersonalAccessToken(String name, Set permissions) {
+ this.name = name;
+ this.permissions = permissions;
+ }
+
+ public BitbucketPersonalAccessToken() {}
+
+ public BitbucketPersonalAccessToken(
+ long id,
+ long createdDate,
+ long lastAuthenticated,
+ String name,
+ String token,
+ BitbucketUser user,
+ Set 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 getPermissions() {
+ return permissions;
+ }
+
+ public void setPermissions(Set 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
+ + '}';
+ }
+}
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/BitbucketServerApi.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/BitbucketServerApi.java
new file mode 100644
index 00000000000..8e48ddac991
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/BitbucketServerApi.java
@@ -0,0 +1,103 @@
+/*
+ * 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;
+
+public interface BitbucketServerApi {
+
+ 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 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 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 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 getPersonalAccessTokens(String userSlug)
+ throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException;
+}
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/BitbucketServerApiHttpClient.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/BitbucketServerApiHttpClient.java
new file mode 100644
index 00000000000..1b00770e4a3
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/BitbucketServerApiHttpClient.java
@@ -0,0 +1,322 @@
+/*
+ * 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.time.Duration.ofSeconds;
+
+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 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.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+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.subject.Subject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class BitbucketServerApiHttpClient implements BitbucketServerApi {
+
+ private static final ObjectMapper OM = new ObjectMapper();
+
+ private static final Logger LOG = LoggerFactory.getLogger(BitbucketServerApiHttpClient.class);
+ private final URI serverUri;
+ private final AuthorizationHeaderProvider headerProvider;
+
+ @Inject
+ public BitbucketServerApiHttpClient(
+ String serverUrl, AuthorizationHeaderProvider authorizationHeaderProvider) {
+ this.serverUri = URI.create(serverUrl);
+ this.headerProvider = authorizationHeaderProvider;
+ }
+
+ @Override
+ public boolean isConnected(String bitbucketServerUrl) {
+ boolean equals = serverUri.equals(URI.create(bitbucketServerUrl));
+ LOG.info("{}={}={}", serverUri, bitbucketServerUrl, equals);
+ return equals;
+ }
+
+ @Override
+ public BitbucketUser getUser(Subject cheUser)
+ throws ScmUnauthorizedException, ScmCommunicationException {
+ try {
+ Set usersByName =
+ getUsers(cheUser.getUserName())
+ .stream()
+ .map(u -> u.getSlug())
+ .collect(Collectors.toSet());
+
+ Optional currentUser = findCurrentUser(usersByName);
+ if (currentUser.isPresent()) {
+ return currentUser.get();
+ }
+ Set usersAllExceptByName =
+ getUsers()
+ .stream()
+ .map(u -> u.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 {
+ HttpClient httpClient = HttpClient.newHttpClient();
+ URI uri = serverUri.resolve("/rest/api/1.0/users/" + slug);
+ HttpRequest request =
+ HttpRequest.newBuilder(uri)
+ .headers(
+ "Authorization", headerProvider.computeAuthorizationHeader("GET", uri.toString()))
+ .timeout(ofSeconds(10))
+ .build();
+
+ try {
+ 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 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 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 {
+ HttpClient httpClient = HttpClient.newHttpClient();
+ 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(ofSeconds(10))
+ .build();
+
+ try {
+ 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 permissions)
+ throws ScmBadRequestException, ScmUnauthorizedException, ScmCommunicationException {
+ HttpClient httpClient = HttpClient.newHttpClient();
+ URI uri = serverUri.resolve("/rest/access-tokens/1.0/users/" + userSlug);
+ HttpRequest request =
+ HttpRequest.newBuilder(uri)
+ .PUT(
+ JsonBodyPublishers.fromObject(
+ new BitbucketPersonalAccessToken(tokenName, permissions)))
+ .headers(
+ HttpHeaders.AUTHORIZATION,
+ headerProvider.computeAuthorizationHeader("PUT", uri.toString()),
+ HttpHeaders.ACCEPT,
+ MediaType.APPLICATION_JSON,
+ HttpHeaders.CONTENT_TYPE,
+ MediaType.APPLICATION_JSON)
+ .timeout(ofSeconds(10))
+ .build();
+
+ try {
+ return executeRequest(
+ httpClient,
+ request,
+ inputStream -> {
+ try {
+ return OM.readValue(inputStream, BitbucketPersonalAccessToken.class);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ });
+ } catch (ScmItemNotFoundException e) {
+ throw new ScmCommunicationException(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public List 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);
+ }
+ }
+
+ private Optional findCurrentUser(Set userSlugs)
+ throws ScmCommunicationException, ScmUnauthorizedException, ScmItemNotFoundException {
+
+ LOG.info("Testing {} items {}", userSlugs.size(), userSlugs);
+ for (String userSlug : userSlugs) {
+ LOG.info("findCurrentUser={}", userSlug);
+ BitbucketUser user = getUser(userSlug);
+ try {
+ getPersonalAccessTokens(userSlug);
+ return Optional.of(user);
+ } catch (ScmItemNotFoundException | ScmUnauthorizedException e) {
+ // ok
+ }
+ }
+ return Optional.empty();
+ }
+
+ private List doGetItems(Class tClass, String api, String filter)
+ throws ScmUnauthorizedException, ScmCommunicationException, ScmBadRequestException,
+ ScmItemNotFoundException {
+ List result = new ArrayList<>();
+ Page 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 Page doGetPage(Class tClass, String api, int start, int limit, String filter)
+ throws ScmUnauthorizedException, ScmBadRequestException, ScmCommunicationException,
+ ScmItemNotFoundException {
+ HttpClient httpClient = HttpClient.newHttpClient();
+ 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(ofSeconds(10))
+ .build();
+ 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 executeRequest(
+ HttpClient httpClient, HttpRequest request, Function function)
+ throws ScmBadRequestException, ScmItemNotFoundException, ScmCommunicationException,
+ ScmUnauthorizedException {
+ try {
+ HttpResponse response =
+ httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
+ LOG.info("executeRequest={} response {}", request, response.statusCode());
+ if (response.statusCode() == 200) {
+ return function.apply(response.body());
+ } else if (response.statusCode() == 204) {
+ return null;
+ } else {
+ String body = CharStreams.toString(new InputStreamReader(response.body(), Charsets.UTF_8));
+ if (response.statusCode() == 400) {
+ throw new ScmBadRequestException(body);
+ } else if (response.statusCode() == 401) {
+ throw new ScmUnauthorizedException(body);
+ } else if (response.statusCode() == 404) {
+ throw new ScmItemNotFoundException(body);
+ } else {
+ throw new ScmCommunicationException(
+ "Unexpected status code " + response.statusCode() + " " + response.toString());
+ }
+ }
+
+ } catch (IOException | InterruptedException | UncheckedIOException e) {
+ throw new ScmCommunicationException(e.getMessage(), e);
+ }
+ }
+}
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/BitbucketUser.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/BitbucketUser.java
new file mode 100644
index 00000000000..cc9566418a8
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/BitbucketUser.java
@@ -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
+ + '\''
+ + '}';
+ }
+}
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/JsonBodyPublishers.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/JsonBodyPublishers.java
new file mode 100644
index 00000000000..76454333c6d
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/JsonBodyPublishers.java
@@ -0,0 +1,36 @@
+/*
+ * 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.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.UncheckedIOException;
+import java.net.http.HttpRequest;
+import java.util.function.Supplier;
+
+/**
+ * {@link Supplier} because https://bugs.openjdk.java.net/browse/JDK-8217264 see
+ * https://stackoverflow.com/questions/53379087/wrapping-bodysubscriberinputstream-in-gzipinputstream-leads-to-hang
+ */
+public class JsonBodyPublishers {
+ private static final ObjectMapper OM = new ObjectMapper();
+
+ private JsonBodyPublishers() {}
+
+ public static HttpRequest.BodyPublisher fromObject(Object object) {
+ try {
+ return HttpRequest.BodyPublishers.ofString(OM.writeValueAsString(object));
+ } catch (JsonProcessingException e) {
+ throw new UncheckedIOException(e.getMessage(), e);
+ }
+ }
+}
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/NopBitbucketServerApi.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/NopBitbucketServerApi.java
new file mode 100644
index 00000000000..9d2ad7fcaf3
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/NopBitbucketServerApi.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+public class NopBitbucketServerApi implements BitbucketServerApi {
+ @Override
+ public boolean isConnected(String bitbucketServerUrl) {
+ return false;
+ }
+
+ @Override
+ public BitbucketUser getUser(Subject cheUser)
+ throws ScmUnauthorizedException, ScmCommunicationException {
+ throw new RuntimeException("Invalid usage of BitbucketServerApi");
+ }
+
+ @Override
+ public BitbucketUser getUser(String slug)
+ throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
+ throw new RuntimeException("Invalid usage of BitbucketServerApi");
+ }
+
+ @Override
+ public List getUsers()
+ throws ScmBadRequestException, ScmUnauthorizedException, ScmCommunicationException {
+ throw new RuntimeException("Invalid usage of BitbucketServerApi");
+ }
+
+ @Override
+ public List getUsers(String filter)
+ throws ScmBadRequestException, ScmUnauthorizedException, ScmCommunicationException {
+ throw new RuntimeException("Invalid usage of BitbucketServerApi");
+ }
+
+ @Override
+ public void deletePersonalAccessTokens(String userSlug, Long tokenId)
+ throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
+ throw new RuntimeException("Invalid usage of BitbucketServerApi");
+ }
+
+ @Override
+ public BitbucketPersonalAccessToken createPersonalAccessTokens(
+ String userSlug, String tokenName, Set permissions)
+ throws ScmBadRequestException, ScmUnauthorizedException, ScmCommunicationException {
+ throw new RuntimeException("Invalid usage of BitbucketServerApi");
+ }
+
+ @Override
+ public List getPersonalAccessTokens(String userSlug)
+ throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
+ throw new RuntimeException("Invalid usage of BitbucketServerApi");
+ }
+}
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/Page.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/Page.java
new file mode 100644
index 00000000000..6681acc87bf
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/Page.java
@@ -0,0 +1,93 @@
+/*
+ * 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;
+
+public class Page {
+ private int start;
+ private int size;
+ private int limit;
+
+ @JsonProperty(value = "isLastPage")
+ private boolean isLastPage;
+
+ private int nextPageStart;
+ List 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 getValues() {
+ return values;
+ }
+
+ public void setValues(List 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
+ + '}';
+ }
+}
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerApiProvider.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerApiProvider.java
new file mode 100644
index 00000000000..f7248c1b316
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerApiProvider.java
@@ -0,0 +1,87 @@
+/*
+ * 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.BitbucketServerApi;
+import org.eclipse.che.api.factory.server.bitbucket.server.BitbucketServerApiHttpClient;
+import org.eclipse.che.api.factory.server.bitbucket.server.NopBitbucketServerApi;
+import org.eclipse.che.commons.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class BitbucketServerApiProvider implements Provider {
+ private final BitbucketServerApi bitbucketServerApi;
+ private static final Logger LOG = LoggerFactory.getLogger(BitbucketServerApiProvider.class);
+
+ @Inject
+ public BitbucketServerApiProvider(
+ @Nullable @Named("che.integration.bitbucket.server_endpoints") String bitbucketEndpoints,
+ @Nullable @Named("che.oauth1.bitbucket.endpoint") String bitbucketEndpoint,
+ Set authenticators) {
+ bitbucketServerApi = doGet(bitbucketEndpoints, bitbucketEndpoint, authenticators);
+ LOG.info("bitbucketServerApi={}", bitbucketServerApi);
+ }
+
+ @Override
+ public BitbucketServerApi get() {
+ return bitbucketServerApi;
+ }
+
+ private static BitbucketServerApi doGet(
+ String bitbucketEndpoints, String bitbucketEndpoint, Set authenticators) {
+ LOG.info("bitbucketEndpoint={}", bitbucketEndpoint);
+ if (isNullOrEmpty(bitbucketEndpoint)) {
+ return new NopBitbucketServerApi();
+ } else {
+ LOG.info("bitbucketEndpoint={}", bitbucketEndpoints);
+ if (isNullOrEmpty(bitbucketEndpoints)) {
+ throw new RuntimeException(
+ "`che.integration.bitbucket.server_endpoints` bitbucket configuration is missing.");
+ } else {
+ LOG.info(
+ "bitbucketEndpoints.contains(bitbucketEndpoint)={}",
+ bitbucketEndpoints.contains(bitbucketEndpoint));
+ LOG.info("authenticators={}", authenticators);
+ if (bitbucketEndpoints.contains(bitbucketEndpoint)) {
+ Optional authenticator =
+ authenticators
+ .stream()
+ .filter(a -> a.getOAuthProvider().equals("bitbucket-server"))
+ .findFirst();
+ LOG.info("authenticator={}", authenticators);
+ if (authenticator.isEmpty()) {
+ throw new RuntimeException(
+ "BitbucketServerOAuthAuthenticator is not configured correctly");
+ }
+ return new BitbucketServerApiHttpClient(
+ bitbucketEndpoint,
+ new BitbucketServerOAuth1AuthorizationHeaderProvider(
+ (BitbucketServerOAuthAuthenticator) authenticator.get()));
+ } else {
+ throw new RuntimeException(
+ "`che.integration.bitbucket.server_endpoints` mast contain `"
+ + bitbucketEndpoint
+ + "` value");
+ }
+ }
+ }
+ }
+}
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerOAuth1AuthorizationHeaderProvider.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerOAuth1AuthorizationHeaderProvider.java
new file mode 100644
index 00000000000..8ad12d339b0
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerOAuth1AuthorizationHeaderProvider.java
@@ -0,0 +1,50 @@
+/*
+ * 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.AuthorizationHeaderProvider;
+import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.subject.Subject;
+
+public class BitbucketServerOAuth1AuthorizationHeaderProvider
+ implements AuthorizationHeaderProvider {
+ private final BitbucketServerOAuthAuthenticator authenticator;
+
+ @Inject
+ public BitbucketServerOAuth1AuthorizationHeaderProvider(
+ BitbucketServerOAuthAuthenticator authenticator) {
+ this.authenticator = authenticator;
+ }
+
+ @Override
+ public String computeAuthorizationHeader(String requestMethod, String requestUrl)
+ throws ScmUnauthorizedException {
+ 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 ScmUnauthorizedException(e.getMessage(), e);
+ }
+ }
+}
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerApiHttpClientTest.java b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerApiHttpClientTest.java
new file mode 100644
index 00000000000..a1bd9cbdf25
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerApiHttpClientTest.java
@@ -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.BitbucketServerApi;
+import org.eclipse.che.api.factory.server.bitbucket.server.BitbucketServerApiHttpClient;
+import org.eclipse.che.api.factory.server.bitbucket.server.BitbucketUser;
+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 BitbucketServerApiHttpClientTest {
+ 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;
+ BitbucketServerApi 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 BitbucketServerApiHttpClient(
+ 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 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 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 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");
+ }
+}
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerPersonalAccessTokenFetcherTest.java b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerPersonalAccessTokenFetcherTest.java
new file mode 100644
index 00000000000..6e79b11bbd6
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerPersonalAccessTokenFetcherTest.java
@@ -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.BitbucketServerApi;
+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 BitbucketServerApi bitbucketServerApi;
+ 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--",
+ "2340590skdf3<0>945i0923i4jasoidfj934ui50",
+ bitbucketUser,
+ ImmutableSet.of("PROJECT_WRITE", "REPO_WRITE"));
+ bitbucketPersonalAccessToken2 =
+ new BitbucketPersonalAccessToken(
+ 3647456,
+ 234345345,
+ 23534534,
+ "che-token--",
+ "34545<0>945i0923i4jasoidfj934ui50",
+ bitbucketUser,
+ ImmutableSet.of("REPO_READ"));
+ bitbucketPersonalAccessToken3 =
+ new BitbucketPersonalAccessToken(
+ 132423,
+ 234345345,
+ 23534534,
+ "che-token--",
+ "3456\\<0>945//i0923i4jasoidfj934ui50",
+ bitbucketUser,
+ ImmutableSet.of("PROJECT_READ", "REPO_READ"));
+ fetcher = new BitbucketServerPersonalAccessTokenFetcher(bitbucketServerApi, apiEndpoint);
+ EnvironmentContext context = new EnvironmentContext();
+ context.setSubject(subject);
+ EnvironmentContext.setCurrent(context);
+ }
+
+ @Test
+ public void shouldSkipToFetchUnknownUrls()
+ throws ScmUnauthorizedException, ScmCommunicationException {
+ // given
+ when(bitbucketServerApi.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(bitbucketServerApi.isConnected(eq(someNotBitbucketURL))).thenReturn(true);
+ doThrow(exception).when(bitbucketServerApi).getUser(eq(subject));
+ // when
+ fetcher.fetchPersonalAccessToken(subject, someNotBitbucketURL);
+ }
+
+ @Test
+ public void shouldBeAbleToFetchPersonalAccessToken()
+ throws ScmUnauthorizedException, ScmCommunicationException, ScmItemNotFoundException,
+ ScmBadRequestException {
+ // given
+ when(bitbucketServerApi.isConnected(eq(someBitbucketURL))).thenReturn(true);
+ when(bitbucketServerApi.getUser(eq(subject))).thenReturn(bitbucketUser);
+ when(bitbucketServerApi.getPersonalAccessTokens(eq(bitbucketUser.getSlug())))
+ .thenReturn(Collections.emptyList());
+
+ when(bitbucketServerApi.createPersonalAccessTokens(
+ eq(bitbucketUser.getSlug()),
+ eq("che-token--"),
+ 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(bitbucketServerApi.isConnected(eq(someBitbucketURL))).thenReturn(true);
+ when(bitbucketServerApi.getUser(eq(subject))).thenReturn(bitbucketUser);
+ when(bitbucketServerApi.getPersonalAccessTokens(eq(bitbucketUser.getSlug())))
+ .thenReturn(ImmutableList.of(bitbucketPersonalAccessToken, bitbucketPersonalAccessToken2));
+ when(bitbucketServerApi.createPersonalAccessTokens(
+ eq(bitbucketUser.getSlug()),
+ eq("che-token--"),
+ eq(ImmutableSet.of("PROJECT_WRITE", "REPO_WRITE"))))
+ .thenReturn(bitbucketPersonalAccessToken3);
+ // when
+ PersonalAccessToken result = fetcher.fetchPersonalAccessToken(subject, someBitbucketURL);
+ // then
+ assertNotNull(result);
+ verify(bitbucketServerApi)
+ .deletePersonalAccessTokens(
+ eq(bitbucketUser.getSlug()), eq(bitbucketPersonalAccessToken.getId()));
+ verify(bitbucketServerApi)
+ .deletePersonalAccessTokens(
+ eq(bitbucketUser.getSlug()), eq(bitbucketPersonalAccessToken2.getId()));
+ }
+
+ @Test(expectedExceptions = {ScmCommunicationException.class})
+ public void shouldRethrowUnExceptionsOnCreatePersonalAccessTokens()
+ throws ScmUnauthorizedException, ScmCommunicationException, ScmItemNotFoundException,
+ ScmBadRequestException {
+ // given
+ when(bitbucketServerApi.isConnected(eq(someBitbucketURL))).thenReturn(true);
+ when(bitbucketServerApi.getUser(eq(subject))).thenReturn(bitbucketUser);
+ when(bitbucketServerApi.getPersonalAccessTokens(eq(bitbucketUser.getSlug())))
+ .thenReturn(Collections.emptyList());
+ doThrow(ScmBadRequestException.class)
+ .when(bitbucketServerApi)
+ .createPersonalAccessTokens(
+ eq(bitbucketUser.getSlug()),
+ eq("che-token--"),
+ 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}};
+ }
+}
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/access-tokens/1.0/users/ksmster/newtoken.json b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/access-tokens/1.0/users/ksmster/newtoken.json
new file mode 100644
index 00000000000..11040668039
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/access-tokens/1.0/users/ksmster/newtoken.json
@@ -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"
+}
\ No newline at end of file
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/access-tokens/1.0/users/ksmster/response.json b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/access-tokens/1.0/users/ksmster/response.json
new file mode 100644
index 00000000000..0430bfa0f53
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/access-tokens/1.0/users/ksmster/response.json
@@ -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
+}
\ No newline at end of file
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/filtered/response.json b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/filtered/response.json
new file mode 100644
index 00000000000..af2b8fc2ba9
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/filtered/response.json
@@ -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
+}
\ No newline at end of file
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/ksmster/response.json b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/ksmster/response.json
new file mode 100644
index 00000000000..7dcb40e18df
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/ksmster/response.json
@@ -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"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response.json b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response.json
new file mode 100644
index 00000000000..d8c9abcaff5
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response.json
@@ -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
+}
\ No newline at end of file
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response_s0_l25.json b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response_s0_l25.json
new file mode 100644
index 00000000000..0ac1dee1ff6
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response_s0_l25.json
@@ -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
+}
\ No newline at end of file
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response_s3_l25.json b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response_s3_l25.json
new file mode 100644
index 00000000000..a8dc6f0a708
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response_s3_l25.json
@@ -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
+}
\ No newline at end of file
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response_s6_l25.json b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response_s6_l25.json
new file mode 100644
index 00000000000..eac9465a729
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response_s6_l25.json
@@ -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
+}
\ No newline at end of file
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response_s9_l25.json b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response_s9_l25.json
new file mode 100644
index 00000000000..a5b58aa0ca3
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response_s9_l25.json
@@ -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
+}
\ No newline at end of file
diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/logback-test.xml b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..09f9fe3ec7a
--- /dev/null
+++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/logback-test.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+ %-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n%nopex
+
+
+
+
+
+
+
+
diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/scm/PersonalAccessTokenFetcher.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/scm/PersonalAccessTokenFetcher.java
index 784b9294338..3fa3db166bf 100644
--- a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/scm/PersonalAccessTokenFetcher.java
+++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/scm/PersonalAccessTokenFetcher.java
@@ -21,7 +21,8 @@ public interface PersonalAccessTokenFetcher {
*
* @param cheUser
* @param scmServerUrl
- * @return - personal access token.
+ * @return - personal access token. Mast return 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
diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/urlfactory/URLFactoryBuilder.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/urlfactory/URLFactoryBuilder.java
index 28281b49780..0c11fafbbd5 100644
--- a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/urlfactory/URLFactoryBuilder.java
+++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/urlfactory/URLFactoryBuilder.java
@@ -100,6 +100,8 @@ public Optional createFactoryFromDevfile(
ex.getMessage());
continue;
} catch (DevfileException e) {
+ // must never happen as devfile location is always an absolute URL
+ LOG.error(e.getMessage(), e);
LOG.debug("Unexpected devfile exception: {}", e.getMessage());
throw new BadRequestException(
format(
diff --git a/wsmaster/pom.xml b/wsmaster/pom.xml
index 402fb791ca8..96c5d7264e0 100644
--- a/wsmaster/pom.xml
+++ b/wsmaster/pom.xml
@@ -26,6 +26,7 @@
che-core-api-auth-shared
che-core-api-auth
+ che-core-api-auth-bitbucket
che-core-api-auth-github
che-core-api-auth-openshift
che-core-api-workspace-shared