From d20ef75df87b17d649e28dc49051f75d89f3734f Mon Sep 17 00:00:00 2001 From: Sergii Kabashniuk Date: Wed, 20 Jan 2021 14:54:36 +0200 Subject: [PATCH] Obtain and persist Bitbucket personal access token as k8s secret Signed-off-by: Sergii Kabashniuk --- assembly/assembly-wsmaster-war/pom.xml | 4 + .../che/api/deploy/WsMasterModule.java | 2 + .../webapp/WEB-INF/classes/che/che.properties | 10 + .../infrastructure-factory/pom.xml | 5 - .../server/deploy/KeycloakServletModule.java | 2 +- pom.xml | 5 + wsmaster/che-core-api-auth-bitbucket/pom.xml | 65 ++++ .../che/security/oauth1/BitbucketModule.java | 31 ++ .../BitbucketServerOAuthAuthenticator.java | 41 +++ ...ucketServerOAuthAuthenticatorProvider.java | 67 ++++ .../oauth1/NoopOAuthAuthenticator.java | 46 +++ ...tServerOAuthAuthenticatorProviderTest.java | 83 +++++ .../src/test/resources/logback-test.xml | 26 ++ .../oauth1/OAuthAuthenticationService.java | 6 +- .../security/oauth1/OAuthAuthenticator.java | 20 +- .../pom.xml | 33 ++ .../bitbucket/BitbucketServerModule.java | 3 + ...ucketServerPersonalAccessTokenFetcher.java | 74 +++- .../server/AuthorizationHeaderSupplier.java | 20 ++ .../server/BitbucketPersonalAccessToken.java | 148 ++++++++ .../bitbucket/server/BitbucketServerApi.java | 107 ++++++ .../bitbucket/server/BitbucketUser.java | 146 ++++++++ .../server/HttpBitbucketServerApi.java | 328 ++++++++++++++++++ .../server/NopBitbucketServerApi.java | 74 ++++ .../factory/server/bitbucket/server/Page.java | 112 ++++++ .../oauth1/BitbucketServerApiProvider.java | 88 +++++ ...rverOAuth1AuthorizationHeaderSupplier.java | 54 +++ ...tServerPersonalAccessTokenFetcherTest.java | 193 +++++++++++ .../bitbucket/HttpBitbucketServerApiTest.java | 222 ++++++++++++ .../BitbucketServerApiProviderTest.java | 122 +++++++ ...OAuth1AuthorizationHeaderSupplierTest.java | 83 +++++ .../1.0/users/ksmster/newtoken.json | 26 ++ .../1.0/users/ksmster/response.json | 58 ++++ .../rest/api/1.0/users/filtered/response.json | 40 +++ .../rest/api/1.0/users/ksmster/response.json | 16 + .../rest/api/1.0/users/response.json | 56 +++ .../rest/api/1.0/users/response_s0_l25.json | 57 +++ .../rest/api/1.0/users/response_s3_l25.json | 57 +++ .../rest/api/1.0/users/response_s6_l25.json | 57 +++ .../rest/api/1.0/users/response_s9_l25.json | 24 ++ .../src/test/resources/logback-test.xml | 26 ++ .../scm/PersonalAccessTokenFetcher.java | 3 +- wsmaster/pom.xml | 1 + 43 files changed, 2628 insertions(+), 13 deletions(-) create mode 100644 wsmaster/che-core-api-auth-bitbucket/pom.xml create mode 100644 wsmaster/che-core-api-auth-bitbucket/src/main/java/org/eclipse/che/security/oauth1/BitbucketModule.java create mode 100644 wsmaster/che-core-api-auth-bitbucket/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerOAuthAuthenticator.java create mode 100644 wsmaster/che-core-api-auth-bitbucket/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerOAuthAuthenticatorProvider.java create mode 100644 wsmaster/che-core-api-auth-bitbucket/src/main/java/org/eclipse/che/security/oauth1/NoopOAuthAuthenticator.java create mode 100644 wsmaster/che-core-api-auth-bitbucket/src/test/java/org/eclipse/che/security/oauth1/BitbucketServerOAuthAuthenticatorProviderTest.java create mode 100644 wsmaster/che-core-api-auth-bitbucket/src/test/resources/logback-test.xml create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/AuthorizationHeaderSupplier.java create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/BitbucketPersonalAccessToken.java create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/BitbucketServerApi.java create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/BitbucketUser.java create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/HttpBitbucketServerApi.java create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/NopBitbucketServerApi.java create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/Page.java create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerApiProvider.java create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerOAuth1AuthorizationHeaderSupplier.java create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerPersonalAccessTokenFetcherTest.java create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/HttpBitbucketServerApiTest.java create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/security/oauth1/BitbucketServerApiProviderTest.java create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/security/oauth1/BitbucketServerOAuth1AuthorizationHeaderSupplierTest.java create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/access-tokens/1.0/users/ksmster/newtoken.json create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/access-tokens/1.0/users/ksmster/response.json create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/filtered/response.json create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/ksmster/response.json create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response.json create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response_s0_l25.json create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response_s3_l25.json create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response_s6_l25.json create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/__files/bitbucket/rest/api/1.0/users/response_s9_l25.json create mode 100644 wsmaster/che-core-api-factory-bitbucket-server/src/test/resources/logback-test.xml 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 b293762cad4..a59d3633c2b 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,16 @@ che.oauth.openshift.clientsecret=NULL che.oauth.openshift.oauth_endpoint= NULL che.oauth.openshift.verify_token_url= NULL +# Configuration of Bitbucket Server OAuth1 client. Used to obtain Personal access tokens. +# Location of the file with Bitbucket Server application consumer key (equivalent to a username). +che.oauth1.bitbucket.consumerkeypath=NULL +# Location of the file with Bitbucket Server application private key +che.oauth1.bitbucket.privatekeypath=NULL +# Bitbucket Server URL. To work correctly with factories the same URL +# has to be part of `che.integration.bitbucket.server_endpoints` too. +che.oauth1.bitbucket.endpoint=NULL + + ### Internal # Che extensions can be scheduled executions on a time basis. diff --git a/infrastructures/infrastructure-factory/pom.xml b/infrastructures/infrastructure-factory/pom.xml index 7d0d5c353ab..2f3650bf90c 100644 --- a/infrastructures/infrastructure-factory/pom.xml +++ b/infrastructures/infrastructure-factory/pom.xml @@ -68,11 +68,6 @@ logback-classic test - - com.github.tomakehurst - wiremock-jre8-standalone - test - org.mockito mockito-core 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..8851a46fb14 --- /dev/null +++ b/wsmaster/che-core-api-auth-bitbucket/pom.xml @@ -0,0 +1,65 @@ + + + + 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 + + + ch.qos.logback + logback-classic + test + + + org.testng + testng + test + + + 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..9366b084df2 --- /dev/null +++ b/wsmaster/che-core-api-auth-bitbucket/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerOAuthAuthenticator.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.security.oauth1; + +import com.google.inject.Singleton; + +/** + * OAuth1 authentication for Bitbucket Server account. + * + * @author Igor Vinokur + */ +@Singleton +public class BitbucketServerOAuthAuthenticator extends OAuthAuthenticator { + public static final String AUTHENTICATOR_NAME = "bitbucket-server"; + + public BitbucketServerOAuthAuthenticator( + String consumerKey, String privateKey, String bitbucketEndpoint, String apiEndpoint) { + super( + consumerKey, + bitbucketEndpoint + "/plugins/servlet/oauth/request-token", + bitbucketEndpoint + "/plugins/servlet/oauth/access-token", + bitbucketEndpoint + "/plugins/servlet/oauth/authorize", + apiEndpoint + "/oauth/1.0/callback", + null, + privateKey); + } + + @Override + public final String getOAuthProvider() { + return AUTHENTICATOR_NAME; + } +} 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..3898c05c4ae --- /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.debug("{} Bitbucket OAuthAuthenticator is used.", authenticator); + } + + @Override + public OAuthAuthenticator get() { + return authenticator; + } + + private static OAuthAuthenticator getOAuthAuthenticator( + String consumerKeyPath, String privateKeyPath, String bitbucketEndpoint, String apiEndpoint) + throws IOException { + if (!isNullOrEmpty(bitbucketEndpoint) + && !isNullOrEmpty(consumerKeyPath) + && !isNullOrEmpty(privateKeyPath)) { + String consumerKey = Files.readString(Path.of(consumerKeyPath)); + String privateKey = Files.readString(Path.of(privateKeyPath)); + if (!isNullOrEmpty(consumerKey) && !isNullOrEmpty(privateKey)) { + return new BitbucketServerOAuthAuthenticator( + consumerKey, privateKey, bitbucketEndpoint, apiEndpoint); + } + } + + return new NoopOAuthAuthenticator(); + } +} 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..f4dc9a5bb7e --- /dev/null +++ b/wsmaster/che-core-api-auth-bitbucket/src/main/java/org/eclipse/che/security/oauth1/NoopOAuthAuthenticator.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 java.net.URL; + +/** + * Dummy implementation of @{@link OAuthAuthenticator} used in the case if no Bitbucket Server + * integration is configured. + */ +public class NoopOAuthAuthenticator extends OAuthAuthenticator { + protected NoopOAuthAuthenticator() { + super(null, null, null, null, null, null, null); + } + + @Override + String getOAuthProvider() { + return "Noop"; + } + + @Override + String getAuthenticateUrl(URL requestUrl, String requestMethod, String signatureMethod) + throws OAuthAuthenticationException { + throw new RuntimeException("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-auth-bitbucket/src/test/java/org/eclipse/che/security/oauth1/BitbucketServerOAuthAuthenticatorProviderTest.java b/wsmaster/che-core-api-auth-bitbucket/src/test/java/org/eclipse/che/security/oauth1/BitbucketServerOAuthAuthenticatorProviderTest.java new file mode 100644 index 00000000000..1a13932e699 --- /dev/null +++ b/wsmaster/che-core-api-auth-bitbucket/src/test/java/org/eclipse/che/security/oauth1/BitbucketServerOAuthAuthenticatorProviderTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.security.oauth1; + +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import com.google.common.io.Files; +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class BitbucketServerOAuthAuthenticatorProviderTest { + private File cfgFile; + private File emptyFile; + + @BeforeClass + public void setup() throws IOException { + cfgFile = File.createTempFile("BitbucketServerOAuthAuthenticatorProviderTest-", "-cfg"); + Files.asCharSink(cfgFile, Charset.defaultCharset()).write("tmp-data"); + cfgFile.deleteOnExit(); + emptyFile = File.createTempFile("BitbucketServerOAuthAuthenticatorProviderTest-", "-empty"); + emptyFile.deleteOnExit(); + } + + @Test(dataProvider = "noopConfig") + public void shouldProvideNoopOAuthAuthenticatorIfSomeConfigurationIsNotSet( + String consumerKeyPath, String privateKeyPath, String bitbucketEndpoint) throws IOException { + // given + BitbucketServerOAuthAuthenticatorProvider provider = + new BitbucketServerOAuthAuthenticatorProvider( + consumerKeyPath, privateKeyPath, bitbucketEndpoint, "http://che.server.com"); + // when + OAuthAuthenticator actual = provider.get(); + // then + assertNotNull(actual); + assertTrue(NoopOAuthAuthenticator.class.isAssignableFrom(actual.getClass())); + } + + @Test + public void shouldBeAbleToConfigureValidBitbucketServerOAuthAuthenticator() throws IOException { + // given + BitbucketServerOAuthAuthenticatorProvider provider = + new BitbucketServerOAuthAuthenticatorProvider( + cfgFile.getPath(), cfgFile.getPath(), "http://bitubucket.com", "http://che.server.com"); + // when + OAuthAuthenticator actual = provider.get(); + // then + assertNotNull(actual); + assertTrue(BitbucketServerOAuthAuthenticator.class.isAssignableFrom(actual.getClass())); + } + + @DataProvider(name = "noopConfig") + public Object[][] noopConfig() { + return new Object[][] { + {null, null, null}, + {cfgFile.getPath(), null, null}, + {null, cfgFile.getPath(), null}, + {cfgFile.getPath(), cfgFile.getPath(), null}, + {emptyFile.getPath(), null, null}, + {null, emptyFile.getPath(), null}, + {emptyFile.getPath(), emptyFile.getPath(), null}, + {cfgFile.getPath(), emptyFile.getPath(), null}, + {emptyFile.getPath(), cfgFile.getPath(), null}, + {emptyFile.getPath(), emptyFile.getPath(), "http://bitubucket.com"}, + {cfgFile.getPath(), emptyFile.getPath(), "http://bitubucket.com"}, + {emptyFile.getPath(), cfgFile.getPath(), "http://bitubucket.com"}, + {null, null, "http://bitubucket.com"} + }; + } +} diff --git a/wsmaster/che-core-api-auth-bitbucket/src/test/resources/logback-test.xml b/wsmaster/che-core-api-auth-bitbucket/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..2250aaa5aa7 --- /dev/null +++ b/wsmaster/che-core-api-auth-bitbucket/src/test/resources/logback-test.xml @@ -0,0 +1,26 @@ + + + + + + + %-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n + + + + + + + diff --git a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth1/OAuthAuthenticationService.java b/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth1/OAuthAuthenticationService.java index 103e3e68c5e..8d32d9b8c20 100644 --- a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth1/OAuthAuthenticationService.java +++ b/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth1/OAuthAuthenticationService.java @@ -28,6 +28,7 @@ import javax.ws.rs.core.Response; import org.eclipse.che.api.core.BadRequestException; import org.eclipse.che.api.core.rest.Service; +import org.eclipse.che.commons.env.EnvironmentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -82,17 +83,16 @@ public Response callback() throws OAuthAuthenticationException, BadRequestExcept @Path("signature") public String signature( @QueryParam("oauth_provider") String providerName, - @QueryParam("user_id") String userId, @QueryParam("request_url") String requestUrl, @QueryParam("request_method") String requestMethod) throws OAuthAuthenticationException, BadRequestException { requiredNotNull(providerName, "Provider name"); - requiredNotNull(userId, "User Id"); requiredNotNull(requestUrl, "Request url"); requiredNotNull(requestMethod, "Request method"); return getAuthenticator(providerName) - .computeAuthorizationHeader(userId, requestMethod, requestUrl); + .computeAuthorizationHeader( + EnvironmentContext.getCurrent().getSubject().getUserId(), requestMethod, requestUrl); } private OAuthAuthenticator getAuthenticator(String oauthProviderName) throws BadRequestException { diff --git a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth1/OAuthAuthenticator.java b/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth1/OAuthAuthenticator.java index 727f4209565..4d01569dfdd 100644 --- a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth1/OAuthAuthenticator.java +++ b/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth1/OAuthAuthenticator.java @@ -39,6 +39,7 @@ import java.util.concurrent.locks.ReentrantLock; import org.eclipse.che.api.auth.shared.dto.OAuthToken; import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.commons.env.EnvironmentContext; /** * Authentication service which allows get access token from OAuth provider site. @@ -105,7 +106,24 @@ String getAuthenticateUrl( throws OAuthAuthenticationException { try { final GenericUrl callbackUrl = new GenericUrl(redirectUri); - callbackUrl.put(STATE_PARAM_KEY, requestUrl.getQuery()); + String userId = getParameterFromState(requestUrl.getQuery(), USER_ID_PARAM_KEY); + String currentUserId = EnvironmentContext.getCurrent().getSubject().getUserId(); + if (userId != null) { + if (currentUserId.equals(userId)) { + callbackUrl.put(STATE_PARAM_KEY, requestUrl.getQuery()); + } else { + throw new OAuthAuthenticationException( + "Provided query parameter " + + USER_ID_PARAM_KEY + + "=" + + userId + + " do not much current user id: " + + currentUserId); + } + } else { + callbackUrl.put( + STATE_PARAM_KEY, requestUrl.getQuery() + "&" + USER_ID_PARAM_KEY + "=" + currentUserId); + } OAuthGetTemporaryToken temporaryToken; if (requestMethod != null && "post".equalsIgnoreCase(requestMethod)) { 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..2b6180eb5d6 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.debug("not a 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.debug("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.debug("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.debug("Token created = {} for {}", token.getId(), token.getUser()); + return new PersonalAccessToken( + scmServerUrl, + EnvironmentContext.getCurrent().getSubject().getUserId(), + user.getName(), + valueOf(user.getId()), + token.getName(), + valueOf(token.getId()), + token.getToken()); + } catch (ScmBadRequestException | ScmItemNotFoundException e) { + throw new ScmCommunicationException(e.getMessage(), e); + } } } diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/AuthorizationHeaderSupplier.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/AuthorizationHeaderSupplier.java new file mode 100644 index 00000000000..178021fa0d0 --- /dev/null +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/AuthorizationHeaderSupplier.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 AuthorizationHeaderSupplier { + 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..8733efd2e5a --- /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,148 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.api.factory.server.bitbucket.server; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.Objects; +import java.util.Set; + +@JsonInclude(JsonInclude.Include.NON_DEFAULT) +public class BitbucketPersonalAccessToken { + private long id; + private long createdDate; + private long lastAuthenticated; + private String name; + private String token; + private BitbucketUser user; + private Set 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 + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BitbucketPersonalAccessToken that = (BitbucketPersonalAccessToken) o; + return id == that.id + && createdDate == that.createdDate + && lastAuthenticated == that.lastAuthenticated + && Objects.equals(name, that.name) + && Objects.equals(token, that.token) + && Objects.equals(user, that.user) + && Objects.equals(permissions, that.permissions); + } + + @Override + public int hashCode() { + return Objects.hash(id, createdDate, lastAuthenticated, name, token, user, permissions); + } +} 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..7d5cae262d1 --- /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,107 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.api.factory.server.bitbucket.server; + +import java.util.List; +import java.util.Set; +import org.eclipse.che.api.factory.server.scm.exception.ScmBadRequestException; +import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException; +import org.eclipse.che.api.factory.server.scm.exception.ScmItemNotFoundException; +import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException; +import org.eclipse.che.commons.subject.Subject; + +/** Bitbucket Server API client. */ +public interface BitbucketServerApi { + /** + * @param bitbucketServerUrl + * @return - true if client is connected to the given bitbucket server. + */ + boolean isConnected(String bitbucketServerUrl); + /** + * @param cheUser - Che user. + * @return - {@link BitbucketUser} that is linked with given {@link Subject} + * @throws ScmUnauthorizedException - in case if {@link Subject} is not linked to any {@link + * BitbucketUser} + */ + BitbucketUser getUser(Subject cheUser) throws ScmUnauthorizedException, ScmCommunicationException; + + /** + * @param slug + * @return - Retrieve the {@link BitbucketUser} matching the supplied userSlug. + * @throws ScmItemNotFoundException + * @throws ScmUnauthorizedException + * @throws ScmCommunicationException + */ + BitbucketUser getUser(String slug) + throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException; + + /** + * @return Retrieve a list of {@link BitbucketUser}. Only authenticated users may call this + * resource. + * @throws ScmBadRequestException + * @throws ScmUnauthorizedException + * @throws ScmCommunicationException + */ + List 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/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/HttpBitbucketServerApi.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/HttpBitbucketServerApi.java new file mode 100644 index 00000000000..579fa719bf7 --- /dev/null +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/HttpBitbucketServerApi.java @@ -0,0 +1,328 @@ +/* + * 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.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.google.common.base.Charsets; +import com.google.common.base.Strings; +import com.google.common.io.CharStreams; +import com.google.common.net.HttpHeaders; +import 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; + +/** + * Implementation of @{@link BitbucketServerApi} that is using @{@link HttpClient} to communicate + * with Bitbucket Server.s + */ +public class HttpBitbucketServerApi implements BitbucketServerApi { + + private static final ObjectMapper OM = new ObjectMapper(); + + private static final Logger LOG = LoggerFactory.getLogger(HttpBitbucketServerApi.class); + private final URI serverUri; + private final AuthorizationHeaderSupplier headerProvider; + + @Inject + public HttpBitbucketServerApi( + String serverUrl, AuthorizationHeaderSupplier authorizationHeaderSupplier) { + this.serverUri = URI.create(serverUrl); + this.headerProvider = authorizationHeaderSupplier; + } + + @Override + public boolean isConnected(String bitbucketServerUrl) { + return serverUri.equals(URI.create(bitbucketServerUrl)); + } + + @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 { + LOG.debug("executeRequest={}", request); + return executeRequest( + httpClient, + request, + inputStream -> { + try { + return OM.readValue(inputStream, BitbucketUser.class); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } catch (ScmBadRequestException e) { + throw new ScmCommunicationException(e.getMessage(), e); + } + } + + @Override + public List 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 { + LOG.debug("executeRequest={}", request); + executeRequest( + httpClient, + request, + inputStream -> { + try { + return OM.readValue(inputStream, String.class); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } catch (ScmBadRequestException e) { + throw new ScmCommunicationException(e.getMessage(), e); + } + } + + @Override + public BitbucketPersonalAccessToken createPersonalAccessTokens( + String userSlug, String tokenName, Set permissions) + throws ScmBadRequestException, ScmUnauthorizedException, ScmCommunicationException { + HttpClient httpClient = HttpClient.newHttpClient(); + URI uri = serverUri.resolve("/rest/access-tokens/1.0/users/" + userSlug); + + try { + HttpRequest request = + HttpRequest.newBuilder(uri) + .PUT( + HttpRequest.BodyPublishers.ofString( + OM.writeValueAsString( + new BitbucketPersonalAccessToken(tokenName, permissions)))) + .headers( + HttpHeaders.AUTHORIZATION, + headerProvider.computeAuthorizationHeader("PUT", uri.toString()), + HttpHeaders.ACCEPT, + MediaType.APPLICATION_JSON, + HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_JSON) + .timeout(ofSeconds(10)) + .build(); + LOG.debug("executeRequest={}", request); + return executeRequest( + httpClient, + request, + inputStream -> { + try { + return OM.readValue(inputStream, BitbucketPersonalAccessToken.class); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } catch (ScmItemNotFoundException | JsonProcessingException e) { + throw new ScmCommunicationException(e.getMessage(), e); + } + } + + @Override + public List 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 { + + for (String userSlug : userSlugs) { + 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(); + LOG.debug("executeRequest={}", request); + final JavaType typeReference = + TypeFactory.defaultInstance().constructParametricType(Page.class, tClass); + return executeRequest( + httpClient, + request, + inputStream -> { + try { + return OM.readValue(inputStream, typeReference); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + + private T executeRequest( + HttpClient httpClient, HttpRequest request, Function function) + throws ScmBadRequestException, ScmItemNotFoundException, ScmCommunicationException, + ScmUnauthorizedException { + try { + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); + LOG.debug("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/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..6ca1d9fbf78 --- /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,74 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.api.factory.server.bitbucket.server; + +import java.util.List; +import java.util.Set; +import org.eclipse.che.api.factory.server.scm.exception.ScmBadRequestException; +import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException; +import org.eclipse.che.api.factory.server.scm.exception.ScmItemNotFoundException; +import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException; +import org.eclipse.che.commons.subject.Subject; + +/** + * Implementation of @{@link BitbucketServerApi} that is going to be deployed in container in case + * if no integration with Bitbucket server is needed. + */ +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..2880e5dfa96 --- /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,112 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.api.factory.server.bitbucket.server; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Objects; + +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 + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Page page = (Page) o; + return start == page.start + && size == page.size + && limit == page.limit + && isLastPage == page.isLastPage + && nextPageStart == page.nextPageStart + && Objects.equals(values, page.values); + } + + @Override + public int hashCode() { + return Objects.hash(start, size, limit, isLastPage, nextPageStart, values); + } +} 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..857b44b3cf8 --- /dev/null +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerApiProvider.java @@ -0,0 +1,88 @@ +/* + * 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.HttpBitbucketServerApi; +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 bitbucketOauth1Endpoint, + Set authenticators) { + bitbucketServerApi = doGet(bitbucketEndpoints, bitbucketOauth1Endpoint, authenticators); + LOG.debug("Bitbucket server api is used {}", bitbucketServerApi); + } + + @Override + public BitbucketServerApi get() { + return bitbucketServerApi; + } + + private static BitbucketServerApi doGet( + String bitbucketEndpoints, + String bitbucketOauth1Endpoint, + Set authenticators) { + if (isNullOrEmpty(bitbucketOauth1Endpoint)) { + return new NopBitbucketServerApi(); + } else { + if (isNullOrEmpty(bitbucketEndpoints)) { + throw new RuntimeException( + "`che.integration.bitbucket.server_endpoints` bitbucket configuration is missing." + + " It should contain values from 'che.oauth1.bitbucket.endpoint'"); + } else { + if (bitbucketEndpoints.contains(bitbucketOauth1Endpoint)) { + Optional authenticator = + authenticators + .stream() + .filter( + a -> + a.getOAuthProvider() + .equals(BitbucketServerOAuthAuthenticator.AUTHENTICATOR_NAME)) + .filter( + a -> BitbucketServerOAuthAuthenticator.class.isAssignableFrom(a.getClass())) + .findFirst(); + if (authenticator.isEmpty()) { + throw new RuntimeException( + "'che.oauth1.bitbucket.endpoint' is set but BitbucketServerOAuthAuthenticator deployed correctly"); + } + return new HttpBitbucketServerApi( + bitbucketOauth1Endpoint, + new BitbucketServerOAuth1AuthorizationHeaderSupplier( + (BitbucketServerOAuthAuthenticator) authenticator.get())); + } else { + throw new RuntimeException( + "`che.integration.bitbucket.server_endpoints` mast contain `" + + bitbucketOauth1Endpoint + + "` value"); + } + } + } + } +} diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerOAuth1AuthorizationHeaderSupplier.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerOAuth1AuthorizationHeaderSupplier.java new file mode 100644 index 00000000000..05156a227c6 --- /dev/null +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/security/oauth1/BitbucketServerOAuth1AuthorizationHeaderSupplier.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.security.oauth1; + +import com.google.common.base.Strings; +import javax.inject.Inject; +import org.eclipse.che.api.factory.server.bitbucket.server.AuthorizationHeaderSupplier; +import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException; +import org.eclipse.che.commons.env.EnvironmentContext; +import org.eclipse.che.commons.subject.Subject; + +/** + * Implementation of @{@link AuthorizationHeaderSupplier} that is used @{@link + * BitbucketServerOAuthAuthenticator} to compute authorization headers. + */ +public class BitbucketServerOAuth1AuthorizationHeaderSupplier + implements AuthorizationHeaderSupplier { + private final BitbucketServerOAuthAuthenticator authenticator; + + @Inject + public BitbucketServerOAuth1AuthorizationHeaderSupplier( + BitbucketServerOAuthAuthenticator authenticator) { + this.authenticator = authenticator; + } + + @Override + public String computeAuthorizationHeader(String requestMethod, String requestUrl) + throws ScmUnauthorizedException { + 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/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 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/java/org/eclipse/che/api/factory/server/bitbucket/HttpBitbucketServerApiTest.java b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/HttpBitbucketServerApiTest.java new file mode 100644 index 00000000000..c886362c566 --- /dev/null +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/HttpBitbucketServerApiTest.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.BitbucketUser; +import org.eclipse.che.api.factory.server.bitbucket.server.HttpBitbucketServerApi; +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 HttpBitbucketServerApiTest { + 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 HttpBitbucketServerApi( + 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/security/oauth1/BitbucketServerApiProviderTest.java b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/security/oauth1/BitbucketServerApiProviderTest.java new file mode 100644 index 00000000000..0d45ec7ee63 --- /dev/null +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/security/oauth1/BitbucketServerApiProviderTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.security.oauth1; + +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.util.Collections; +import java.util.Set; +import org.eclipse.che.api.factory.server.bitbucket.server.BitbucketServerApi; +import org.eclipse.che.api.factory.server.bitbucket.server.HttpBitbucketServerApi; +import org.eclipse.che.api.factory.server.bitbucket.server.NopBitbucketServerApi; +import org.eclipse.che.commons.annotation.Nullable; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class BitbucketServerApiProviderTest { + BitbucketServerOAuthAuthenticator oAuthAuthenticator; + + @BeforeClass + public void setUp() { + oAuthAuthenticator = + new BitbucketServerOAuthAuthenticator( + "df", "private", " https://bitbucket2.server.com", " https://che.server.com"); + } + + @Test + public void shouldBeAbleToCreateBitbucketServerApi() { + // given + BitbucketServerApiProvider bitbucketServerApiProvider = + new BitbucketServerApiProvider( + "https://bitbucket.server.com, https://bitbucket2.server.com", + "https://bitbucket.server.com", + ImmutableSet.of(oAuthAuthenticator)); + // when + BitbucketServerApi actual = bitbucketServerApiProvider.get(); + // then + assertNotNull(actual); + assertTrue(HttpBitbucketServerApi.class.isAssignableFrom(actual.getClass())); + } + + @Test(dataProvider = "noopConfig") + public void shouldProvideNoopOAuthAuthenticatorIfSomeConfigurationIsNotSet( + @Nullable String bitbucketEndpoints, + @Nullable String bitbucketOauth1Endpoint, + Set authenticators) + throws IOException { + // given + BitbucketServerApiProvider bitbucketServerApiProvider = + new BitbucketServerApiProvider(bitbucketEndpoints, bitbucketOauth1Endpoint, authenticators); + // when + BitbucketServerApi actual = bitbucketServerApiProvider.get(); + // then + assertNotNull(actual); + assertTrue(NopBitbucketServerApi.class.isAssignableFrom(actual.getClass())); + } + + @Test( + expectedExceptions = RuntimeException.class, + expectedExceptionsMessageRegExp = + "`che.integration.bitbucket.server_endpoints` bitbucket configuration is missing. It should contain values from 'che.oauth1.bitbucket.endpoint'") + public void shouldFailToBuildIfEndpointsAreMisconfigured() { + // given + // when + BitbucketServerApiProvider bitbucketServerApiProvider = + new BitbucketServerApiProvider( + "", "https://bitbucket.server.com", ImmutableSet.of(oAuthAuthenticator)); + } + + @Test( + expectedExceptions = RuntimeException.class, + expectedExceptionsMessageRegExp = + "'che.oauth1.bitbucket.endpoint' is set but BitbucketServerOAuthAuthenticator deployed correctly") + public void shouldFailToBuildIfEndpointsAreMisconfigured2() { + // given + // when + BitbucketServerApiProvider bitbucketServerApiProvider = + new BitbucketServerApiProvider( + "https://bitbucket.server.com, https://bitbucket2.server.com", + "https://bitbucket.server.com", + Collections.emptySet()); + } + + @Test( + expectedExceptions = RuntimeException.class, + expectedExceptionsMessageRegExp = + "`che.integration.bitbucket.server_endpoints` mast contain `https://bitbucket.server.com` value") + public void shouldFailToBuildIfEndpointsAreMisconfigured3() { + // given + // when + BitbucketServerApiProvider bitbucketServerApiProvider = + new BitbucketServerApiProvider( + "https://bitbucket3.server.com, https://bitbucket2.server.com", + "https://bitbucket.server.com", + ImmutableSet.of(oAuthAuthenticator)); + } + + @DataProvider(name = "noopConfig") + public Object[][] noopConfig() { + return new Object[][] { + {null, null, null}, + {"https://bitbucket.server.com, https://bitbucket2.server.com", null, null}, + { + "https://bitbucket.server.com, https://bitbucket2.server.com", + null, + ImmutableSet.of(oAuthAuthenticator) + } + }; + } +} diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/security/oauth1/BitbucketServerOAuth1AuthorizationHeaderSupplierTest.java b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/security/oauth1/BitbucketServerOAuth1AuthorizationHeaderSupplierTest.java new file mode 100644 index 00000000000..7356a1667ae --- /dev/null +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/security/oauth1/BitbucketServerOAuth1AuthorizationHeaderSupplierTest.java @@ -0,0 +1,83 @@ +package org.eclipse.che.security.oauth1; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; + +import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException; +import org.eclipse.che.commons.env.EnvironmentContext; +import org.eclipse.che.commons.subject.Subject; +import org.eclipse.che.commons.subject.SubjectImpl; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +@Listeners(MockitoTestNGListener.class) +public class BitbucketServerOAuth1AuthorizationHeaderSupplierTest { + @Mock BitbucketServerOAuthAuthenticator authenticator; + @InjectMocks BitbucketServerOAuth1AuthorizationHeaderSupplier supplier; + + Subject subject = new SubjectImpl("user", "234234", "t234234", false); + + @BeforeMethod + public void setUp() { + EnvironmentContext.getCurrent().setSubject(subject); + } + + @Test + public void shouldBeAbleToComputeAuthorizationHeader() + throws ScmUnauthorizedException, OAuthAuthenticationException { + // given + when(authenticator.computeAuthorizationHeader( + eq(subject.getUserId()), eq("POST"), eq("/api/user"))) + .thenReturn("signature"); + // when + String actual = supplier.computeAuthorizationHeader("POST", "/api/user"); + // then + assertEquals(actual, "signature"); + } + + @Test( + expectedExceptions = ScmUnauthorizedException.class, + expectedExceptionsMessageRegExp = + "user is not authorized in bitbucket-server OAuth1 provider") + public void shouldThrowScmUnauthorizedExceptionIfHeaderIsNull() + throws OAuthAuthenticationException, ScmUnauthorizedException { + // given + when(authenticator.computeAuthorizationHeader( + eq(subject.getUserId()), eq("POST"), eq("/api/user"))) + .thenReturn(null); + // when + supplier.computeAuthorizationHeader("POST", "/api/user"); + } + + @Test( + expectedExceptions = ScmUnauthorizedException.class, + expectedExceptionsMessageRegExp = + "user is not authorized in bitbucket-server OAuth1 provider") + public void shouldThrowScmUnauthorizedExceptionIfHeaderIsEmpty() + throws OAuthAuthenticationException, ScmUnauthorizedException { + // given + when(authenticator.computeAuthorizationHeader( + eq(subject.getUserId()), eq("POST"), eq("/api/user"))) + .thenReturn(""); + // when + supplier.computeAuthorizationHeader("POST", "/api/user"); + } + + @Test( + expectedExceptions = ScmUnauthorizedException.class, + expectedExceptionsMessageRegExp = "this is a message") + public void shouldThrowScmUnauthorizedExceptionOnOAuthAuthenticationException() + throws OAuthAuthenticationException, ScmUnauthorizedException { + // given + when(authenticator.computeAuthorizationHeader( + eq(subject.getUserId()), eq("POST"), eq("/api/user"))) + .thenThrow(new OAuthAuthenticationException("this is a message")); + // when + supplier.computeAuthorizationHeader("POST", "/api/user"); + } +} 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..2ce6b01e9a4 --- /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/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