From c9f724bb8f99752ccbecb3f8399de2d82e213f4e Mon Sep 17 00:00:00 2001 From: Pavol Baran Date: Wed, 26 Jan 2022 11:06:57 +0100 Subject: [PATCH] GitLab OAuth authentication with embededOAuthAPI (#195) GitLab OAuth authentication with embededOAuthAPI Signed-off-by: Pavol Baran --- assembly/assembly-wsmaster-war/pom.xml | 4 + .../che/api/deploy/WsMasterModule.java | 2 + .../webapp/WEB-INF/classes/che/che.properties | 7 ++ pom.xml | 5 + wsmaster/che-core-api-auth-gitlab/pom.xml | 72 +++++++++++ .../che/security/oauth/GitLabModule.java | 29 +++++ .../oauth/GitLabOAuthAuthenticator.java | 117 ++++++++++++++++++ .../GitLabOAuthAuthenticatorProvider.java | 83 +++++++++++++ .../che/security/oauth/GitLabUser.java | 70 +++++++++++ .../GitLabOAuthAuthenticatorProviderTest.java | 108 ++++++++++++++++ .../gitlab/GitlabOAuthTokenFetcher.java | 3 + wsmaster/pom.xml | 1 + 12 files changed, 501 insertions(+) create mode 100644 wsmaster/che-core-api-auth-gitlab/pom.xml create mode 100644 wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabModule.java create mode 100644 wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticator.java create mode 100644 wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProvider.java create mode 100644 wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabUser.java create mode 100644 wsmaster/che-core-api-auth-gitlab/src/test/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProviderTest.java diff --git a/assembly/assembly-wsmaster-war/pom.xml b/assembly/assembly-wsmaster-war/pom.xml index 2505c40d22..81ed9936ee 100644 --- a/assembly/assembly-wsmaster-war/pom.xml +++ b/assembly/assembly-wsmaster-war/pom.xml @@ -103,6 +103,10 @@ org.eclipse.che.core che-core-api-auth-bitbucket + + org.eclipse.che.core + che-core-api-auth-gitlab + org.eclipse.che.core che-core-api-auth-openshift diff --git a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java index 19cb76512d..452918b8c7 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 @@ -105,6 +105,7 @@ import org.eclipse.che.multiuser.resource.api.ResourceModule; import org.eclipse.che.security.PBKDF2PasswordEncryptor; import org.eclipse.che.security.PasswordEncryptor; import org.eclipse.che.security.oauth.EmbeddedOAuthAPI; +import org.eclipse.che.security.oauth.GitLabModule; import org.eclipse.che.security.oauth.OAuthAPI; import org.eclipse.che.security.oauth.OpenShiftOAuthModule; import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientConfigFactory; @@ -284,6 +285,7 @@ public class WsMasterModule extends AbstractModule { install(new FactoryModuleBuilder().build(PassThroughProxyProvisionerFactory.class)); installDefaultSecureServerExposer(infrastructure); install(new org.eclipse.che.security.oauth1.BitbucketModule()); + install(new GitLabModule()); configureMultiUserMode(persistenceProperties, infrastructure); diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties index 5de83b82ad..854e1a315f 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 @@ -767,3 +767,10 @@ che.integration.gitlab.server_endpoints=NULL # Address of the GitLab server with configured OAuth 2 integration che.integration.gitlab.oauth_endpoint=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 + +# Location of the file with GitLab client secret. +che.oauth2.gitlab.clientsecret_filepath=NULL diff --git a/pom.xml b/pom.xml index 7c7bf44958..1124955d63 100644 --- a/pom.xml +++ b/pom.xml @@ -661,6 +661,11 @@ che-core-api-auth-github ${che.version} + + org.eclipse.che.core + che-core-api-auth-gitlab + ${che.version} + org.eclipse.che.core che-core-api-auth-openshift diff --git a/wsmaster/che-core-api-auth-gitlab/pom.xml b/wsmaster/che-core-api-auth-gitlab/pom.xml new file mode 100644 index 0000000000..15ce911dbd --- /dev/null +++ b/wsmaster/che-core-api-auth-gitlab/pom.xml @@ -0,0 +1,72 @@ + + + + 4.0.0 + + che-master-parent + org.eclipse.che.core + 7.42.0-SNAPSHOT + + che-core-api-auth-gitlab + jar + Che Core :: API :: Authentication GitLab + + + com.google.guava + guava + + + com.google.http-client + google-http-client + + + com.google.inject + guice + + + com.sun.mail + jakarta.mail + + + 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-annotations + + + org.eclipse.che.core + che-core-commons-json + + + org.slf4j + slf4j-api + + + org.testng + testng + test + + + 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 new file mode 100644 index 0000000000..ca6d934fb0 --- /dev/null +++ b/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabModule.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2012-2021 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 com.google.inject.AbstractModule; +import com.google.inject.multibindings.Multibinder; + +/** + * Setup GitlabOAuthAuthenticator in guice container. + * + * @author Pavol Baran + */ +public class GitLabModule extends AbstractModule { + @Override + protected void configure() { + Multibinder oAuthAuthenticators = + Multibinder.newSetBinder(binder(), OAuthAuthenticator.class); + oAuthAuthenticators.addBinding().toProvider(GitLabOAuthAuthenticatorProvider.class); + } +} 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/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticator.java new file mode 100644 index 0000000000..0967893733 --- /dev/null +++ b/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticator.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2012-2021 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 com.google.api.client.util.store.MemoryDataStoreFactory; +import jakarta.mail.internet.AddressException; +import jakarta.mail.internet.InternetAddress; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import javax.inject.Singleton; +import org.eclipse.che.api.auth.shared.dto.OAuthToken; +import org.eclipse.che.commons.json.JsonHelper; +import org.eclipse.che.commons.json.JsonParseException; +import org.eclipse.che.security.oauth.shared.User; + +/** + * OAuth2 authenticator for GitLab account. + * + * @author Pavol Baran + */ +@Singleton +public class GitLabOAuthAuthenticator extends OAuthAuthenticator { + private final String gitlabUserEndpoint; + private final String cheApiEndpoint; + + public GitLabOAuthAuthenticator( + String clientId, String clientSecret, String gitlabEndpoint, String cheApiEndpoint) + throws IOException { + this.gitlabUserEndpoint = gitlabEndpoint + "/api/v4/user"; + this.cheApiEndpoint = cheApiEndpoint; + configure( + clientId, + clientSecret, + new String[] {}, + gitlabEndpoint + "/oauth/authorize", + gitlabEndpoint + "/oauth/token", + new MemoryDataStoreFactory()); + } + + @Override + public User getUser(OAuthToken accessToken) throws OAuthAuthenticationException { + GitLabUser user = getJson(gitlabUserEndpoint, accessToken.getToken(), GitLabUser.class); + final String email = user.getEmail(); + + if (isNullOrEmpty(email)) { + throw new OAuthAuthenticationException( + "Sorry, we failed to find any verified email associated with your GitLab account." + + " Please, verify at least one email in your account and try to connect with GitLab again."); + } + try { + new InternetAddress(email).validate(); + } catch (AddressException e) { + throw new OAuthAuthenticationException(e.getMessage()); + } + return user; + } + + @Override + public String getOAuthProvider() { + return "gitlab"; + } + + @Override + protected String findRedirectUrl(URL requestUrl) { + return cheApiEndpoint + "/oauth/callback"; + } + + @Override + protected O getJson(String getUserUrl, String accessToken, Class userClass) + throws OAuthAuthenticationException { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = + HttpRequest.newBuilder(URI.create(getUserUrl)) + .header("Authorization", "Bearer " + accessToken) + .build(); + + try { + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofInputStream()); + return JsonHelper.fromJson(response.body(), userClass, null); + } catch (IOException | InterruptedException | JsonParseException e) { + throw new OAuthAuthenticationException(e.getMessage(), e); + } + } + + @Override + public OAuthToken getToken(String userId) throws IOException { + final OAuthToken token = super.getToken(userId); + try { + if (token == null + || token.getToken() == null + || token.getToken().isEmpty() + || getJson(gitlabUserEndpoint, token.getToken(), GitLabUser.class) == null) { + return null; + } + } catch (OAuthAuthenticationException e) { + return null; + } + return token; + } +} 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 new file mode 100644 index 0000000000..62c493aef3 --- /dev/null +++ b/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProvider.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2012-2021 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.Inject; +import javax.inject.Named; +import javax.inject.Provider; +import javax.inject.Singleton; +import org.eclipse.che.api.auth.shared.dto.OAuthToken; +import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.security.oauth.shared.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides implementation of GitLab {@link OAuthAuthenticator} based on available configuration. + * + * @author Pavol Baran + */ +@Singleton +public class GitLabOAuthAuthenticatorProvider implements Provider { + private static final Logger LOG = LoggerFactory.getLogger(GitLabOAuthAuthenticatorProvider.class); + private final OAuthAuthenticator authenticator; + + @Inject + public GitLabOAuthAuthenticatorProvider( + @Nullable @Named("che.oauth2.gitlab.clientid_filepath") String clientIdPath, + @Nullable @Named("che.oauth2.gitlab.clientsecret_filepath") String clientSecretPath, + @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 User getUser(OAuthToken accessToken) throws OAuthAuthenticationException { + throw new OAuthAuthenticationException( + "The fallback noop authenticator cannot be used for GitLab authentication. Make sure OAuth is properly configured."); + } + + @Override + public String getOAuthProvider() { + return "Noop"; + } + } +} 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/src/main/java/org/eclipse/che/security/oauth/GitLabUser.java new file mode 100644 index 0000000000..2e463bef43 --- /dev/null +++ b/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabUser.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2012-2021 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 org.eclipse.che.security.oauth.shared.User; + +/** + * Represents GitLab user. + * + * @author Pavol Baran + */ +public class GitLabUser implements User { + private String id; + private String name; + private String email; + + @Override + public String getId() { + return id; + } + + @Override + public void setId(String id) { + this.id = id; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public String getEmail() { + return email; + } + + @Override + public void setEmail(String email) { + this.email = email; + } + + @Override + public String toString() { + return "GitLabUser{" + + "id='" + + id + + '\'' + + ", name='" + + name + + '\'' + + ", email='" + + email + + '\'' + + '}'; + } +} diff --git a/wsmaster/che-core-api-auth-gitlab/src/test/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProviderTest.java b/wsmaster/che-core-api-auth-gitlab/src/test/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProviderTest.java new file mode 100644 index 0000000000..af846056a2 --- /dev/null +++ b/wsmaster/che-core-api-auth-gitlab/src/test/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProviderTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2012-2021 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 org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import com.google.common.io.Files; +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class GitLabOAuthAuthenticatorProviderTest { + private static final String TEST_URI = "https://gitlab.com"; + private File credentialFile; + private File emptyFile; + + @BeforeClass + public void setup() throws IOException { + credentialFile = File.createTempFile("GitLabOAuthAuthenticatorProviderTest-", "-credentials"); + Files.asCharSink(credentialFile, Charset.defaultCharset()).write("id/secret"); + credentialFile.deleteOnExit(); + emptyFile = File.createTempFile("GitLabOAuthAuthenticatorProviderTest-", "-empty"); + emptyFile.deleteOnExit(); + } + + @Test(dataProvider = "noopConfig") + public void shouldProvideNoopAuthenticatorWhenInvalidConfigurationSet( + String gitHubClientIdPath, String gitHubClientSecretPath, String gitlabEndpoint) + throws IOException { + // given + GitLabOAuthAuthenticatorProvider provider = + new GitLabOAuthAuthenticatorProvider( + gitHubClientIdPath, gitHubClientSecretPath, gitlabEndpoint, "che.api"); + // when + OAuthAuthenticator authenticator = provider.get(); + // then + assertNotNull(authenticator); + assertTrue( + GitLabOAuthAuthenticatorProvider.NoopOAuthAuthenticator.class.isAssignableFrom( + authenticator.getClass())); + } + + @Test + public void shouldProvideNoopAuthenticatorWhenConfigFilesAreEmpty() throws IOException { + // given + GitLabOAuthAuthenticatorProvider provider = + new GitLabOAuthAuthenticatorProvider( + emptyFile.getPath(), emptyFile.getPath(), TEST_URI, "che.api"); + // when + OAuthAuthenticator authenticator = provider.get(); + // then + assertNotNull(authenticator); + assertTrue( + GitLabOAuthAuthenticatorProvider.NoopOAuthAuthenticator.class.isAssignableFrom( + authenticator.getClass())); + } + + @Test + public void shouldProvideValidGitLabOAuthAuthenticator() throws IOException { + // given + GitLabOAuthAuthenticatorProvider provider = + new GitLabOAuthAuthenticatorProvider( + credentialFile.getPath(), credentialFile.getPath(), TEST_URI, "che.api"); + // when + OAuthAuthenticator authenticator = provider.get(); + + // then + assertNotNull(authenticator); + assertTrue(GitLabOAuthAuthenticator.class.isAssignableFrom(authenticator.getClass())); + } + + @DataProvider(name = "noopConfig") + public Object[][] noopConfig() { + return new Object[][] { + {null, null, null}, + {null, null, TEST_URI}, + {"", "", TEST_URI}, + {"", emptyFile.getPath(), TEST_URI}, + {emptyFile.getPath(), "", TEST_URI}, + {emptyFile.getPath(), emptyFile.getPath(), null}, + {credentialFile.getPath(), credentialFile.getPath(), null}, + {null, emptyFile.getPath(), TEST_URI}, + {emptyFile.getPath(), null, TEST_URI}, + {credentialFile.getPath(), null, TEST_URI}, + {null, credentialFile.getPath(), TEST_URI}, + {credentialFile.getPath(), "", TEST_URI}, + {"", credentialFile.getPath(), TEST_URI}, + {credentialFile.getPath(), null, null}, + {credentialFile.getPath(), credentialFile.getPath(), ""}, + {credentialFile.getPath(), credentialFile.getPath(), null}, + {credentialFile.getPath(), emptyFile.getPath(), TEST_URI}, + {emptyFile.getPath(), credentialFile.getPath(), TEST_URI}, + }; + } +} 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 f27d6eb8ef..f6d7aa0282 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 @@ -14,6 +14,7 @@ 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; @@ -179,6 +180,8 @@ public class GitlabOAuthTokenFetcher implements PersonalAccessTokenFetcher { return apiEndpoint + "/oauth/authenticate?oauth_provider=" + OAUTH_PROVIDER_NAME + + "&scope=" + + Joiner.on('+').join(DEFAULT_TOKEN_SCOPES) + "&request_method=POST&signature_method=rsa"; } diff --git a/wsmaster/pom.xml b/wsmaster/pom.xml index c41321885e..85a8f4ebc1 100644 --- a/wsmaster/pom.xml +++ b/wsmaster/pom.xml @@ -28,6 +28,7 @@ che-core-api-auth che-core-api-auth-bitbucket che-core-api-auth-github + che-core-api-auth-gitlab che-core-api-auth-openshift che-core-api-workspace-shared che-core-api-workspace