From c1c40f66953111e861c7773a46abdc4312b57855 Mon Sep 17 00:00:00 2001 From: ivinokur Date: Wed, 23 Oct 2024 09:29:19 +0300 Subject: [PATCH] feat: Allow to configure 2 gitlab providers simultaneously --- .../che/api/deploy/WsMasterModule.java | 6 + .../webapp/WEB-INF/classes/che/che.properties | 12 +- pom.xml | 10 + .../che-core-api-auth-gitlab-common/pom.xml | 69 +++++ ...tractGitLabOAuthAuthenticatorProvider.java | 80 ++++++ .../oauth/GitLabOAuthAuthenticator.java | 10 +- .../che/security/oauth/GitLabUser.java | 2 +- wsmaster/che-core-api-auth-gitlab/pom.xml | 14 +- .../che/security/oauth/GitLabModule.java | 3 +- .../GitLabOAuthAuthenticatorProvider.java | 51 +--- ...itLabOAuthAuthenticatorProviderSecond.java | 39 +++ .../oauth/GitLabAuthenticatorTest.java | 4 +- .../pom.xml | 141 +++++++++++ ...stractGitlabFactoryParametersResolver.java | 127 ++++++++++ .../AbstractGitlabOAuthTokenFetcher.java | 223 +++++++++++++++++ .../gitlab/AbstractGitlabScmFileResolver.java | 75 ++++++ .../gitlab/AbstractGitlabUrlParser.java | 206 +++++++++++++++ .../gitlab/AbstractGitlabUserDataFetcher.java | 71 ++++++ .../server/gitlab/GitlabApiClient.java | 0 .../GitlabAuthorizingFileContentProvider.java | 2 +- .../server/gitlab/GitlabOauthTokenInfo.java | 2 +- .../gitlab/GitlabPersonalAccessTokenInfo.java | 2 +- .../api/factory/server/gitlab/GitlabUrl.java | 2 +- .../api/factory/server/gitlab/GitlabUser.java | 2 +- wsmaster/che-core-api-factory-gitlab/pom.xml | 24 +- .../GitlabFactoryParametersResolver.java | 100 +------- ...GitlabFactoryParametersResolverSecond.java | 48 ++++ .../factory/server/gitlab/GitlabModule.java | 5 +- .../gitlab/GitlabOAuthTokenFetcher.java | 235 +----------------- .../gitlab/GitlabOAuthTokenFetcherSecond.java | 31 +++ .../server/gitlab/GitlabScmFileResolver.java | 55 +--- .../gitlab/GitlabScmFileResolverSecond.java | 28 +++ .../server/gitlab/GitlabUrlParser.java | 180 +------------- .../server/gitlab/GitlabUrlParserSecond.java | 36 +++ .../server/gitlab/GitlabUserDataFetcher.java | 78 +----- .../gitlab/GitlabUserDataFetcherSecond.java | 32 +++ .../gitlab/GitlabOAuthTokenFetcherTest.java | 15 +- .../server/gitlab/GitlabUrlParserTest.java | 8 +- .../gitlab/GitlabUserDataFetcherTest.java | 5 +- wsmaster/pom.xml | 2 + 40 files changed, 1294 insertions(+), 741 deletions(-) create mode 100644 wsmaster/che-core-api-auth-gitlab-common/pom.xml create mode 100644 wsmaster/che-core-api-auth-gitlab-common/src/main/java/org/eclipse/che/security/oauth/AbstractGitLabOAuthAuthenticatorProvider.java rename wsmaster/{che-core-api-auth-gitlab => che-core-api-auth-gitlab-common}/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticator.java (94%) rename wsmaster/{che-core-api-auth-gitlab => che-core-api-auth-gitlab-common}/src/main/java/org/eclipse/che/security/oauth/GitLabUser.java (96%) create mode 100644 wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProviderSecond.java create mode 100644 wsmaster/che-core-api-factory-gitlab-common/pom.xml create mode 100644 wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabFactoryParametersResolver.java create mode 100644 wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabOAuthTokenFetcher.java create mode 100644 wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabScmFileResolver.java create mode 100644 wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabUrlParser.java create mode 100644 wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabUserDataFetcher.java rename wsmaster/{che-core-api-factory-gitlab => che-core-api-factory-gitlab-common}/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabApiClient.java (100%) rename wsmaster/{che-core-api-factory-gitlab => che-core-api-factory-gitlab-common}/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabAuthorizingFileContentProvider.java (96%) rename wsmaster/{che-core-api-factory-gitlab => che-core-api-factory-gitlab-common}/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabOauthTokenInfo.java (98%) rename wsmaster/{che-core-api-factory-gitlab => che-core-api-factory-gitlab-common}/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabPersonalAccessTokenInfo.java (98%) rename wsmaster/{che-core-api-factory-gitlab => che-core-api-factory-gitlab-common}/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrl.java (99%) rename wsmaster/{che-core-api-factory-gitlab => che-core-api-factory-gitlab-common}/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUser.java (98%) create mode 100644 wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabFactoryParametersResolverSecond.java create mode 100644 wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabOAuthTokenFetcherSecond.java create mode 100644 wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabScmFileResolverSecond.java create mode 100644 wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrlParserSecond.java create mode 100644 wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUserDataFetcherSecond.java 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 965dfec5cf7..c0f0cf18acb 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 @@ -49,7 +49,9 @@ import org.eclipse.che.api.factory.server.github.GithubScmFileResolver; import org.eclipse.che.api.factory.server.github.GithubScmFileResolverSecond; import org.eclipse.che.api.factory.server.gitlab.GitlabFactoryParametersResolver; +import org.eclipse.che.api.factory.server.gitlab.GitlabFactoryParametersResolverSecond; import org.eclipse.che.api.factory.server.gitlab.GitlabScmFileResolver; +import org.eclipse.che.api.factory.server.gitlab.GitlabScmFileResolverSecond; import org.eclipse.che.api.system.server.ServiceTermination; import org.eclipse.che.api.system.server.SystemModule; import org.eclipse.che.api.user.server.NotImplementedTokenValidator; @@ -157,6 +159,9 @@ protected void configure() { .addBinding() .to(BitbucketServerAuthorizingFactoryParametersResolver.class); factoryParametersResolverMultibinder.addBinding().to(GitlabFactoryParametersResolver.class); + factoryParametersResolverMultibinder + .addBinding() + .to(GitlabFactoryParametersResolverSecond.class); factoryParametersResolverMultibinder.addBinding().to(BitbucketFactoryParametersResolver.class); factoryParametersResolverMultibinder .addBinding() @@ -172,6 +177,7 @@ protected void configure() { scmFileResolverResolverMultibinder.addBinding().to(GithubScmFileResolverSecond.class); scmFileResolverResolverMultibinder.addBinding().to(BitbucketScmFileResolver.class); scmFileResolverResolverMultibinder.addBinding().to(GitlabScmFileResolver.class); + scmFileResolverResolverMultibinder.addBinding().to(GitlabScmFileResolverSecond.class); scmFileResolverResolverMultibinder.addBinding().to(BitbucketServerScmFileResolver.class); scmFileResolverResolverMultibinder.addBinding().to(AzureDevOpsScmFileResolver.class); scmFileResolverResolverMultibinder.addBinding().to(GitSshScmFileResolver.class); 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 d6b2b544c49..8a27612ca33 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 @@ -647,19 +647,27 @@ che.workspace.devfile.async.storage.plugin=eclipse/che-async-pv-plugin/latest che.integration.bitbucket.server_endpoints=NULL # GitLab endpoints used for factory integrations. -# A comma separated list of GitLab server URLs or `NULL` if no integration is expected. -che.integration.gitlab.server_endpoints=NULL # The address of the GitLab server with configured OAuth 2 integration. che.integration.gitlab.oauth_endpoint=NULL +# The address of the GitLab server with configured OAuth 2 integration. (The second GitLab instance). +che.integration.gitlab.oauth_endpoint_2=NULL + # Configuration of GitLab OAuth2 client. Used to obtain personal access tokens. # Location of the file with GitLab client ID. che.oauth2.gitlab.clientid_filepath=NULL +# Configuration of GitLab OAuth2 client. Used to obtain personal access tokens. +# Location of the file with GitLab client ID. (The second GitLab instance). +che.oauth2.gitlab.clientid_filepath_2=NULL + # Location of the file with GitLab client secret. che.oauth2.gitlab.clientsecret_filepath=NULL +# Location of the file with GitLab client secret. (The second GitLab instance). +che.oauth2.gitlab.clientsecret_filepath_2=NULL + ### Advanced authorization # Comma separated list of users allowed to access Che. che.infra.kubernetes.advanced_authorization.allow_users=NULL diff --git a/pom.xml b/pom.xml index 7deff6f2f70..0e45c55663e 100644 --- a/pom.xml +++ b/pom.xml @@ -706,6 +706,11 @@ che-core-api-auth-gitlab ${che.version} + + org.eclipse.che.core + che-core-api-auth-gitlab-common + ${che.version} + org.eclipse.che.core che-core-api-auth-openshift @@ -793,6 +798,11 @@ che-core-api-factory-gitlab ${che.version} + + org.eclipse.che.core + che-core-api-factory-gitlab-common + ${che.version} + org.eclipse.che.core che-core-api-factory-shared diff --git a/wsmaster/che-core-api-auth-gitlab-common/pom.xml b/wsmaster/che-core-api-auth-gitlab-common/pom.xml new file mode 100644 index 00000000000..302825ec9e1 --- /dev/null +++ b/wsmaster/che-core-api-auth-gitlab-common/pom.xml @@ -0,0 +1,69 @@ + + + + 4.0.0 + + che-master-parent + org.eclipse.che.core + 7.94.0-SNAPSHOT + + che-core-api-auth-gitlab-common + jar + Che Core :: API :: Authentication GitLab Common + + + com.google.guava + guava + + + com.google.http-client + google-http-client + + + jakarta.inject + jakarta.inject-api + + + org.eclipse.che.core + che-core-api-auth + + + org.eclipse.che.core + che-core-api-auth-shared + + + org.eclipse.che.core + che-core-commons-json + + + org.eclipse.che.core + che-core-commons-lang + + + org.slf4j + slf4j-api + + + org.testng + testng + test + + + org.wiremock + wiremock-standalone + test + + + diff --git a/wsmaster/che-core-api-auth-gitlab-common/src/main/java/org/eclipse/che/security/oauth/AbstractGitLabOAuthAuthenticatorProvider.java b/wsmaster/che-core-api-auth-gitlab-common/src/main/java/org/eclipse/che/security/oauth/AbstractGitLabOAuthAuthenticatorProvider.java new file mode 100644 index 00000000000..50389add71f --- /dev/null +++ b/wsmaster/che-core-api-auth-gitlab-common/src/main/java/org/eclipse/che/security/oauth/AbstractGitLabOAuthAuthenticatorProvider.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2012-2024 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.oauth; + +import static com.google.common.base.Strings.isNullOrEmpty; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import javax.inject.Provider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides implementation of GitLab {@link OAuthAuthenticator} based on available configuration. + * + * @author Pavol Baran + */ +public class AbstractGitLabOAuthAuthenticatorProvider implements Provider { + private static final Logger LOG = + LoggerFactory.getLogger(AbstractGitLabOAuthAuthenticatorProvider.class); + private final OAuthAuthenticator authenticator; + private final String providerName; + + public AbstractGitLabOAuthAuthenticatorProvider( + String clientIdPath, + String clientSecretPath, + String gitlabEndpoint, + String cheApiEndpoint, + String providerName) + throws IOException { + this.providerName = providerName; + authenticator = + getOAuthAuthenticator(clientIdPath, clientSecretPath, gitlabEndpoint, cheApiEndpoint); + LOG.debug("{} GitLab OAuth Authenticator is used.", authenticator); + } + + @Override + public OAuthAuthenticator get() { + return authenticator; + } + + private OAuthAuthenticator getOAuthAuthenticator( + String clientIdPath, String clientSecretPath, String gitlabEndpoint, String cheApiEndpoint) + throws IOException { + if (!isNullOrEmpty(clientIdPath) + && !isNullOrEmpty(clientSecretPath) + && !isNullOrEmpty(gitlabEndpoint)) { + String clientId = Files.readString(Path.of(clientIdPath)); + String clientSecret = Files.readString(Path.of(clientSecretPath)); + if (!isNullOrEmpty(clientId) && !isNullOrEmpty(clientSecret)) { + return new GitLabOAuthAuthenticator( + clientId, clientSecret, gitlabEndpoint, cheApiEndpoint, providerName); + } + } + return new NoopOAuthAuthenticator(); + } + + static class NoopOAuthAuthenticator extends OAuthAuthenticator { + + @Override + public String getOAuthProvider() { + return "Noop"; + } + + @Override + public String getEndpointUrl() { + return "Noop"; + } + } +} diff --git a/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticator.java b/wsmaster/che-core-api-auth-gitlab-common/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticator.java similarity index 94% rename from wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticator.java rename to wsmaster/che-core-api-auth-gitlab-common/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticator.java index 4acece02350..6e78b7281b7 100644 --- a/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticator.java +++ b/wsmaster/che-core-api-auth-gitlab-common/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticator.java @@ -40,13 +40,19 @@ public class GitLabOAuthAuthenticator extends OAuthAuthenticator { private final String clientId; private final String clientSecret; private final String gitlabEndpoint; + private final String providerName; public GitLabOAuthAuthenticator( - String clientId, String clientSecret, String gitlabEndpoint, String cheApiEndpoint) + String clientId, + String clientSecret, + String gitlabEndpoint, + String cheApiEndpoint, + String providerName) throws IOException { this.clientId = clientId; this.clientSecret = clientSecret; this.gitlabEndpoint = trimEnd(gitlabEndpoint, '/'); + this.providerName = providerName; String trimmedGitlabEndpoint = trimEnd(gitlabEndpoint, '/'); this.gitlabUserEndpoint = trimmedGitlabEndpoint + "/api/v4/user"; this.cheApiEndpoint = cheApiEndpoint; @@ -61,7 +67,7 @@ public GitLabOAuthAuthenticator( @Override public String getOAuthProvider() { - return "gitlab"; + return providerName; } @Override diff --git a/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabUser.java b/wsmaster/che-core-api-auth-gitlab-common/src/main/java/org/eclipse/che/security/oauth/GitLabUser.java similarity index 96% rename from wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabUser.java rename to wsmaster/che-core-api-auth-gitlab-common/src/main/java/org/eclipse/che/security/oauth/GitLabUser.java index 078f847ef4a..1b6c9f70652 100644 --- a/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabUser.java +++ b/wsmaster/che-core-api-auth-gitlab-common/src/main/java/org/eclipse/che/security/oauth/GitLabUser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2023 Red Hat, Inc. + * Copyright (c) 2012-2024 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/ diff --git a/wsmaster/che-core-api-auth-gitlab/pom.xml b/wsmaster/che-core-api-auth-gitlab/pom.xml index 79c702aaff0..fd9748af1d2 100644 --- a/wsmaster/che-core-api-auth-gitlab/pom.xml +++ b/wsmaster/che-core-api-auth-gitlab/pom.xml @@ -49,23 +49,15 @@ org.eclipse.che.core - che-core-api-auth-shared - - - org.eclipse.che.core - che-core-commons-annotations + che-core-api-auth-gitlab-common org.eclipse.che.core - che-core-commons-json + che-core-api-auth-shared org.eclipse.che.core - che-core-commons-lang - - - org.slf4j - slf4j-api + che-core-commons-annotations org.testng diff --git a/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabModule.java b/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabModule.java index ef3951a3dc5..29b3c190716 100644 --- a/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabModule.java +++ b/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabModule.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2023 Red Hat, Inc. + * Copyright (c) 2012-2024 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/ @@ -25,5 +25,6 @@ protected void configure() { Multibinder oAuthAuthenticators = Multibinder.newSetBinder(binder(), OAuthAuthenticator.class); oAuthAuthenticators.addBinding().toProvider(GitLabOAuthAuthenticatorProvider.class); + oAuthAuthenticators.addBinding().toProvider(GitLabOAuthAuthenticatorProviderSecond.class); } } diff --git a/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProvider.java b/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProvider.java index a8a11015ec5..17965bb3170 100644 --- a/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProvider.java +++ b/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2023 Red Hat, Inc. + * Copyright (c) 2012-2024 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/ @@ -11,18 +11,11 @@ */ package org.eclipse.che.security.oauth; -import static com.google.common.base.Strings.isNullOrEmpty; - import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import javax.inject.Inject; import javax.inject.Named; -import javax.inject.Provider; import javax.inject.Singleton; import org.eclipse.che.commons.annotation.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Provides implementation of GitLab {@link OAuthAuthenticator} based on available configuration. @@ -30,9 +23,8 @@ * @author Pavol Baran */ @Singleton -public class GitLabOAuthAuthenticatorProvider implements Provider { - private static final Logger LOG = LoggerFactory.getLogger(GitLabOAuthAuthenticatorProvider.class); - private final OAuthAuthenticator authenticator; +public class GitLabOAuthAuthenticatorProvider extends AbstractGitLabOAuthAuthenticatorProvider { + private static final String PROVIDER_NAME = "gitlab"; @Inject public GitLabOAuthAuthenticatorProvider( @@ -41,41 +33,6 @@ public GitLabOAuthAuthenticatorProvider( @Nullable @Named("che.integration.gitlab.oauth_endpoint") String gitlabEndpoint, @Named("che.api") String cheApiEndpoint) throws IOException { - authenticator = - getOAuthAuthenticator(clientIdPath, clientSecretPath, gitlabEndpoint, cheApiEndpoint); - LOG.debug("{} GitLab OAuth Authenticator is used.", authenticator); - } - - @Override - public OAuthAuthenticator get() { - return authenticator; - } - - private OAuthAuthenticator getOAuthAuthenticator( - String clientIdPath, String clientSecretPath, String gitlabEndpoint, String cheApiEndpoint) - throws IOException { - if (!isNullOrEmpty(clientIdPath) - && !isNullOrEmpty(clientSecretPath) - && !isNullOrEmpty(gitlabEndpoint)) { - String clientId = Files.readString(Path.of(clientIdPath)); - String clientSecret = Files.readString(Path.of(clientSecretPath)); - if (!isNullOrEmpty(clientId) && !isNullOrEmpty(clientSecret)) { - return new GitLabOAuthAuthenticator(clientId, clientSecret, gitlabEndpoint, cheApiEndpoint); - } - } - return new NoopOAuthAuthenticator(); - } - - static class NoopOAuthAuthenticator extends OAuthAuthenticator { - - @Override - public String getOAuthProvider() { - return "Noop"; - } - - @Override - public String getEndpointUrl() { - return "Noop"; - } + super(clientIdPath, clientSecretPath, gitlabEndpoint, cheApiEndpoint, PROVIDER_NAME); } } diff --git a/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProviderSecond.java b/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProviderSecond.java new file mode 100644 index 00000000000..42abe5d5427 --- /dev/null +++ b/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProviderSecond.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012-2024 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.oauth; + +import java.io.IOException; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import org.eclipse.che.commons.annotation.Nullable; + +/** + * Provides implementation of GitLab {@link OAuthAuthenticator} based on available configuration. + * + * @author Pavol Baran + */ +@Singleton +public class GitLabOAuthAuthenticatorProviderSecond + extends AbstractGitLabOAuthAuthenticatorProvider { + private static final String PROVIDER_NAME = "gitlab_2"; + + @Inject + public GitLabOAuthAuthenticatorProviderSecond( + @Nullable @Named("che.oauth2.gitlab.clientid_filepath_2") String clientIdPath, + @Nullable @Named("che.oauth2.gitlab.clientsecret_filepath_2") String clientSecretPath, + @Nullable @Named("che.integration.gitlab.oauth_endpoint_2") String gitlabEndpoint, + @Named("che.api") String cheApiEndpoint) + throws IOException { + super(clientIdPath, clientSecretPath, gitlabEndpoint, cheApiEndpoint, PROVIDER_NAME); + } +} diff --git a/wsmaster/che-core-api-auth-gitlab/src/test/java/org/eclipse/che/security/oauth/GitLabAuthenticatorTest.java b/wsmaster/che-core-api-auth-gitlab/src/test/java/org/eclipse/che/security/oauth/GitLabAuthenticatorTest.java index dba4c89e4d6..2fcb9bfe5aa 100644 --- a/wsmaster/che-core-api-auth-gitlab/src/test/java/org/eclipse/che/security/oauth/GitLabAuthenticatorTest.java +++ b/wsmaster/che-core-api-auth-gitlab/src/test/java/org/eclipse/che/security/oauth/GitLabAuthenticatorTest.java @@ -49,7 +49,7 @@ public void shouldGetToken() throws Exception { // given GitLabOAuthAuthenticator gitLabOAuthAuthenticator = new GitLabOAuthAuthenticator( - "id", "secret", wireMockServer.url("/"), "https://che.api.com"); + "id", "secret", wireMockServer.url("/"), "https://che.api.com", "gitlab"); Field flowField = OAuthAuthenticator.class.getDeclaredField("flow"); Field credentialDataStoreField = ((Class) flowField.getGenericType()).getDeclaredField("credentialDataStore"); @@ -74,7 +74,7 @@ public void shouldGetEmptyToken() throws Exception { // given GitLabOAuthAuthenticator gitLabOAuthAuthenticator = new GitLabOAuthAuthenticator( - "id", "secret", wireMockServer.url("/"), "https://che.api.com"); + "id", "secret", wireMockServer.url("/"), "https://che.api.com", "gitlab"); Field flowField = OAuthAuthenticator.class.getDeclaredField("flow"); Field credentialDataStoreField = ((Class) flowField.getGenericType()).getDeclaredField("credentialDataStore"); diff --git a/wsmaster/che-core-api-factory-gitlab-common/pom.xml b/wsmaster/che-core-api-factory-gitlab-common/pom.xml new file mode 100644 index 00000000000..ddc3653370b --- /dev/null +++ b/wsmaster/che-core-api-factory-gitlab-common/pom.xml @@ -0,0 +1,141 @@ + + + + 4.0.0 + + che-master-parent + org.eclipse.che.core + 7.94.0-SNAPSHOT + + che-core-api-factory-gitlab-common + jar + Che Core :: API :: Factory Resolver Gitlab Common + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + com.google.guava + guava + + + jakarta.validation + jakarta.validation-api + + + org.eclipse.che.core + che-core-api-auth + + + org.eclipse.che.core + che-core-api-auth-shared + + + org.eclipse.che.core + che-core-api-core + + + org.eclipse.che.core + che-core-api-dto + + + org.eclipse.che.core + che-core-api-factory + + + org.eclipse.che.core + che-core-api-factory-shared + + + org.eclipse.che.core + che-core-api-workspace + + + org.eclipse.che.core + che-core-commons-lang + + + org.slf4j + slf4j-api + + + jakarta.servlet + jakarta.servlet-api + provided + + + ch.qos.logback + logback-classic + test + + + com.github.tomakehurst + wiremock-jre8-standalone + test + + + jakarta.ws.rs + jakarta.ws.rs-api + test + + + org.eclipse.che.core + che-core-commons-json + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-testng + test + + + org.slf4j + jcl-over-slf4j + test + + + org.testng + testng + test + + + org.wiremock + wiremock-standalone + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + diff --git a/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabFactoryParametersResolver.java b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabFactoryParametersResolver.java new file mode 100644 index 00000000000..8a42b8420a5 --- /dev/null +++ b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabFactoryParametersResolver.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2012-2024 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.gitlab; + +import static org.eclipse.che.api.factory.shared.Constants.URL_PARAMETER_NAME; +import static org.eclipse.che.dto.server.DtoFactory.newDto; + +import jakarta.validation.constraints.NotNull; +import java.util.Map; +import org.eclipse.che.api.core.ApiException; +import org.eclipse.che.api.core.BadRequestException; +import org.eclipse.che.api.factory.server.BaseFactoryParameterResolver; +import org.eclipse.che.api.factory.server.FactoryParametersResolver; +import org.eclipse.che.api.factory.server.scm.AuthorisationRequestManager; +import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager; +import org.eclipse.che.api.factory.server.urlfactory.RemoteFactoryUrl; +import org.eclipse.che.api.factory.server.urlfactory.URLFactoryBuilder; +import org.eclipse.che.api.factory.shared.dto.FactoryDevfileV2Dto; +import org.eclipse.che.api.factory.shared.dto.FactoryMetaDto; +import org.eclipse.che.api.factory.shared.dto.FactoryVisitor; +import org.eclipse.che.api.factory.shared.dto.ScmInfoDto; +import org.eclipse.che.api.workspace.server.devfile.URLFetcher; + +/** + * Provides Factory Parameters resolver for Gitlab repositories. + * + * @author Max Shaposhnyk + */ +public class AbstractGitlabFactoryParametersResolver extends BaseFactoryParameterResolver + implements FactoryParametersResolver { + + private final URLFetcher urlFetcher; + private final AbstractGitlabUrlParser gitlabURLParser; + private final PersonalAccessTokenManager personalAccessTokenManager; + private final String providerName; + + public AbstractGitlabFactoryParametersResolver( + URLFactoryBuilder urlFactoryBuilder, + URLFetcher urlFetcher, + AbstractGitlabUrlParser gitlabURLParser, + PersonalAccessTokenManager personalAccessTokenManager, + AuthorisationRequestManager authorisationRequestManager, + String providerName) { + super(authorisationRequestManager, urlFactoryBuilder, providerName); + this.urlFetcher = urlFetcher; + this.gitlabURLParser = gitlabURLParser; + this.personalAccessTokenManager = personalAccessTokenManager; + this.providerName = providerName; + } + + /** + * Check if this resolver can be used with the given parameters. + * + * @param factoryParameters map of parameters dedicated to factories + * @return true if it will be accepted by the resolver implementation or false if it is not + * accepted + */ + @Override + public boolean accept(@NotNull final Map factoryParameters) { + return factoryParameters.containsKey(URL_PARAMETER_NAME) + && gitlabURLParser.isValid(factoryParameters.get(URL_PARAMETER_NAME)); + } + + @Override + public String getProviderName() { + return providerName; + } + + /** + * Create factory object based on provided parameters + * + * @param factoryParameters map containing factory data parameters provided through URL + * @throws BadRequestException when data are invalid + */ + @Override + public FactoryMetaDto createFactory(@NotNull final Map factoryParameters) + throws ApiException { + // no need to check null value of url parameter as accept() method has performed the check + final GitlabUrl gitlabUrl = gitlabURLParser.parse(factoryParameters.get(URL_PARAMETER_NAME)); + // create factory from the following location if location exists, else create default factory + return createFactory( + factoryParameters, + gitlabUrl, + new GitlabFactoryVisitor(gitlabUrl), + new GitlabAuthorizingFileContentProvider( + gitlabUrl, urlFetcher, personalAccessTokenManager)); + } + + /** + * Visitor that puts the default devfile or updates devfile projects into the Gitlab Factory, if + * needed. + */ + private class GitlabFactoryVisitor implements FactoryVisitor { + + private final GitlabUrl gitlabUrl; + + private GitlabFactoryVisitor(GitlabUrl gitlabUrl) { + this.gitlabUrl = gitlabUrl; + } + + @Override + public FactoryDevfileV2Dto visit(FactoryDevfileV2Dto factoryDto) { + ScmInfoDto scmInfo = + newDto(ScmInfoDto.class) + .withScmProviderName(gitlabUrl.getProviderName()) + .withRepositoryUrl(gitlabUrl.repositoryLocation()); + if (gitlabUrl.getBranch() != null) { + scmInfo.withBranch(gitlabUrl.getBranch()); + } + return factoryDto.withScmInfo(scmInfo); + } + } + + @Override + public RemoteFactoryUrl parseFactoryUrl(String factoryUrl) throws ApiException { + return gitlabURLParser.parse(factoryUrl); + } +} diff --git a/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabOAuthTokenFetcher.java b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabOAuthTokenFetcher.java new file mode 100644 index 00000000000..05949fb1729 --- /dev/null +++ b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabOAuthTokenFetcher.java @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2012-2024 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.gitlab; + +import static java.lang.String.format; +import static org.eclipse.che.commons.lang.StringUtils.trimEnd; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import java.util.Optional; +import java.util.Set; +import org.eclipse.che.api.auth.shared.dto.OAuthToken; +import org.eclipse.che.api.core.BadRequestException; +import org.eclipse.che.api.core.ConflictException; +import org.eclipse.che.api.core.ForbiddenException; +import org.eclipse.che.api.core.NotFoundException; +import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.core.UnauthorizedException; +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.PersonalAccessTokenParams; +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.api.factory.server.scm.exception.UnknownScmProviderException; +import org.eclipse.che.commons.lang.NameGenerator; +import org.eclipse.che.commons.lang.Pair; +import org.eclipse.che.commons.subject.Subject; +import org.eclipse.che.security.oauth.OAuthAPI; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** GitLab OAuth token retriever. */ +public class AbstractGitlabOAuthTokenFetcher implements PersonalAccessTokenFetcher { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractGitlabOAuthTokenFetcher.class); + public static final Set DEFAULT_TOKEN_SCOPES = ImmutableSet.of("api", "write_repository"); + + private final OAuthAPI oAuthAPI; + private final String serverUrl; + private final String apiEndpoint; + private final String providerName; + + public AbstractGitlabOAuthTokenFetcher( + String serverUrl, String apiEndpoint, OAuthAPI oAuthAPI, String providerName) { + this.serverUrl = trimEnd(serverUrl, '/'); + this.apiEndpoint = apiEndpoint; + this.providerName = providerName; + this.oAuthAPI = oAuthAPI; + } + + @Override + public PersonalAccessToken refreshPersonalAccessToken(Subject cheSubject, String scmServerUrl) + throws ScmUnauthorizedException, ScmCommunicationException, UnknownScmProviderException { + return fetchOrRefreshPersonalAccessToken(cheSubject, scmServerUrl, true); + } + + @Override + public PersonalAccessToken fetchPersonalAccessToken(Subject cheSubject, String scmServerUrl) + throws ScmUnauthorizedException, ScmCommunicationException, UnknownScmProviderException { + return fetchOrRefreshPersonalAccessToken(cheSubject, scmServerUrl, false); + } + + private PersonalAccessToken fetchOrRefreshPersonalAccessToken( + Subject cheSubject, String scmServerUrl, boolean forceRefreshToken) + throws ScmUnauthorizedException, ScmCommunicationException, UnknownScmProviderException { + scmServerUrl = trimEnd(scmServerUrl, '/'); + GitlabApiClient gitlabApiClient = getApiClient(scmServerUrl); + if (gitlabApiClient == null || !gitlabApiClient.isConnected(scmServerUrl)) { + LOG.debug("not a valid url {} for current fetcher ", scmServerUrl); + return null; + } + if (oAuthAPI == null) { + throw new ScmCommunicationException( + format( + "OAuth 2 is not configured for SCM provider [%s]. For details, refer " + + "the documentation in section of SCM providers configuration.", + providerName)); + } + OAuthToken oAuthToken; + try { + oAuthToken = + forceRefreshToken + ? oAuthAPI.refreshToken(providerName) + : oAuthAPI.getOrRefreshToken(providerName); + String tokenName = NameGenerator.generate(OAUTH_2_PREFIX, 5); + String tokenId = NameGenerator.generate("id-", 5); + Optional> valid = + isValid( + new PersonalAccessTokenParams( + scmServerUrl, providerName, tokenName, tokenId, oAuthToken.getToken(), null)); + if (valid.isEmpty()) { + throw buildScmUnauthorizedException(cheSubject); + } else if (!valid.get().first) { + throw new ScmCommunicationException( + "Current token doesn't have the necessary privileges. Please make sure Che app scopes are correct and containing at least: " + + DEFAULT_TOKEN_SCOPES); + } + return new PersonalAccessToken( + scmServerUrl, + providerName, + cheSubject.getUserId(), + valid.get().second, + tokenName, + tokenId, + oAuthToken.getToken()); + } catch (UnauthorizedException e) { + throw buildScmUnauthorizedException(cheSubject); + } catch (NotFoundException nfe) { + throw new UnknownScmProviderException(nfe.getMessage(), scmServerUrl); + } catch (ServerException | ForbiddenException | BadRequestException | ConflictException e) { + LOG.warn(e.getMessage()); + throw new ScmCommunicationException(e.getMessage(), e); + } + } + + private ScmUnauthorizedException buildScmUnauthorizedException(Subject cheSubject) { + return new ScmUnauthorizedException( + cheSubject.getUserName() + " is not authorized in " + providerName + " OAuth provider.", + providerName, + "2.0", + getLocalAuthenticateUrl()); + } + + @Override + public Optional isValid(PersonalAccessToken personalAccessToken) { + GitlabApiClient gitlabApiClient = getApiClient(personalAccessToken.getScmProviderUrl()); + if (gitlabApiClient == null + || !gitlabApiClient.isConnected(personalAccessToken.getScmProviderUrl())) { + if (personalAccessToken.getScmTokenName().equals(providerName)) { + gitlabApiClient = new GitlabApiClient(personalAccessToken.getScmProviderUrl()); + } else { + LOG.debug( + "not a valid url {} for current fetcher ", personalAccessToken.getScmProviderUrl()); + return Optional.empty(); + } + } + if (personalAccessToken.getScmTokenName() != null + && personalAccessToken.getScmTokenName().startsWith(OAUTH_2_PREFIX)) { + // validation OAuth token by special API call + try { + GitlabOauthTokenInfo info = + gitlabApiClient.getOAuthTokenInfo(personalAccessToken.getToken()); + return Optional.of(Sets.newHashSet(info.getScope()).containsAll(DEFAULT_TOKEN_SCOPES)); + } catch (ScmItemNotFoundException | ScmCommunicationException | ScmUnauthorizedException e) { + return Optional.of(Boolean.FALSE); + } + } else { + // validating personal access token from secret. Since PAT API is accessible only in + // latest GitLab version, we just perform check by accessing something from API. + try { + GitlabUser user = gitlabApiClient.getUser(personalAccessToken.getToken()); + if (personalAccessToken.getScmUserName().equals(user.getUsername())) { + return Optional.of(Boolean.TRUE); + } else { + return Optional.of(Boolean.FALSE); + } + } catch (ScmItemNotFoundException + | ScmCommunicationException + | ScmBadRequestException + | ScmUnauthorizedException e) { + return Optional.of(Boolean.FALSE); + } + } + } + + @Override + public Optional> isValid(PersonalAccessTokenParams params) + throws ScmCommunicationException { + GitlabApiClient gitlabApiClient = getApiClient(params.getScmProviderUrl()); + if (gitlabApiClient == null || !gitlabApiClient.isConnected(params.getScmProviderUrl())) { + if (providerName.equals(params.getScmTokenName())) { + gitlabApiClient = new GitlabApiClient(params.getScmProviderUrl()); + } else { + LOG.debug("not a valid url {} for current fetcher ", params.getScmProviderUrl()); + return Optional.empty(); + } + } + try { + GitlabUser user = gitlabApiClient.getUser(params.getToken()); + if (params.getScmTokenName() != null && params.getScmTokenName().startsWith(OAUTH_2_PREFIX)) { + // validation OAuth token by special API call + GitlabOauthTokenInfo info = gitlabApiClient.getOAuthTokenInfo(params.getToken()); + return Optional.of( + Pair.of( + Sets.newHashSet(info.getScope()).containsAll(DEFAULT_TOKEN_SCOPES) + ? Boolean.TRUE + : Boolean.FALSE, + user.getUsername())); + } + // validating personal access token from secret. Since PAT API is accessible only in + // latest GitLab version, we just perform check by accessing something from API. + // TODO: add PAT scope validation + return Optional.of(Pair.of(Boolean.TRUE, user.getUsername())); + } catch (ScmItemNotFoundException | ScmBadRequestException | ScmUnauthorizedException e) { + return Optional.empty(); + } + } + + private String getLocalAuthenticateUrl() { + return apiEndpoint + + "/oauth/authenticate?oauth_provider=" + + providerName + + "&scope=" + + Joiner.on('+').join(DEFAULT_TOKEN_SCOPES) + + "&request_method=POST&signature_method=rsa"; + } + + private GitlabApiClient getApiClient(String serverUrl) { + return serverUrl.equals(this.serverUrl) ? new GitlabApiClient(serverUrl) : null; + } +} diff --git a/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabScmFileResolver.java b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabScmFileResolver.java new file mode 100644 index 00000000000..58d7f19e296 --- /dev/null +++ b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabScmFileResolver.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2012-2024 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.gitlab; + +import static org.eclipse.che.api.factory.server.ApiExceptionMapper.toApiException; + +import java.io.IOException; +import org.eclipse.che.api.core.ApiException; +import org.eclipse.che.api.core.NotFoundException; +import org.eclipse.che.api.factory.server.ScmFileResolver; +import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager; +import org.eclipse.che.api.workspace.server.devfile.URLFetcher; +import org.eclipse.che.api.workspace.server.devfile.exception.DevfileException; + +/** GitLab specific SCM file resolver. */ +public class AbstractGitlabScmFileResolver implements ScmFileResolver { + + private final AbstractGitlabUrlParser gitlabUrlParser; + private final URLFetcher urlFetcher; + private final PersonalAccessTokenManager personalAccessTokenManager; + + public AbstractGitlabScmFileResolver( + AbstractGitlabUrlParser gitlabUrlParser, + URLFetcher urlFetcher, + PersonalAccessTokenManager personalAccessTokenManager) { + this.gitlabUrlParser = gitlabUrlParser; + this.urlFetcher = urlFetcher; + this.personalAccessTokenManager = personalAccessTokenManager; + } + + @Override + public boolean accept(String repository) { + return gitlabUrlParser.isValid(repository); + } + + @Override + public String fileContent(String repository, String filePath) throws ApiException { + GitlabUrl gitlabUrl = gitlabUrlParser.parse(repository); + + try { + return fetchContent(gitlabUrl, filePath, false); + } catch (DevfileException exception) { + // This catch might mean that the authentication was rejected by user, try to repeat the fetch + // without authentication flow. + try { + return fetchContent(gitlabUrl, filePath, true); + } catch (DevfileException devfileException) { + throw toApiException(devfileException); + } + } + } + + private String fetchContent(GitlabUrl gitlabUrl, String filePath, boolean skipAuthentication) + throws DevfileException, NotFoundException { + try { + GitlabAuthorizingFileContentProvider contentProvider = + new GitlabAuthorizingFileContentProvider( + gitlabUrl, urlFetcher, personalAccessTokenManager); + return skipAuthentication + ? contentProvider.fetchContentWithoutAuthentication(filePath) + : contentProvider.fetchContent(filePath); + } catch (IOException e) { + throw new NotFoundException(e.getMessage()); + } + } +} diff --git a/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabUrlParser.java b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabUrlParser.java new file mode 100644 index 00000000000..76aa1e13270 --- /dev/null +++ b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabUrlParser.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2012-2024 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.gitlab; + +import static com.google.common.base.Strings.isNullOrEmpty; +import static java.lang.String.format; +import static java.util.regex.Pattern.compile; +import static org.eclipse.che.commons.lang.StringUtils.trimEnd; + +import jakarta.validation.constraints.NotNull; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.eclipse.che.api.factory.server.scm.PersonalAccessToken; +import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager; +import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException; +import org.eclipse.che.api.factory.server.scm.exception.ScmConfigurationPersistenceException; +import org.eclipse.che.api.factory.server.scm.exception.ScmItemNotFoundException; +import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException; +import org.eclipse.che.api.factory.server.urlfactory.DevfileFilenamesProvider; +import org.eclipse.che.commons.env.EnvironmentContext; + +/** + * Parser of String Gitlab URLs and provide {@link GitlabUrl} objects. + * + * @author Max Shaposhnyk + */ +public class AbstractGitlabUrlParser { + + private final DevfileFilenamesProvider devfileFilenamesProvider; + private final PersonalAccessTokenManager personalAccessTokenManager; + private final String providerName; + private static final List gitlabUrlPatternTemplates = + List.of( + "^(?%s)://(?%s)/(?([^/]++/?)+)/-/tree/(?.++)(/)?", + "^(?%s)://(?%s)/(?.*)"); // a wider one, should be the last in + // the list + private final String gitlabSSHPatternTemplate = "^git@(?%s):(?.*)$"; + // list + private final List gitlabUrlPatterns = new ArrayList<>(); + + public AbstractGitlabUrlParser( + String serverUrl, + DevfileFilenamesProvider devfileFilenamesProvider, + PersonalAccessTokenManager personalAccessTokenManager, + String providerName) { + this.devfileFilenamesProvider = devfileFilenamesProvider; + this.personalAccessTokenManager = personalAccessTokenManager; + this.providerName = providerName; + if (isNullOrEmpty(serverUrl)) { + gitlabUrlPatternTemplates.forEach( + t -> gitlabUrlPatterns.add(compile(format(t, "https", "gitlab.com")))); + gitlabUrlPatterns.add(compile(format(gitlabSSHPatternTemplate, "gitlab.com"))); + } else { + String trimmedEndpoint = trimEnd(serverUrl, '/'); + URI uri = URI.create(trimmedEndpoint); + String schema = uri.getScheme(); + String host = uri.getHost(); + for (String gitlabUrlPatternTemplate : gitlabUrlPatternTemplates) { + gitlabUrlPatterns.add(compile(format(gitlabUrlPatternTemplate, schema, host))); + } + gitlabUrlPatterns.add(compile(format(gitlabSSHPatternTemplate, host))); + } + } + + private boolean isUserTokenPresent(String repositoryUrl) { + Optional serverUrlOptional = getServerUrl(repositoryUrl); + if (serverUrlOptional.isPresent()) { + String serverUrl = serverUrlOptional.get(); + try { + Optional token = + personalAccessTokenManager.get(EnvironmentContext.getCurrent().getSubject(), serverUrl); + if (token.isPresent()) { + PersonalAccessToken accessToken = token.get(); + return accessToken.getScmTokenName().equals(providerName); + } + } catch (ScmConfigurationPersistenceException | ScmCommunicationException exception) { + return false; + } + } + return false; + } + + public boolean isValid(@NotNull String url) { + return gitlabUrlPatterns.stream() + .anyMatch(pattern -> pattern.matcher(trimEnd(url, '/')).matches()) + // If the Gitlab URL is not configured, try to find it in a manually added user namespace + // token. + || isUserTokenPresent(url) + // Try to call an API request to see if the URL matches Gitlab. + || isApiRequestRelevant(url); + } + + private boolean isApiRequestRelevant(String repositoryUrl) { + Optional serverUrlOptional = getServerUrl(repositoryUrl); + if (serverUrlOptional.isPresent()) { + GitlabApiClient gitlabApiClient = new GitlabApiClient(serverUrlOptional.get()); + try { + // If the token request catches the unauthorised error, it means that the provided url + // belongs to Gitlab. + gitlabApiClient.getOAuthTokenInfo(""); + } catch (ScmUnauthorizedException e) { + return true; + } catch (ScmItemNotFoundException | IllegalArgumentException | ScmCommunicationException e) { + return false; + } + } + return false; + } + + private Optional getPatternMatcherByUrl(String url) { + URI uri = + URI.create( + url.matches(format(gitlabSSHPatternTemplate, ".*")) + ? "ssh://" + url.replace(":", "/") + : url); + String scheme = uri.getScheme(); + String host = uri.getHost(); + return gitlabUrlPatternTemplates.stream() + .map(t -> compile(format(t, scheme, host)).matcher(url)) + .filter(Matcher::matches) + .findAny() + .or( + () -> { + Matcher matcher = compile(format(gitlabSSHPatternTemplate, host)).matcher(url); + if (matcher.matches()) { + return Optional.of(matcher); + } + return Optional.empty(); + }); + } + + private Optional getServerUrl(String repositoryUrl) { + if (repositoryUrl.startsWith("git@")) { + String substring = repositoryUrl.substring(4); + return Optional.of("https://" + substring.substring(0, substring.indexOf(":"))); + } + Matcher serverUrlMatcher = compile("[^/|:]/").matcher(repositoryUrl); + if (serverUrlMatcher.find()) { + return Optional.of( + repositoryUrl.substring(0, repositoryUrl.indexOf(serverUrlMatcher.group()) + 1)); + } + return Optional.empty(); + } + + /** + * Parses url-s like https://gitlab.apps.cluster-327a.327a.example.opentlc.com/root/proj1.git into + * {@link GitlabUrl} objects. + */ + public GitlabUrl parse(String url) { + String trimmedUrl = trimEnd(url, '/'); + Optional matcherOptional = + gitlabUrlPatterns.stream() + .map(pattern -> pattern.matcher(trimmedUrl)) + .filter(Matcher::matches) + .findFirst() + .or(() -> getPatternMatcherByUrl(trimmedUrl)); + if (matcherOptional.isPresent()) { + return parse(matcherOptional.get()).withUrl(trimmedUrl); + } else { + throw new UnsupportedOperationException( + "The gitlab integration is not configured properly and cannot be used at this moment." + + "Please refer to docs to check the Gitlab integration instructions"); + } + } + + private GitlabUrl parse(Matcher matcher) { + String scheme = null; + try { + scheme = matcher.group("scheme"); + } catch (IllegalArgumentException e) { + // ok no such group + } + String host = matcher.group("host"); + String subGroups = trimEnd(matcher.group("subgroups"), '/'); + if (subGroups.endsWith(".git")) { + subGroups = subGroups.substring(0, subGroups.length() - 4); + } + + String branch = null; + try { + branch = matcher.group("branch"); + } catch (IllegalArgumentException e) { + // ok no such group + } + + return new GitlabUrl() + .withHostName(host) + .withScheme(scheme) + .withSubGroups(subGroups) + .withBranch(branch) + .withDevfileFilenames(devfileFilenamesProvider.getConfiguredDevfileFilenames()); + } +} diff --git a/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabUserDataFetcher.java b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabUserDataFetcher.java new file mode 100644 index 00000000000..2ed3a454b8a --- /dev/null +++ b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabUserDataFetcher.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2012-2024 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.gitlab; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableSet; +import java.util.Set; +import org.eclipse.che.api.factory.server.scm.*; +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; + +/** Gitlab OAuth token retriever. */ +public class AbstractGitlabUserDataFetcher extends AbstractGitUserDataFetcher { + + private final String serverUrl; + private final String apiEndpoint; + private final String providerName; + + public static final Set DEFAULT_TOKEN_SCOPES = + ImmutableSet.of("api", "write_repository", "openid"); + + public AbstractGitlabUserDataFetcher( + String serverUrl, + String apiEndpoint, + PersonalAccessTokenManager personalAccessTokenManager, + String providerName) { + super(providerName, serverUrl, personalAccessTokenManager); + this.serverUrl = serverUrl; + this.apiEndpoint = apiEndpoint; + this.providerName = providerName; + } + + @Override + protected GitUserData fetchGitUserDataWithOAuthToken(String token) + throws ScmItemNotFoundException, ScmCommunicationException, ScmBadRequestException, + ScmUnauthorizedException { + GitlabUser user = new GitlabApiClient(serverUrl).getUser(token); + return new GitUserData(user.getName(), user.getEmail()); + } + + @Override + protected GitUserData fetchGitUserDataWithPersonalAccessToken( + PersonalAccessToken personalAccessToken) + throws ScmItemNotFoundException, ScmCommunicationException, ScmBadRequestException, + ScmUnauthorizedException { + GitlabUser user = + new GitlabApiClient(personalAccessToken.getScmProviderUrl()) + .getUser(personalAccessToken.getToken()); + return new GitUserData(user.getName(), user.getEmail()); + } + + protected String getLocalAuthenticateUrl() { + return apiEndpoint + + "/oauth/authenticate?oauth_provider=" + + providerName + + "&scope=" + + Joiner.on('+').join(DEFAULT_TOKEN_SCOPES) + + "&request_method=POST&signature_method=rsa"; + } +} diff --git a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabApiClient.java b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabApiClient.java similarity index 100% rename from wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabApiClient.java rename to wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabApiClient.java diff --git a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabAuthorizingFileContentProvider.java b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabAuthorizingFileContentProvider.java similarity index 96% rename from wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabAuthorizingFileContentProvider.java rename to wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabAuthorizingFileContentProvider.java index 1137407b943..9cb713abf7f 100644 --- a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabAuthorizingFileContentProvider.java +++ b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabAuthorizingFileContentProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2023 Red Hat, Inc. + * Copyright (c) 2012-2024 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/ diff --git a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabOauthTokenInfo.java b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabOauthTokenInfo.java similarity index 98% rename from wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabOauthTokenInfo.java rename to wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabOauthTokenInfo.java index 9a1c32d28e3..3116ae59ba6 100644 --- a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabOauthTokenInfo.java +++ b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabOauthTokenInfo.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2021 Red Hat, Inc. + * Copyright (c) 2012-2024 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/ diff --git a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabPersonalAccessTokenInfo.java b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabPersonalAccessTokenInfo.java similarity index 98% rename from wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabPersonalAccessTokenInfo.java rename to wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabPersonalAccessTokenInfo.java index ebfcf700f59..0114029d876 100644 --- a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabPersonalAccessTokenInfo.java +++ b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabPersonalAccessTokenInfo.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2023 Red Hat, Inc. + * Copyright (c) 2012-2024 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/ diff --git a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrl.java b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrl.java similarity index 99% rename from wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrl.java rename to wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrl.java index f0bda151871..a30e6dbdd5f 100644 --- a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrl.java +++ b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2023 Red Hat, Inc. + * Copyright (c) 2012-2024 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/ diff --git a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUser.java b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUser.java similarity index 98% rename from wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUser.java rename to wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUser.java index b0f1bc201f9..2937e4d863d 100644 --- a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUser.java +++ b/wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2021 Red Hat, Inc. + * Copyright (c) 2012-2024 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/ diff --git a/wsmaster/che-core-api-factory-gitlab/pom.xml b/wsmaster/che-core-api-factory-gitlab/pom.xml index dd44899d23b..fced7ce4b8e 100644 --- a/wsmaster/che-core-api-factory-gitlab/pom.xml +++ b/wsmaster/che-core-api-factory-gitlab/pom.xml @@ -23,14 +23,6 @@ jar Che Core :: API :: Factory Resolver Gitlab - - com.fasterxml.jackson.core - jackson-annotations - - - com.fasterxml.jackson.core - jackson-databind - com.google.guava guava @@ -43,10 +35,6 @@ jakarta.inject jakarta.inject-api - - jakarta.validation - jakarta.validation-api - org.eclipse.che.core che-core-api-auth @@ -67,6 +55,10 @@ org.eclipse.che.core che-core-api-factory + + org.eclipse.che.core + che-core-api-factory-gitlab-common + org.eclipse.che.core che-core-api-factory-shared @@ -83,18 +75,10 @@ org.eclipse.che.core che-core-commons-annotations - - org.eclipse.che.core - che-core-commons-inject - org.eclipse.che.core che-core-commons-lang - - org.slf4j - slf4j-api - jakarta.servlet jakarta.servlet-api diff --git a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabFactoryParametersResolver.java b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabFactoryParametersResolver.java index b9bfd2024a1..1991f90f40c 100644 --- a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabFactoryParametersResolver.java +++ b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabFactoryParametersResolver.java @@ -11,25 +11,12 @@ */ package org.eclipse.che.api.factory.server.gitlab; -import static org.eclipse.che.api.factory.shared.Constants.URL_PARAMETER_NAME; -import static org.eclipse.che.dto.server.DtoFactory.newDto; - -import jakarta.validation.constraints.NotNull; -import java.util.Map; import javax.inject.Inject; import javax.inject.Singleton; -import org.eclipse.che.api.core.ApiException; -import org.eclipse.che.api.core.BadRequestException; -import org.eclipse.che.api.factory.server.BaseFactoryParameterResolver; import org.eclipse.che.api.factory.server.FactoryParametersResolver; import org.eclipse.che.api.factory.server.scm.AuthorisationRequestManager; import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager; -import org.eclipse.che.api.factory.server.urlfactory.RemoteFactoryUrl; import org.eclipse.che.api.factory.server.urlfactory.URLFactoryBuilder; -import org.eclipse.che.api.factory.shared.dto.FactoryDevfileV2Dto; -import org.eclipse.che.api.factory.shared.dto.FactoryMetaDto; -import org.eclipse.che.api.factory.shared.dto.FactoryVisitor; -import org.eclipse.che.api.factory.shared.dto.ScmInfoDto; import org.eclipse.che.api.workspace.server.devfile.URLFetcher; /** @@ -38,16 +25,11 @@ * @author Max Shaposhnyk */ @Singleton -public class GitlabFactoryParametersResolver extends BaseFactoryParameterResolver +public class GitlabFactoryParametersResolver extends AbstractGitlabFactoryParametersResolver implements FactoryParametersResolver { private static final String PROVIDER_NAME = "gitlab"; - private final URLFactoryBuilder urlFactoryBuilder; - private final URLFetcher urlFetcher; - private final GitlabUrlParser gitlabURLParser; - private final PersonalAccessTokenManager personalAccessTokenManager; - @Inject public GitlabFactoryParametersResolver( URLFactoryBuilder urlFactoryBuilder, @@ -55,78 +37,12 @@ public GitlabFactoryParametersResolver( GitlabUrlParser gitlabURLParser, PersonalAccessTokenManager personalAccessTokenManager, AuthorisationRequestManager authorisationRequestManager) { - super(authorisationRequestManager, urlFactoryBuilder, PROVIDER_NAME); - this.urlFactoryBuilder = urlFactoryBuilder; - this.urlFetcher = urlFetcher; - this.gitlabURLParser = gitlabURLParser; - this.personalAccessTokenManager = personalAccessTokenManager; - } - - /** - * Check if this resolver can be used with the given parameters. - * - * @param factoryParameters map of parameters dedicated to factories - * @return true if it will be accepted by the resolver implementation or false if it is not - * accepted - */ - @Override - public boolean accept(@NotNull final Map factoryParameters) { - return factoryParameters.containsKey(URL_PARAMETER_NAME) - && gitlabURLParser.isValid(factoryParameters.get(URL_PARAMETER_NAME)); - } - - @Override - public String getProviderName() { - return PROVIDER_NAME; - } - - /** - * Create factory object based on provided parameters - * - * @param factoryParameters map containing factory data parameters provided through URL - * @throws BadRequestException when data are invalid - */ - @Override - public FactoryMetaDto createFactory(@NotNull final Map factoryParameters) - throws ApiException { - // no need to check null value of url parameter as accept() method has performed the check - final GitlabUrl gitlabUrl = gitlabURLParser.parse(factoryParameters.get(URL_PARAMETER_NAME)); - // create factory from the following location if location exists, else create default factory - return createFactory( - factoryParameters, - gitlabUrl, - new GitlabFactoryVisitor(gitlabUrl), - new GitlabAuthorizingFileContentProvider( - gitlabUrl, urlFetcher, personalAccessTokenManager)); - } - - /** - * Visitor that puts the default devfile or updates devfile projects into the Gitlab Factory, if - * needed. - */ - private class GitlabFactoryVisitor implements FactoryVisitor { - - private final GitlabUrl gitlabUrl; - - private GitlabFactoryVisitor(GitlabUrl gitlabUrl) { - this.gitlabUrl = gitlabUrl; - } - - @Override - public FactoryDevfileV2Dto visit(FactoryDevfileV2Dto factoryDto) { - ScmInfoDto scmInfo = - newDto(ScmInfoDto.class) - .withScmProviderName(gitlabUrl.getProviderName()) - .withRepositoryUrl(gitlabUrl.repositoryLocation()); - if (gitlabUrl.getBranch() != null) { - scmInfo.withBranch(gitlabUrl.getBranch()); - } - return factoryDto.withScmInfo(scmInfo); - } - } - - @Override - public RemoteFactoryUrl parseFactoryUrl(String factoryUrl) throws ApiException { - return gitlabURLParser.parse(factoryUrl); + super( + urlFactoryBuilder, + urlFetcher, + gitlabURLParser, + personalAccessTokenManager, + authorisationRequestManager, + PROVIDER_NAME); } } diff --git a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabFactoryParametersResolverSecond.java b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabFactoryParametersResolverSecond.java new file mode 100644 index 00000000000..c9b6019adc8 --- /dev/null +++ b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabFactoryParametersResolverSecond.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2012-2024 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.gitlab; + +import javax.inject.Inject; +import javax.inject.Singleton; +import org.eclipse.che.api.factory.server.FactoryParametersResolver; +import org.eclipse.che.api.factory.server.scm.AuthorisationRequestManager; +import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager; +import org.eclipse.che.api.factory.server.urlfactory.URLFactoryBuilder; +import org.eclipse.che.api.workspace.server.devfile.URLFetcher; + +/** + * Provides Factory Parameters resolver for Gitlab repositories. + * + * @author Max Shaposhnyk + */ +@Singleton +public class GitlabFactoryParametersResolverSecond extends AbstractGitlabFactoryParametersResolver + implements FactoryParametersResolver { + + private static final String PROVIDER_NAME = "gitlab_2"; + + @Inject + public GitlabFactoryParametersResolverSecond( + URLFactoryBuilder urlFactoryBuilder, + URLFetcher urlFetcher, + GitlabUrlParserSecond gitlabURLParser, + PersonalAccessTokenManager personalAccessTokenManager, + AuthorisationRequestManager authorisationRequestManager) { + super( + urlFactoryBuilder, + urlFetcher, + gitlabURLParser, + personalAccessTokenManager, + authorisationRequestManager, + PROVIDER_NAME); + } +} diff --git a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabModule.java b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabModule.java index 3fbca073b94..9207f6fa17a 100644 --- a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabModule.java +++ b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabModule.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2023 Red Hat, Inc. + * Copyright (c) 2012-2024 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/ @@ -23,8 +23,11 @@ protected void configure() { Multibinder tokenFetcherMultibinder = Multibinder.newSetBinder(binder(), PersonalAccessTokenFetcher.class); tokenFetcherMultibinder.addBinding().to(GitlabOAuthTokenFetcher.class); + tokenFetcherMultibinder.addBinding().to(GitlabOAuthTokenFetcherSecond.class); + Multibinder gitUserDataMultibinder = Multibinder.newSetBinder(binder(), GitUserDataFetcher.class); gitUserDataMultibinder.addBinding().to(GitlabUserDataFetcher.class); + gitUserDataMultibinder.addBinding().to(GitlabUserDataFetcherSecond.class); } } diff --git a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabOAuthTokenFetcher.java b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabOAuthTokenFetcher.java index 9444afff638..747554955de 100644 --- a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabOAuthTokenFetcher.java +++ b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabOAuthTokenFetcher.java @@ -11,250 +11,21 @@ */ package org.eclipse.che.api.factory.server.gitlab; -import static java.lang.String.format; -import static java.util.stream.Collectors.toList; - -import com.google.common.base.Joiner; -import com.google.common.base.Splitter; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Sets; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.Set; import javax.inject.Inject; import javax.inject.Named; -import org.eclipse.che.api.auth.shared.dto.OAuthToken; -import org.eclipse.che.api.core.BadRequestException; -import org.eclipse.che.api.core.ConflictException; -import org.eclipse.che.api.core.ForbiddenException; -import org.eclipse.che.api.core.NotFoundException; -import org.eclipse.che.api.core.ServerException; -import org.eclipse.che.api.core.UnauthorizedException; -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.PersonalAccessTokenParams; -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.api.factory.server.scm.exception.UnknownScmProviderException; import org.eclipse.che.commons.annotation.Nullable; -import org.eclipse.che.commons.lang.NameGenerator; -import org.eclipse.che.commons.lang.Pair; -import org.eclipse.che.commons.lang.StringUtils; -import org.eclipse.che.commons.subject.Subject; -import org.eclipse.che.inject.ConfigurationException; import org.eclipse.che.security.oauth.OAuthAPI; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** GitLab OAuth token retriever. */ -public class GitlabOAuthTokenFetcher implements PersonalAccessTokenFetcher { +public class GitlabOAuthTokenFetcher extends AbstractGitlabOAuthTokenFetcher { - private static final Logger LOG = LoggerFactory.getLogger(GitlabOAuthTokenFetcher.class); private static final String OAUTH_PROVIDER_NAME = "gitlab"; - public static final Set DEFAULT_TOKEN_SCOPES = ImmutableSet.of("api", "write_repository"); - - private final List registeredGitlabEndpoints; - private final OAuthAPI oAuthAPI; - private final String apiEndpoint; @Inject public GitlabOAuthTokenFetcher( - @Nullable @Named("che.integration.gitlab.server_endpoints") String gitlabEndpoints, - @Nullable @Named("che.integration.gitlab.oauth_endpoint") String oauthEndpoint, + @Nullable @Named("che.integration.gitlab.oauth_endpoint") String serverUrl, @Named("che.api") String apiEndpoint, OAuthAPI oAuthAPI) { - this.apiEndpoint = apiEndpoint; - if (gitlabEndpoints != null) { - this.registeredGitlabEndpoints = - Splitter.on(",") - .splitToStream(gitlabEndpoints) - .map(e -> StringUtils.trimEnd(e, '/')) - .collect(toList()); - } else { - this.registeredGitlabEndpoints = Collections.emptyList(); - } - if (oauthEndpoint != null) { - if (!registeredGitlabEndpoints.contains(StringUtils.trimEnd(oauthEndpoint, '/'))) { - throw new ConfigurationException( - "GitLab OAuth integration endpoint must be present in registered GitLab endpoints list."); - } - this.oAuthAPI = oAuthAPI; - } else { - this.oAuthAPI = null; - } - } - - @Override - public PersonalAccessToken refreshPersonalAccessToken(Subject cheSubject, String scmServerUrl) - throws ScmUnauthorizedException, ScmCommunicationException, UnknownScmProviderException { - return fetchOrRefreshPersonalAccessToken(cheSubject, scmServerUrl, true); - } - - @Override - public PersonalAccessToken fetchPersonalAccessToken(Subject cheSubject, String scmServerUrl) - throws ScmUnauthorizedException, ScmCommunicationException, UnknownScmProviderException { - return fetchOrRefreshPersonalAccessToken(cheSubject, scmServerUrl, false); - } - - private PersonalAccessToken fetchOrRefreshPersonalAccessToken( - Subject cheSubject, String scmServerUrl, boolean forceRefreshToken) - throws ScmUnauthorizedException, ScmCommunicationException, UnknownScmProviderException { - scmServerUrl = StringUtils.trimEnd(scmServerUrl, '/'); - GitlabApiClient gitlabApiClient = getApiClient(scmServerUrl); - if (gitlabApiClient == null || !gitlabApiClient.isConnected(scmServerUrl)) { - LOG.debug("not a valid url {} for current fetcher ", scmServerUrl); - return null; - } - if (oAuthAPI == null) { - throw new ScmCommunicationException( - format( - "OAuth 2 is not configured for SCM provider [%s]. For details, refer " - + "the documentation in section of SCM providers configuration.", - OAUTH_PROVIDER_NAME)); - } - OAuthToken oAuthToken; - try { - oAuthToken = - forceRefreshToken - ? oAuthAPI.refreshToken(OAUTH_PROVIDER_NAME) - : oAuthAPI.getOrRefreshToken(OAUTH_PROVIDER_NAME); - String tokenName = NameGenerator.generate(OAUTH_2_PREFIX, 5); - String tokenId = NameGenerator.generate("id-", 5); - Optional> valid = - isValid( - new PersonalAccessTokenParams( - scmServerUrl, - OAUTH_PROVIDER_NAME, - tokenName, - tokenId, - oAuthToken.getToken(), - null)); - if (valid.isEmpty()) { - throw buildScmUnauthorizedException(cheSubject); - } else if (!valid.get().first) { - throw new ScmCommunicationException( - "Current token doesn't have the necessary privileges. Please make sure Che app scopes are correct and containing at least: " - + DEFAULT_TOKEN_SCOPES); - } - return new PersonalAccessToken( - scmServerUrl, - OAUTH_PROVIDER_NAME, - cheSubject.getUserId(), - valid.get().second, - tokenName, - tokenId, - oAuthToken.getToken()); - } catch (UnauthorizedException e) { - throw buildScmUnauthorizedException(cheSubject); - } catch (NotFoundException nfe) { - throw new UnknownScmProviderException(nfe.getMessage(), scmServerUrl); - } catch (ServerException | ForbiddenException | BadRequestException | ConflictException e) { - LOG.warn(e.getMessage()); - throw new ScmCommunicationException(e.getMessage(), e); - } - } - - private ScmUnauthorizedException buildScmUnauthorizedException(Subject cheSubject) { - return new ScmUnauthorizedException( - cheSubject.getUserName() - + " is not authorized in " - + OAUTH_PROVIDER_NAME - + " OAuth provider.", - OAUTH_PROVIDER_NAME, - "2.0", - getLocalAuthenticateUrl()); - } - - @Override - public Optional isValid(PersonalAccessToken personalAccessToken) { - GitlabApiClient gitlabApiClient = getApiClient(personalAccessToken.getScmProviderUrl()); - if (gitlabApiClient == null - || !gitlabApiClient.isConnected(personalAccessToken.getScmProviderUrl())) { - if (personalAccessToken.getScmTokenName().equals(OAUTH_PROVIDER_NAME)) { - gitlabApiClient = new GitlabApiClient(personalAccessToken.getScmProviderUrl()); - } else { - LOG.debug( - "not a valid url {} for current fetcher ", personalAccessToken.getScmProviderUrl()); - return Optional.empty(); - } - } - if (personalAccessToken.getScmTokenName() != null - && personalAccessToken.getScmTokenName().startsWith(OAUTH_2_PREFIX)) { - // validation OAuth token by special API call - try { - GitlabOauthTokenInfo info = - gitlabApiClient.getOAuthTokenInfo(personalAccessToken.getToken()); - return Optional.of(Sets.newHashSet(info.getScope()).containsAll(DEFAULT_TOKEN_SCOPES)); - } catch (ScmItemNotFoundException | ScmCommunicationException | ScmUnauthorizedException e) { - return Optional.of(Boolean.FALSE); - } - } else { - // validating personal access token from secret. Since PAT API is accessible only in - // latest GitLab version, we just perform check by accessing something from API. - try { - GitlabUser user = gitlabApiClient.getUser(personalAccessToken.getToken()); - if (personalAccessToken.getScmUserName().equals(user.getUsername())) { - return Optional.of(Boolean.TRUE); - } else { - return Optional.of(Boolean.FALSE); - } - } catch (ScmItemNotFoundException - | ScmCommunicationException - | ScmBadRequestException - | ScmUnauthorizedException e) { - return Optional.of(Boolean.FALSE); - } - } - } - - @Override - public Optional> isValid(PersonalAccessTokenParams params) - throws ScmCommunicationException { - GitlabApiClient gitlabApiClient = getApiClient(params.getScmProviderUrl()); - if (gitlabApiClient == null || !gitlabApiClient.isConnected(params.getScmProviderUrl())) { - if (OAUTH_PROVIDER_NAME.equals(params.getScmTokenName())) { - gitlabApiClient = new GitlabApiClient(params.getScmProviderUrl()); - } else { - LOG.debug("not a valid url {} for current fetcher ", params.getScmProviderUrl()); - return Optional.empty(); - } - } - try { - GitlabUser user = gitlabApiClient.getUser(params.getToken()); - if (params.getScmTokenName() != null && params.getScmTokenName().startsWith(OAUTH_2_PREFIX)) { - // validation OAuth token by special API call - GitlabOauthTokenInfo info = gitlabApiClient.getOAuthTokenInfo(params.getToken()); - return Optional.of( - Pair.of( - Sets.newHashSet(info.getScope()).containsAll(DEFAULT_TOKEN_SCOPES) - ? Boolean.TRUE - : Boolean.FALSE, - user.getUsername())); - } - // validating personal access token from secret. Since PAT API is accessible only in - // latest GitLab version, we just perform check by accessing something from API. - // TODO: add PAT scope validation - return Optional.of(Pair.of(Boolean.TRUE, user.getUsername())); - } catch (ScmItemNotFoundException | ScmBadRequestException | ScmUnauthorizedException e) { - return Optional.empty(); - } - } - - private String getLocalAuthenticateUrl() { - return apiEndpoint - + "/oauth/authenticate?oauth_provider=" - + OAUTH_PROVIDER_NAME - + "&scope=" - + Joiner.on('+').join(DEFAULT_TOKEN_SCOPES) - + "&request_method=POST&signature_method=rsa"; - } - - private GitlabApiClient getApiClient(String scmServerUrl) { - return registeredGitlabEndpoints.contains(scmServerUrl) - ? new GitlabApiClient(scmServerUrl) - : null; + super(serverUrl, apiEndpoint, oAuthAPI, OAUTH_PROVIDER_NAME); } } diff --git a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabOAuthTokenFetcherSecond.java b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabOAuthTokenFetcherSecond.java new file mode 100644 index 00000000000..a2cf599aab7 --- /dev/null +++ b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabOAuthTokenFetcherSecond.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012-2024 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.gitlab; + +import javax.inject.Inject; +import javax.inject.Named; +import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.security.oauth.OAuthAPI; + +/** GitLab OAuth token retriever. */ +public class GitlabOAuthTokenFetcherSecond extends AbstractGitlabOAuthTokenFetcher { + + private static final String OAUTH_PROVIDER_NAME = "gitlab_2"; + + @Inject + public GitlabOAuthTokenFetcherSecond( + @Nullable @Named("che.integration.gitlab.oauth_endpoint_2") String serverUrl, + @Named("che.api") String apiEndpoint, + OAuthAPI oAuthAPI) { + super(serverUrl, apiEndpoint, oAuthAPI, OAUTH_PROVIDER_NAME); + } +} diff --git a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabScmFileResolver.java b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabScmFileResolver.java index 8a2ee17c7b6..4cfb64b9b4a 100644 --- a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabScmFileResolver.java +++ b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabScmFileResolver.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2023 Red Hat, Inc. + * Copyright (c) 2012-2024 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/ @@ -11,67 +11,18 @@ */ package org.eclipse.che.api.factory.server.gitlab; -import static org.eclipse.che.api.factory.server.ApiExceptionMapper.toApiException; - -import java.io.IOException; import javax.inject.Inject; -import org.eclipse.che.api.core.ApiException; -import org.eclipse.che.api.core.NotFoundException; -import org.eclipse.che.api.factory.server.ScmFileResolver; import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager; import org.eclipse.che.api.workspace.server.devfile.URLFetcher; -import org.eclipse.che.api.workspace.server.devfile.exception.DevfileException; /** GitLab specific SCM file resolver. */ -public class GitlabScmFileResolver implements ScmFileResolver { - - private final GitlabUrlParser gitlabUrlParser; - private final URLFetcher urlFetcher; - private final PersonalAccessTokenManager personalAccessTokenManager; +public class GitlabScmFileResolver extends AbstractGitlabScmFileResolver { @Inject public GitlabScmFileResolver( GitlabUrlParser gitlabUrlParser, URLFetcher urlFetcher, PersonalAccessTokenManager personalAccessTokenManager) { - this.gitlabUrlParser = gitlabUrlParser; - this.urlFetcher = urlFetcher; - this.personalAccessTokenManager = personalAccessTokenManager; - } - - @Override - public boolean accept(String repository) { - return gitlabUrlParser.isValid(repository); - } - - @Override - public String fileContent(String repository, String filePath) throws ApiException { - GitlabUrl gitlabUrl = gitlabUrlParser.parse(repository); - - try { - return fetchContent(gitlabUrl, filePath, false); - } catch (DevfileException exception) { - // This catch might mean that the authentication was rejected by user, try to repeat the fetch - // without authentication flow. - try { - return fetchContent(gitlabUrl, filePath, true); - } catch (DevfileException devfileException) { - throw toApiException(devfileException); - } - } - } - - private String fetchContent(GitlabUrl gitlabUrl, String filePath, boolean skipAuthentication) - throws DevfileException, NotFoundException { - try { - GitlabAuthorizingFileContentProvider contentProvider = - new GitlabAuthorizingFileContentProvider( - gitlabUrl, urlFetcher, personalAccessTokenManager); - return skipAuthentication - ? contentProvider.fetchContentWithoutAuthentication(filePath) - : contentProvider.fetchContent(filePath); - } catch (IOException e) { - throw new NotFoundException(e.getMessage()); - } + super(gitlabUrlParser, urlFetcher, personalAccessTokenManager); } } diff --git a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabScmFileResolverSecond.java b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabScmFileResolverSecond.java new file mode 100644 index 00000000000..aa9a0cb7d7b --- /dev/null +++ b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabScmFileResolverSecond.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2012-2024 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.gitlab; + +import javax.inject.Inject; +import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager; +import org.eclipse.che.api.workspace.server.devfile.URLFetcher; + +/** GitLab specific SCM file resolver. */ +public class GitlabScmFileResolverSecond extends AbstractGitlabScmFileResolver { + + @Inject + public GitlabScmFileResolverSecond( + GitlabUrlParserSecond gitlabUrlParser, + URLFetcher urlFetcher, + PersonalAccessTokenManager personalAccessTokenManager) { + super(gitlabUrlParser, urlFetcher, personalAccessTokenManager); + } +} diff --git a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrlParser.java b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrlParser.java index 53faa2663da..6d5b6a29f68 100644 --- a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrlParser.java +++ b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrlParser.java @@ -11,200 +11,26 @@ */ package org.eclipse.che.api.factory.server.gitlab; -import static java.lang.String.format; -import static java.util.regex.Pattern.compile; -import static org.eclipse.che.commons.lang.StringUtils.trimEnd; - -import com.google.common.base.Splitter; -import jakarta.validation.constraints.NotNull; -import java.net.URI; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import javax.inject.Inject; import javax.inject.Named; -import org.eclipse.che.api.factory.server.scm.PersonalAccessToken; import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager; -import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException; -import org.eclipse.che.api.factory.server.scm.exception.ScmConfigurationPersistenceException; -import org.eclipse.che.api.factory.server.scm.exception.ScmItemNotFoundException; -import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException; import org.eclipse.che.api.factory.server.urlfactory.DevfileFilenamesProvider; import org.eclipse.che.commons.annotation.Nullable; -import org.eclipse.che.commons.env.EnvironmentContext; /** * Parser of String Gitlab URLs and provide {@link GitlabUrl} objects. * * @author Max Shaposhnyk */ -public class GitlabUrlParser { +public class GitlabUrlParser extends AbstractGitlabUrlParser { - private final DevfileFilenamesProvider devfileFilenamesProvider; - private final PersonalAccessTokenManager personalAccessTokenManager; - private static final List gitlabUrlPatternTemplates = - List.of( - "^(?%s)://(?%s)/(?([^/]++/?)+)/-/tree/(?.++)(/)?", - "^(?%s)://(?%s)/(?.*)"); // a wider one, should be the last in - // the list - private final String gitlabSSHPatternTemplate = "^git@(?%s):(?.*)$"; - // list - private final List gitlabUrlPatterns = new ArrayList<>(); private static final String OAUTH_PROVIDER_NAME = "gitlab"; @Inject public GitlabUrlParser( - @Nullable @Named("che.integration.gitlab.server_endpoints") String gitlabEndpoints, + @Nullable @Named("che.integration.gitlab.oauth_endpoint") String serverUrl, DevfileFilenamesProvider devfileFilenamesProvider, PersonalAccessTokenManager personalAccessTokenManager) { - this.devfileFilenamesProvider = devfileFilenamesProvider; - this.personalAccessTokenManager = personalAccessTokenManager; - if (gitlabEndpoints != null) { - for (String gitlabEndpoint : Splitter.on(",").split(gitlabEndpoints)) { - String trimmedEndpoint = trimEnd(gitlabEndpoint, '/'); - URI uri = URI.create(trimmedEndpoint); - String schema = uri.getScheme(); - String host = uri.getHost(); - for (String gitlabUrlPatternTemplate : gitlabUrlPatternTemplates) { - gitlabUrlPatterns.add(compile(format(gitlabUrlPatternTemplate, schema, host))); - } - gitlabUrlPatterns.add(compile(format(gitlabSSHPatternTemplate, host))); - } - } else { - gitlabUrlPatternTemplates.forEach( - t -> gitlabUrlPatterns.add(compile(format(t, "https", "gitlab.com")))); - gitlabUrlPatterns.add(compile(format(gitlabSSHPatternTemplate, "gitlab.com"))); - } - } - - private boolean isUserTokenPresent(String repositoryUrl) { - Optional serverUrlOptional = getServerUrl(repositoryUrl); - if (serverUrlOptional.isPresent()) { - String serverUrl = serverUrlOptional.get(); - try { - Optional token = - personalAccessTokenManager.get(EnvironmentContext.getCurrent().getSubject(), serverUrl); - if (token.isPresent()) { - PersonalAccessToken accessToken = token.get(); - return accessToken.getScmTokenName().equals(OAUTH_PROVIDER_NAME); - } - } catch (ScmConfigurationPersistenceException | ScmCommunicationException exception) { - return false; - } - } - return false; - } - - public boolean isValid(@NotNull String url) { - return gitlabUrlPatterns.stream() - .anyMatch(pattern -> pattern.matcher(trimEnd(url, '/')).matches()) - // If the Gitlab URL is not configured, try to find it in a manually added user namespace - // token. - || isUserTokenPresent(url) - // Try to call an API request to see if the URL matches Gitlab. - || isApiRequestRelevant(url); - } - - private boolean isApiRequestRelevant(String repositoryUrl) { - Optional serverUrlOptional = getServerUrl(repositoryUrl); - if (serverUrlOptional.isPresent()) { - GitlabApiClient gitlabApiClient = new GitlabApiClient(serverUrlOptional.get()); - try { - // If the token request catches the unauthorised error, it means that the provided url - // belongs to Gitlab. - gitlabApiClient.getOAuthTokenInfo(""); - } catch (ScmUnauthorizedException e) { - return true; - } catch (ScmItemNotFoundException | IllegalArgumentException | ScmCommunicationException e) { - return false; - } - } - return false; - } - - private Optional getPatternMatcherByUrl(String url) { - URI uri = - URI.create( - url.matches(format(gitlabSSHPatternTemplate, ".*")) - ? "ssh://" + url.replace(":", "/") - : url); - String scheme = uri.getScheme(); - String host = uri.getHost(); - return gitlabUrlPatternTemplates.stream() - .map(t -> compile(format(t, scheme, host)).matcher(url)) - .filter(Matcher::matches) - .findAny() - .or( - () -> { - Matcher matcher = compile(format(gitlabSSHPatternTemplate, host)).matcher(url); - if (matcher.matches()) { - return Optional.of(matcher); - } - return Optional.empty(); - }); - } - - private Optional getServerUrl(String repositoryUrl) { - if (repositoryUrl.startsWith("git@")) { - String substring = repositoryUrl.substring(4); - return Optional.of("https://" + substring.substring(0, substring.indexOf(":"))); - } - Matcher serverUrlMatcher = compile("[^/|:]/").matcher(repositoryUrl); - if (serverUrlMatcher.find()) { - return Optional.of( - repositoryUrl.substring(0, repositoryUrl.indexOf(serverUrlMatcher.group()) + 1)); - } - return Optional.empty(); - } - - /** - * Parses url-s like https://gitlab.apps.cluster-327a.327a.example.opentlc.com/root/proj1.git into - * {@link GitlabUrl} objects. - */ - public GitlabUrl parse(String url) { - String trimmedUrl = trimEnd(url, '/'); - Optional matcherOptional = - gitlabUrlPatterns.stream() - .map(pattern -> pattern.matcher(trimmedUrl)) - .filter(Matcher::matches) - .findFirst() - .or(() -> getPatternMatcherByUrl(trimmedUrl)); - if (matcherOptional.isPresent()) { - return parse(matcherOptional.get()).withUrl(trimmedUrl); - } else { - throw new UnsupportedOperationException( - "The gitlab integration is not configured properly and cannot be used at this moment." - + "Please refer to docs to check the Gitlab integration instructions"); - } - } - - private GitlabUrl parse(Matcher matcher) { - String scheme = null; - try { - scheme = matcher.group("scheme"); - } catch (IllegalArgumentException e) { - // ok no such group - } - String host = matcher.group("host"); - String subGroups = trimEnd(matcher.group("subgroups"), '/'); - if (subGroups.endsWith(".git")) { - subGroups = subGroups.substring(0, subGroups.length() - 4); - } - - String branch = null; - try { - branch = matcher.group("branch"); - } catch (IllegalArgumentException e) { - // ok no such group - } - - return new GitlabUrl() - .withHostName(host) - .withScheme(scheme) - .withSubGroups(subGroups) - .withBranch(branch) - .withDevfileFilenames(devfileFilenamesProvider.getConfiguredDevfileFilenames()); + super(serverUrl, devfileFilenamesProvider, personalAccessTokenManager, OAUTH_PROVIDER_NAME); } } diff --git a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrlParserSecond.java b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrlParserSecond.java new file mode 100644 index 00000000000..3480215a03d --- /dev/null +++ b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrlParserSecond.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2012-2024 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.gitlab; + +import javax.inject.Inject; +import javax.inject.Named; +import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager; +import org.eclipse.che.api.factory.server.urlfactory.DevfileFilenamesProvider; +import org.eclipse.che.commons.annotation.Nullable; + +/** + * Parser of String Gitlab URLs and provide {@link GitlabUrl} objects. + * + * @author Max Shaposhnyk + */ +public class GitlabUrlParserSecond extends AbstractGitlabUrlParser { + + private static final String OAUTH_PROVIDER_NAME = "gitlab_2"; + + @Inject + public GitlabUrlParserSecond( + @Nullable @Named("che.integration.gitlab.oauth_endpoint_2") String serverUrl, + DevfileFilenamesProvider devfileFilenamesProvider, + PersonalAccessTokenManager personalAccessTokenManager) { + super(serverUrl, devfileFilenamesProvider, personalAccessTokenManager, OAUTH_PROVIDER_NAME); + } +} diff --git a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUserDataFetcher.java b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUserDataFetcher.java index e276d2c6987..028a46228c5 100644 --- a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUserDataFetcher.java +++ b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUserDataFetcher.java @@ -11,94 +11,22 @@ */ package org.eclipse.che.api.factory.server.gitlab; -import static com.google.common.base.Strings.isNullOrEmpty; -import static java.util.stream.Collectors.toList; - -import com.google.common.base.Joiner; -import com.google.common.base.Splitter; -import com.google.common.collect.ImmutableSet; -import java.util.Collections; -import java.util.List; -import java.util.Set; import javax.inject.Inject; import javax.inject.Named; import org.eclipse.che.api.factory.server.scm.*; -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.annotation.Nullable; -import org.eclipse.che.commons.lang.StringUtils; -import org.eclipse.che.inject.ConfigurationException; /** Gitlab OAuth token retriever. */ -public class GitlabUserDataFetcher extends AbstractGitUserDataFetcher { - private final String apiEndpoint; +public class GitlabUserDataFetcher extends AbstractGitlabUserDataFetcher { /** Name of this OAuth provider as found in OAuthAPI. */ private static final String OAUTH_PROVIDER_NAME = "gitlab"; - private final List registeredGitlabEndpoints; - - public static final Set DEFAULT_TOKEN_SCOPES = - ImmutableSet.of("api", "write_repository", "openid"); - @Inject public GitlabUserDataFetcher( - @Nullable @Named("che.integration.gitlab.server_endpoints") String gitlabEndpoints, - @Nullable @Named("che.integration.gitlab.oauth_endpoint") String oauthEndpoint, + @Nullable @Named("che.integration.gitlab.oauth_endpoint") String serverUrl, @Named("che.api") String apiEndpoint, PersonalAccessTokenManager personalAccessTokenManager) { - super( - OAUTH_PROVIDER_NAME, - isNullOrEmpty(gitlabEndpoints) ? "https://gitlab.com" : gitlabEndpoints, - personalAccessTokenManager); - this.apiEndpoint = apiEndpoint; - if (gitlabEndpoints != null) { - this.registeredGitlabEndpoints = - Splitter.on(",") - .splitToStream(gitlabEndpoints) - .map(e -> StringUtils.trimEnd(e, '/')) - .collect(toList()); - } else { - this.registeredGitlabEndpoints = Collections.emptyList(); - } - if (oauthEndpoint != null) { - if (!registeredGitlabEndpoints.contains(StringUtils.trimEnd(oauthEndpoint, '/'))) { - throw new ConfigurationException( - "GitLab OAuth integration endpoint must be present in registered GitLab endpoints list."); - } - } - } - - @Override - protected GitUserData fetchGitUserDataWithOAuthToken(String token) - throws ScmItemNotFoundException, ScmCommunicationException, ScmBadRequestException, - ScmUnauthorizedException { - for (String gitlabServerEndpoint : this.registeredGitlabEndpoints) { - GitlabUser user = new GitlabApiClient(gitlabServerEndpoint).getUser(token); - return new GitUserData(user.getName(), user.getEmail()); - } - throw new ScmCommunicationException("Failed to retrieve git user data from Gitlab"); - } - - @Override - protected GitUserData fetchGitUserDataWithPersonalAccessToken( - PersonalAccessToken personalAccessToken) - throws ScmItemNotFoundException, ScmCommunicationException, ScmBadRequestException, - ScmUnauthorizedException { - GitlabUser user = - new GitlabApiClient(personalAccessToken.getScmProviderUrl()) - .getUser(personalAccessToken.getToken()); - return new GitUserData(user.getName(), user.getEmail()); - } - - protected String getLocalAuthenticateUrl() { - return apiEndpoint - + "/oauth/authenticate?oauth_provider=" - + OAUTH_PROVIDER_NAME - + "&scope=" - + Joiner.on('+').join(DEFAULT_TOKEN_SCOPES) - + "&request_method=POST&signature_method=rsa"; + super(serverUrl, apiEndpoint, personalAccessTokenManager, OAUTH_PROVIDER_NAME); } } diff --git a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUserDataFetcherSecond.java b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUserDataFetcherSecond.java new file mode 100644 index 00000000000..a7696cfd91f --- /dev/null +++ b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabUserDataFetcherSecond.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012-2024 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.gitlab; + +import javax.inject.Inject; +import javax.inject.Named; +import org.eclipse.che.api.factory.server.scm.*; +import org.eclipse.che.commons.annotation.Nullable; + +/** Gitlab OAuth token retriever. */ +public class GitlabUserDataFetcherSecond extends AbstractGitlabUserDataFetcher { + + /** Name of this OAuth provider as found in OAuthAPI. */ + private static final String OAUTH_PROVIDER_NAME = "gitlab_2"; + + @Inject + public GitlabUserDataFetcherSecond( + @Nullable @Named("che.integration.gitlab.oauth_endpoint_2") String serverUrl, + @Named("che.api") String apiEndpoint, + PersonalAccessTokenManager personalAccessTokenManager) { + super(serverUrl, apiEndpoint, personalAccessTokenManager, OAUTH_PROVIDER_NAME); + } +} diff --git a/wsmaster/che-core-api-factory-gitlab/src/test/java/org/eclipse/che/api/factory/server/gitlab/GitlabOAuthTokenFetcherTest.java b/wsmaster/che-core-api-factory-gitlab/src/test/java/org/eclipse/che/api/factory/server/gitlab/GitlabOAuthTokenFetcherTest.java index 45aab1561b0..926408df4ca 100644 --- a/wsmaster/che-core-api-factory-gitlab/src/test/java/org/eclipse/che/api/factory/server/gitlab/GitlabOAuthTokenFetcherTest.java +++ b/wsmaster/che-core-api-factory-gitlab/src/test/java/org/eclipse/che/api/factory/server/gitlab/GitlabOAuthTokenFetcherTest.java @@ -38,7 +38,6 @@ import org.eclipse.che.commons.lang.Pair; import org.eclipse.che.commons.subject.Subject; import org.eclipse.che.commons.subject.SubjectImpl; -import org.eclipse.che.inject.ConfigurationException; import org.eclipse.che.security.oauth.OAuthAPI; import org.mockito.Mock; import org.mockito.testng.MockitoTestNGListener; @@ -64,8 +63,7 @@ void start() { WireMock.configureFor("localhost", wireMockServer.port()); wireMock = new WireMock("localhost", wireMockServer.port()); oAuthTokenFetcher = - new GitlabOAuthTokenFetcher( - wireMockServer.url("/"), wireMockServer.url("/"), "http://che.api", oAuthAPI); + new GitlabOAuthTokenFetcher(wireMockServer.url("/"), "http://che.api", oAuthAPI); } @AfterMethod @@ -139,15 +137,6 @@ public void shouldReturnToken() throws Exception { assertNotNull(token); } - @Test( - expectedExceptions = ConfigurationException.class, - expectedExceptionsMessageRegExp = - "GitLab OAuth integration endpoint must be present in registered GitLab endpoints list.") - public void shouldThrowConfigurationExceptionIfOauthEndpointNotInTheList() throws Exception { - new GitlabOAuthTokenFetcher( - wireMockServer.url("/"), "http://foo.bar", "http://che.api", oAuthAPI); - } - @Test( expectedExceptions = ScmCommunicationException.class, expectedExceptionsMessageRegExp = @@ -156,7 +145,7 @@ public void shouldThrowConfigurationExceptionIfOauthEndpointNotInTheList() throw public void shouldThrowScmCommunicationExceptionWhenNoOauthIsConfigured() throws Exception { Subject subject = new SubjectImpl("Username", "id1", "token", false); GitlabOAuthTokenFetcher localFetcher = - new GitlabOAuthTokenFetcher(wireMockServer.url("/"), null, "http://che.api", oAuthAPI); + new GitlabOAuthTokenFetcher(wireMockServer.url("/"), "http://che.api", null); localFetcher.fetchPersonalAccessToken(subject, wireMockServer.url("/")); } diff --git a/wsmaster/che-core-api-factory-gitlab/src/test/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrlParserTest.java b/wsmaster/che-core-api-factory-gitlab/src/test/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrlParserTest.java index e8022fd043c..28e7159a05f 100644 --- a/wsmaster/che-core-api-factory-gitlab/src/test/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrlParserTest.java +++ b/wsmaster/che-core-api-factory-gitlab/src/test/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrlParserTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2023 Red Hat, Inc. + * Copyright (c) 2012-2024 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/ @@ -58,7 +58,7 @@ public void prepare() { public void setUp() { gitlabUrlParser = new GitlabUrlParser( - "https://gitlab1.com,https://gitlab.foo.xxx", + "https://gitlab1.com", devfileFilenamesProvider, mock(PersonalAccessTokenManager.class)); } @@ -126,14 +126,14 @@ public Object[][] urls() { return new Object[][] { {"https://gitlab1.com/user/project/test1.git"}, {"https://gitlab1.com/user/project1.git"}, - {"https://gitlab.foo.xxx/scm/project/test1.git"}, + {"https://gitlab1.com/scm/project/test1.git"}, {"https://gitlab1.com/user/project/"}, {"https://gitlab1.com/user/project/repo/"}, {"https://gitlab1.com/user/project/-/tree/master/"}, {"https://gitlab1.com/user/project/repo/-/tree/master/subfolder"}, {"git@gitlab1.com:user/project/test1.git"}, {"git@gitlab1.com:user/project1.git"}, - {"git@gitlab.foo.xxx:scm/project/test1.git"}, + {"git@gitlab1.com:scm/project/test1.git"}, {"git@gitlab1.com:user/project/"}, {"git@gitlab1.com:user/project/repo/"}, }; diff --git a/wsmaster/che-core-api-factory-gitlab/src/test/java/org/eclipse/che/api/factory/server/gitlab/GitlabUserDataFetcherTest.java b/wsmaster/che-core-api-factory-gitlab/src/test/java/org/eclipse/che/api/factory/server/gitlab/GitlabUserDataFetcherTest.java index 5e16f9d0608..96a57b591ad 100644 --- a/wsmaster/che-core-api-factory-gitlab/src/test/java/org/eclipse/che/api/factory/server/gitlab/GitlabUserDataFetcherTest.java +++ b/wsmaster/che-core-api-factory-gitlab/src/test/java/org/eclipse/che/api/factory/server/gitlab/GitlabUserDataFetcherTest.java @@ -60,10 +60,7 @@ void start() { wireMock = new WireMock("localhost", wireMockServer.port()); gitlabUserDataFetcher = new GitlabUserDataFetcher( - wireMockServer.url("/"), - wireMockServer.url("/"), - "http://che.api", - personalAccessTokenManager); + wireMockServer.url("/"), "http://che.api", personalAccessTokenManager); stubFor( get(urlEqualTo("/api/v4/user")) diff --git a/wsmaster/pom.xml b/wsmaster/pom.xml index c98aabfd89d..1533c6efa68 100644 --- a/wsmaster/pom.xml +++ b/wsmaster/pom.xml @@ -31,6 +31,7 @@ che-core-api-auth-github che-core-api-auth-github-common che-core-api-auth-gitlab + che-core-api-auth-gitlab-common che-core-api-auth-openshift che-core-api-workspace-shared che-core-api-workspace @@ -47,6 +48,7 @@ che-core-api-factory-github che-core-api-factory-github-common che-core-api-factory-gitlab + che-core-api-factory-gitlab-common che-core-api-factory-bitbucket che-core-api-factory-bitbucket-server che-core-api-ssh