Merge pull request #305 from eclipse-che/che-20938

chore: Apply git user data configmap to mount to /etc/gitconfig
pull/309/head
Valeriy Svydenko 2022-05-27 11:56:49 +03:00 committed by GitHub
commit 68c1af03aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 923 additions and 6 deletions

View File

@ -125,6 +125,10 @@
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-dto</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-factory</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-model</artifactId>

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* Copyright (c) 2012-2022 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/
@ -49,6 +49,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.environment.Kubernete
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironmentFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.RemoveNamespaceOnWorkspaceRemove;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.CredentialsSecretConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.GitconfigUserDataConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.PreferencesConfigMapConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.SshKeysConfigurator;
@ -118,6 +119,7 @@ public class KubernetesInfraModule extends AbstractModule {
namespaceConfigurators.addBinding().to(UserProfileConfigurator.class);
namespaceConfigurators.addBinding().to(UserPreferencesConfigurator.class);
namespaceConfigurators.addBinding().to(SshKeysConfigurator.class);
namespaceConfigurators.addBinding().to(GitconfigUserDataConfigurator.class);
bind(KubernetesNamespaceService.class);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* Copyright (c) 2012-2022 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/
@ -53,6 +53,7 @@ public abstract class AbstractWorkspaceServiceAccount<
public static final String CONFIGMAPS_ROLE_NAME = "workspace-configmaps";
public static final String CREDENTIALS_SECRET_NAME = "workspace-credentials-secret";
public static final String PREFERENCES_CONFIGMAP_NAME = "workspace-preferences-configmap";
public static final String GIT_USERDATA_CONFIGMAP_NAME = "workspace-userdata-gitconfig-configmap";
protected final String namespace;
protected final String serviceAccountName;

View File

@ -0,0 +1,111 @@
/*
* Copyright (c) 2012-2022 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.workspace.infrastructure.kubernetes.namespace.configurator;
import static java.util.Collections.singletonMap;
import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount.GIT_USERDATA_CONFIGMAP_NAME;
import com.google.common.collect.ImmutableMap;
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import org.eclipse.che.api.factory.server.scm.GitUserData;
import org.eclipse.che.api.factory.server.scm.GitUserDataFetcher;
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class GitconfigUserDataConfigurator implements NamespaceConfigurator {
private static final Logger LOG = LoggerFactory.getLogger(GitconfigUserDataConfigurator.class);
private final KubernetesClientFactory clientFactory;
private final Set<GitUserDataFetcher> gitUserDataFetchers;
private static final String CONFIGMAP_DATA_KEY = "gitconfig";
@Inject
public GitconfigUserDataConfigurator(
KubernetesClientFactory clientFactory, Set<GitUserDataFetcher> gitUserDataFetchers) {
this.clientFactory = clientFactory;
this.gitUserDataFetchers = gitUserDataFetchers;
}
@Override
public void configure(NamespaceResolutionContext namespaceResolutionContext, String namespaceName)
throws InfrastructureException {
var client = clientFactory.create();
GitUserData gitUserData = null;
for (GitUserDataFetcher fetcher : gitUserDataFetchers) {
try {
gitUserData = fetcher.fetchGitUserData();
break;
} catch (ScmUnauthorizedException | ScmCommunicationException e) {
LOG.debug("No GitUserDataFetcher is configured. " + e.getMessage());
}
}
Map<String, String> annotations =
ImmutableMap.of(
"controller.devfile.io/mount-as",
"subpath",
"controller.devfile.io/mount-path",
"/etc/");
Map<String, String> labels =
ImmutableMap.of(
"controller.devfile.io/mount-to-devworkspace",
"true",
"controller.devfile.io/watch-configmap",
"true");
if (gitUserData != null
&& client
.configMaps()
.inNamespace(namespaceName)
.withName(GIT_USERDATA_CONFIGMAP_NAME)
.get()
== null
&& client
.configMaps()
.inNamespace(namespaceName)
.withLabels(labels)
.list()
.getItems()
.stream()
.noneMatch(
configMap ->
configMap
.getMetadata()
.getAnnotations()
.entrySet()
.containsAll(annotations.entrySet())
&& configMap.getData().containsKey(CONFIGMAP_DATA_KEY))) {
ConfigMap configMap =
new ConfigMapBuilder()
.withNewMetadata()
.withName(GIT_USERDATA_CONFIGMAP_NAME)
.withLabels(labels)
.withAnnotations(annotations)
.endMetadata()
.build();
configMap.setData(
singletonMap(
CONFIGMAP_DATA_KEY,
String.format(
"[user]\n\tname = %1$s\n\temail = %2$s",
gitUserData.getScmUsername(), gitUserData.getScmUserEmail())));
client.configMaps().inNamespace(namespaceName).create(configMap);
}
}
}

View File

@ -0,0 +1,145 @@
/*
* Copyright (c) 2012-2022 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.workspace.infrastructure.kubernetes.namespace.configurator;
import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount.GIT_USERDATA_CONFIGMAP_NAME;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableMap;
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import org.eclipse.che.api.factory.server.scm.GitUserData;
import org.eclipse.che.api.factory.server.scm.GitUserDataFetcher;
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(MockitoTestNGListener.class)
public class GitconfigUserdataConfiguratorTest {
private NamespaceConfigurator configurator;
@Mock private KubernetesClientFactory clientFactory;
@Mock private GitUserDataFetcher gitUserDataFetcher;
private KubernetesServer serverMock;
private NamespaceResolutionContext namespaceResolutionContext;
private final String TEST_NAMESPACE_NAME = "namespace123";
private final String TEST_WORKSPACE_ID = "workspace123";
private final String TEST_USER_ID = "user123";
private final String TEST_USERNAME = "jondoe";
@BeforeMethod
public void setUp()
throws InfrastructureException, ScmCommunicationException, ScmUnauthorizedException {
configurator = new GitconfigUserDataConfigurator(clientFactory, Set.of(gitUserDataFetcher));
serverMock = new KubernetesServer(true, true);
serverMock.before();
KubernetesClient client = spy(serverMock.getClient());
when(clientFactory.create()).thenReturn(client);
namespaceResolutionContext =
new NamespaceResolutionContext(TEST_WORKSPACE_ID, TEST_USER_ID, TEST_USERNAME);
}
@Test
public void createUserdataConfigmapWhenDoesNotExist()
throws ScmCommunicationException, ScmUnauthorizedException, InfrastructureException,
InterruptedException {
// given
when(gitUserDataFetcher.fetchGitUserData()).thenReturn(new GitUserData("gitUser", "gitEmail"));
// when
configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME);
// then create a secret
Assert.assertEquals(serverMock.getLastRequest().getMethod(), "POST");
Assert.assertNotNull(
serverMock
.getClient()
.configMaps()
.inNamespace(TEST_NAMESPACE_NAME)
.withName(GIT_USERDATA_CONFIGMAP_NAME)
.get());
}
@Test
public void doNothingWhenGitUserDataIsNull()
throws InfrastructureException, InterruptedException {
// when
configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME);
// then - don't create the configmap
var configMaps =
serverMock.getClient().configMaps().inNamespace(TEST_NAMESPACE_NAME).list().getItems();
Assert.assertEquals(configMaps.size(), 0);
}
@Test
public void doNothingWhenSecretAlreadyExists()
throws InfrastructureException, InterruptedException, ScmCommunicationException,
ScmUnauthorizedException {
// given
when(gitUserDataFetcher.fetchGitUserData()).thenReturn(new GitUserData("gitUser", "gitEmail"));
Map<String, String> annotations =
ImmutableMap.of(
"controller.devfile.io/mount-as",
"subpath",
"controller.devfile.io/mount-path",
"/etc/",
"already",
"created");
Map<String, String> labels =
ImmutableMap.of(
"controller.devfile.io/mount-to-devworkspace",
"true",
"controller.devfile.io/watch-configmap",
"true",
"already",
"created");
ConfigMap configMap =
new ConfigMapBuilder()
.withNewMetadata()
.withName(GIT_USERDATA_CONFIGMAP_NAME)
.withLabels(labels)
.withAnnotations(annotations)
.endMetadata()
.build();
configMap.setData(Collections.singletonMap("gitconfig", "empty"));
serverMock.getClient().configMaps().inNamespace(TEST_NAMESPACE_NAME).create(configMap);
// when
configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME);
// then - don't create the configmap
Assert.assertEquals(serverMock.getLastRequest().getMethod(), "GET");
var configMaps =
serverMock.getClient().configMaps().inNamespace(TEST_NAMESPACE_NAME).list().getItems();
Assert.assertEquals(configMaps.size(), 1);
Assert.assertEquals(configMaps.get(0).getMetadata().getAnnotations().get("already"), "created");
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* Copyright (c) 2012-2022 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/
@ -54,6 +54,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.environment.Kubernete
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironmentFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.CredentialsSecretConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.GitconfigUserDataConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.PreferencesConfigMapConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.SshKeysConfigurator;
@ -127,6 +128,7 @@ public class OpenShiftInfraModule extends AbstractModule {
namespaceConfigurators.addBinding().to(OpenShiftWorkspaceServiceAccountConfigurator.class);
namespaceConfigurators.addBinding().to(OpenShiftStopWorkspaceRoleConfigurator.class);
namespaceConfigurators.addBinding().to(SshKeysConfigurator.class);
namespaceConfigurators.addBinding().to(GitconfigUserDataConfigurator.class);
bind(KubernetesNamespaceService.class);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* Copyright (c) 2012-2022 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/
@ -14,6 +14,7 @@ package org.eclipse.che.api.factory.server.bitbucket;
import com.google.inject.AbstractModule;
import com.google.inject.multibindings.Multibinder;
import org.eclipse.che.api.factory.server.bitbucket.server.BitbucketServerApiClient;
import org.eclipse.che.api.factory.server.scm.GitUserDataFetcher;
import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenFetcher;
import org.eclipse.che.security.oauth1.BitbucketServerApiProvider;
@ -24,5 +25,8 @@ public class BitbucketServerModule extends AbstractModule {
Multibinder.newSetBinder(binder(), PersonalAccessTokenFetcher.class);
tokenFetcherMultibinder.addBinding().to(BitbucketServerPersonalAccessTokenFetcher.class);
bind(BitbucketServerApiClient.class).toProvider(BitbucketServerApiProvider.class);
Multibinder<GitUserDataFetcher> gitUserDataMultibinder =
Multibinder.newSetBinder(binder(), GitUserDataFetcher.class);
gitUserDataMultibinder.addBinding().to(BitbucketServerUserDataFetcher.class);
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright (c) 2012-2022 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.api.factory.server.bitbucket;
import static java.util.stream.Collectors.toList;
import com.google.common.base.Splitter;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import org.eclipse.che.api.factory.server.bitbucket.server.BitbucketServerApiClient;
import org.eclipse.che.api.factory.server.bitbucket.server.BitbucketUser;
import org.eclipse.che.api.factory.server.scm.GitUserData;
import org.eclipse.che.api.factory.server.scm.GitUserDataFetcher;
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.env.EnvironmentContext;
import org.eclipse.che.commons.lang.StringUtils;
import org.eclipse.che.commons.subject.Subject;
/** Bitbucket git user data retriever. */
public class BitbucketServerUserDataFetcher implements GitUserDataFetcher {
/** Bitbucket API client. */
private final BitbucketServerApiClient bitbucketServerApiClient;
private final List<String> registeredBitbucketEndpoints;
@Inject
public BitbucketServerUserDataFetcher(
BitbucketServerApiClient bitbucketServerApiClient,
@Nullable @Named("che.integration.bitbucket.server_endpoints") String bitbucketEndpoints) {
if (bitbucketEndpoints != null) {
this.registeredBitbucketEndpoints =
Splitter.on(",")
.splitToStream(bitbucketEndpoints)
.map(e -> StringUtils.trimEnd(e, '/'))
.collect(toList());
} else {
this.registeredBitbucketEndpoints = Collections.emptyList();
}
this.bitbucketServerApiClient = bitbucketServerApiClient;
}
@Override
public GitUserData fetchGitUserData() throws ScmUnauthorizedException, ScmCommunicationException {
GitUserData gitUserData = null;
for (String bitbucketServerEndpoint : this.registeredBitbucketEndpoints) {
if (bitbucketServerApiClient.isConnected(bitbucketServerEndpoint)) {
Subject cheSubject = EnvironmentContext.getCurrent().getSubject();
try {
BitbucketUser user = bitbucketServerApiClient.getUser(cheSubject);
gitUserData = new GitUserData(user.getName(), user.getEmailAddress());
} catch (ScmItemNotFoundException e) {
throw new ScmCommunicationException(e.getMessage(), e);
}
break;
}
}
if (gitUserData == null) {
throw new ScmCommunicationException("Failed to retrieve git user data from Bitbucket");
}
return gitUserData;
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) 2012-2022 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.api.factory.server.bitbucket;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import java.net.MalformedURLException;
import org.eclipse.che.api.factory.server.bitbucket.server.BitbucketServerApiClient;
import org.eclipse.che.api.factory.server.bitbucket.server.BitbucketUser;
import org.eclipse.che.api.factory.server.scm.GitUserData;
import org.eclipse.che.api.factory.server.scm.exception.ScmBadRequestException;
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
import org.eclipse.che.api.factory.server.scm.exception.ScmItemNotFoundException;
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.subject.Subject;
import org.eclipse.che.commons.subject.SubjectImpl;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(MockitoTestNGListener.class)
public class BitbucketServerUserDataFetcherTest {
String someBitbucketURL = "https://some.bitbucketserver.com";
Subject subject;
@Mock BitbucketServerApiClient bitbucketServerApiClient;
BitbucketUser bitbucketUser;
BitbucketServerUserDataFetcher fetcher;
@BeforeMethod
public void setup() throws MalformedURLException {
subject = new SubjectImpl("another_user", "user987", "token111", false);
bitbucketUser =
new BitbucketUser("User", "user", 32423523, "NORMAL", true, "user", "user@users.com");
fetcher = new BitbucketServerUserDataFetcher(bitbucketServerApiClient, someBitbucketURL);
EnvironmentContext context = new EnvironmentContext();
context.setSubject(subject);
EnvironmentContext.setCurrent(context);
}
@Test
public void shouldBeAbleToFetchPersonalAccessToken()
throws ScmUnauthorizedException, ScmCommunicationException, ScmItemNotFoundException,
ScmBadRequestException {
// given
when(bitbucketServerApiClient.isConnected(eq(someBitbucketURL))).thenReturn(true);
when(bitbucketServerApiClient.getUser(eq(subject))).thenReturn(bitbucketUser);
// when
GitUserData gitUserData = fetcher.fetchGitUserData();
// then
assertEquals(gitUserData.getScmUsername(), "user");
assertEquals(gitUserData.getScmUserEmail(), "user@users.com");
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* Copyright (c) 2012-2022 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/
@ -13,6 +13,7 @@ package org.eclipse.che.api.factory.server.github;
import com.google.inject.AbstractModule;
import com.google.inject.multibindings.Multibinder;
import org.eclipse.che.api.factory.server.scm.GitUserDataFetcher;
import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenFetcher;
public class GithubModule extends AbstractModule {
@ -22,5 +23,8 @@ public class GithubModule extends AbstractModule {
Multibinder<PersonalAccessTokenFetcher> tokenFetcherMultibinder =
Multibinder.newSetBinder(binder(), PersonalAccessTokenFetcher.class);
tokenFetcherMultibinder.addBinding().to(GithubPersonalAccessTokenFetcher.class);
Multibinder<GitUserDataFetcher> gitUserDataMultibinder =
Multibinder.newSetBinder(binder(), GitUserDataFetcher.class);
gitUserDataMultibinder.addBinding().to(GithubUserDataFetcher.class);
}
}

View File

@ -0,0 +1,100 @@
/*
* Copyright (c) 2012-2022 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.github;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
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.GitUserData;
import org.eclipse.che.api.factory.server.scm.GitUserDataFetcher;
import org.eclipse.che.api.factory.server.scm.exception.ScmBadRequestException;
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
import org.eclipse.che.api.factory.server.scm.exception.ScmItemNotFoundException;
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.subject.Subject;
import org.eclipse.che.security.oauth.OAuthAPI;
/** GitHub user data retriever. */
public class GithubUserDataFetcher implements GitUserDataFetcher {
private final String apiEndpoint;
private final OAuthAPI oAuthAPI;
/** GitHub API client. */
private final GithubApiClient githubApiClient;
/** Name of this OAuth provider as found in OAuthAPI. */
private static final String OAUTH_PROVIDER_NAME = "github";
/** Collection of OAuth scopes required to make integration with GitHub work. */
public static final Set<String> DEFAULT_TOKEN_SCOPES = ImmutableSet.of("repo");
@Inject
public GithubUserDataFetcher(@Named("che.api") String apiEndpoint, OAuthAPI oAuthAPI) {
this(apiEndpoint, oAuthAPI, new GithubApiClient());
}
/** Constructor used for testing only. */
public GithubUserDataFetcher(
String apiEndpoint, OAuthAPI oAuthAPI, GithubApiClient githubApiClient) {
this.apiEndpoint = apiEndpoint;
this.oAuthAPI = oAuthAPI;
this.githubApiClient = githubApiClient;
}
@Override
public GitUserData fetchGitUserData() throws ScmUnauthorizedException, ScmCommunicationException {
OAuthToken oAuthToken;
try {
oAuthToken = oAuthAPI.getToken(OAUTH_PROVIDER_NAME);
// Find the user associated to the OAuth token by querying the GitHub API.
GithubUser user = githubApiClient.getUser(oAuthToken.getToken());
return new GitUserData(user.getName(), user.getEmail());
} catch (UnauthorizedException e) {
Subject cheSubject = EnvironmentContext.getCurrent().getSubject();
throw new ScmUnauthorizedException(
cheSubject.getUserName()
+ " is not authorized in "
+ OAUTH_PROVIDER_NAME
+ " OAuth provider.",
OAUTH_PROVIDER_NAME,
"2.0",
getLocalAuthenticateUrl());
} catch (NotFoundException
| ServerException
| ForbiddenException
| BadRequestException
| ScmItemNotFoundException
| ScmBadRequestException
| ConflictException e) {
throw new ScmCommunicationException(e.getMessage(), e);
}
}
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";
}
}

View File

@ -0,0 +1,86 @@
/*
* Copyright (c) 2012-2022 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.github;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static org.eclipse.che.dto.server.DtoFactory.newDto;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.common.Slf4jNotifier;
import com.google.common.net.HttpHeaders;
import org.eclipse.che.api.auth.shared.dto.OAuthToken;
import org.eclipse.che.api.factory.server.scm.GitUserData;
import org.eclipse.che.security.oauth.OAuthAPI;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(MockitoTestNGListener.class)
public class GithubGitUserDataFetcherTest {
@Mock OAuthAPI oAuthAPI;
GithubUserDataFetcher githubGUDFetcher;
final int httpPort = 3301;
WireMockServer wireMockServer;
WireMock wireMock;
final String githubOauthToken = "gho_token1";
@BeforeMethod
void start() {
wireMockServer =
new WireMockServer(wireMockConfig().notifier(new Slf4jNotifier(false)).port(httpPort));
wireMockServer.start();
WireMock.configureFor("localhost", httpPort);
wireMock = new WireMock("localhost", httpPort);
githubGUDFetcher =
new GithubUserDataFetcher(
"http://che.api", oAuthAPI, new GithubApiClient(wireMockServer.url("/")));
stubFor(
get(urlEqualTo("/user"))
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("token " + githubOauthToken))
.willReturn(
aResponse()
.withHeader("Content-Type", "application/json; charset=utf-8")
.withHeader(GithubApiClient.GITHUB_OAUTH_SCOPES_HEADER, "repo")
.withBodyFile("github/rest/user/response.json")));
}
@AfterMethod
void stop() {
wireMockServer.stop();
}
@Test
public void shouldFetchGitUserData() throws Exception {
OAuthToken oAuthToken = newDto(OAuthToken.class).withToken(githubOauthToken).withScope("repo");
when(oAuthAPI.getToken(anyString())).thenReturn(oAuthToken);
GitUserData gitUserData = githubGUDFetcher.fetchGitUserData();
assertEquals(gitUserData.getScmUsername(), "Github User");
assertEquals(gitUserData.getScmUserEmail(), "github-user@acme.com");
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* Copyright (c) 2012-2022 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/
@ -13,6 +13,7 @@ package org.eclipse.che.api.factory.server.gitlab;
import com.google.inject.AbstractModule;
import com.google.inject.multibindings.Multibinder;
import org.eclipse.che.api.factory.server.scm.GitUserDataFetcher;
import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenFetcher;
public class GitlabModule extends AbstractModule {
@ -22,5 +23,8 @@ public class GitlabModule extends AbstractModule {
Multibinder<PersonalAccessTokenFetcher> tokenFetcherMultibinder =
Multibinder.newSetBinder(binder(), PersonalAccessTokenFetcher.class);
tokenFetcherMultibinder.addBinding().to(GitlabOAuthTokenFetcher.class);
Multibinder<GitUserDataFetcher> gitUserDataMultibinder =
Multibinder.newSetBinder(binder(), GitUserDataFetcher.class);
gitUserDataMultibinder.addBinding().to(GitlabUserDataFetcher.class);
}
}

View File

@ -0,0 +1,138 @@
/*
* Copyright (c) 2012-2022 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 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.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.GitUserData;
import org.eclipse.che.api.factory.server.scm.GitUserDataFetcher;
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.env.EnvironmentContext;
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;
/** Gitlab OAuth token retriever. */
public class GitlabUserDataFetcher implements GitUserDataFetcher {
private final String apiEndpoint;
private final OAuthAPI oAuthAPI;
/** Name of this OAuth provider as found in OAuthAPI. */
private static final String OAUTH_PROVIDER_NAME = "gitlab";
private final List<String> registeredGitlabEndpoints;
public static final Set<String> 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,
@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 GitUserData fetchGitUserData() throws ScmUnauthorizedException, ScmCommunicationException {
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 = oAuthAPI.getToken(OAUTH_PROVIDER_NAME);
} catch (UnauthorizedException e) {
Subject cheSubject = EnvironmentContext.getCurrent().getSubject();
throw new ScmUnauthorizedException(
cheSubject.getUserName()
+ " is not authorized in "
+ OAUTH_PROVIDER_NAME
+ " OAuth provider.",
OAUTH_PROVIDER_NAME,
"2.0",
getLocalAuthenticateUrl());
} catch (NotFoundException
| ServerException
| ForbiddenException
| BadRequestException
| ConflictException e) {
throw new ScmCommunicationException(e.getMessage(), e);
}
GitUserData gitUserData = null;
for (String gitlabServerEndpoint : this.registeredGitlabEndpoints) {
try {
GitlabUser user = new GitlabApiClient(gitlabServerEndpoint).getUser(oAuthToken.getToken());
gitUserData = new GitUserData(user.getName(), user.getEmail());
break;
} catch (ScmItemNotFoundException | ScmBadRequestException e) {
throw new ScmCommunicationException(e.getMessage(), e);
}
}
if (gitUserData == null) {
throw new ScmCommunicationException("Failed to retrieve git user data from Gitlab");
}
return gitUserData;
}
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";
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright (c) 2012-2022 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.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static org.eclipse.che.dto.server.DtoFactory.newDto;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.common.Slf4jNotifier;
import com.google.common.net.HttpHeaders;
import org.eclipse.che.api.auth.shared.dto.OAuthToken;
import org.eclipse.che.api.factory.server.scm.GitUserData;
import org.eclipse.che.security.oauth.OAuthAPI;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(MockitoTestNGListener.class)
public class GitlabUserDataFetcherTest {
@Mock OAuthAPI oAuthAPI;
GitlabUserDataFetcher gitlabUserDataFetcher;
WireMockServer wireMockServer;
WireMock wireMock;
@BeforeMethod
void start() {
wireMockServer =
new WireMockServer(wireMockConfig().notifier(new Slf4jNotifier(false)).dynamicPort());
wireMockServer.start();
WireMock.configureFor("localhost", wireMockServer.port());
wireMock = new WireMock("localhost", wireMockServer.port());
gitlabUserDataFetcher =
new GitlabUserDataFetcher(
wireMockServer.url("/"), wireMockServer.url("/"), "http://che.api", oAuthAPI);
stubFor(
get(urlEqualTo("/api/v4/user"))
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer oauthtoken"))
.willReturn(
aResponse()
.withHeader("Content-Type", "application/json; charset=utf-8")
.withBodyFile("gitlab/rest/api/v4/user/response.json")));
}
@AfterMethod
void stop() {
wireMockServer.stop();
}
@Test
public void shouldFetchGitUserData() throws Exception {
OAuthToken oAuthToken =
newDto(OAuthToken.class).withToken("oauthtoken").withScope("api write_repository openid");
when(oAuthAPI.getToken(anyString())).thenReturn(oAuthToken);
GitUserData gitUserData = gitlabUserDataFetcher.fetchGitUserData();
assertEquals(gitUserData.getScmUsername(), "John Smith");
assertEquals(gitUserData.getScmUserEmail(), "john@example.com");
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2012-2022 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.scm;
import java.util.Objects;
/** Personal SCM user data such as `username` and `email`. Is used to sign git commits. */
public class GitUserData {
private final String scmUsername;
private final String scmUserEmail;
public GitUserData(String scmUsername, String scmUserEmail) {
this.scmUsername = scmUsername;
this.scmUserEmail = scmUserEmail;
}
public String getScmUsername() {
return scmUsername;
}
public String getScmUserEmail() {
return scmUserEmail;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GitUserData that = (GitUserData) o;
return Objects.equals(scmUsername, that.scmUsername)
&& Objects.equals(scmUserEmail, that.scmUserEmail);
}
@Override
public int hashCode() {
return Objects.hash(scmUsername, scmUserEmail);
}
@Override
public String toString() {
return "GitUserData{"
+ ", scmUsername='"
+ scmUsername
+ '\''
+ ", scmUserEmail='"
+ scmUserEmail
+ '\''
+ '}';
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2012-2022 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.scm;
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
public interface GitUserDataFetcher {
/**
* Retrieve a {@link GitUserData} object from concrete scm provider
*
* @return - {@link GitUserData} object.
* @throws ScmUnauthorizedException - in case if user is not authorized che server to create a new
* token. Further user interaction is needed before calling this method next time.
* @throws ScmCommunicationException - Some unexpected problem occurred during communication with
* scm provider.
*/
GitUserData fetchGitUserData() throws ScmUnauthorizedException, ScmCommunicationException;
}