diff --git a/infrastructures/infrastructure-factory/src/main/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesPersonalAccessTokenManager.java b/infrastructures/infrastructure-factory/src/main/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesPersonalAccessTokenManager.java index f122cbddb8..8e8a662d34 100644 --- a/infrastructures/infrastructure-factory/src/main/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesPersonalAccessTokenManager.java +++ b/infrastructures/infrastructure-factory/src/main/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesPersonalAccessTokenManager.java @@ -14,7 +14,6 @@ package org.eclipse.che.api.factory.server.scm.kubernetes; import static com.google.common.base.Strings.isNullOrEmpty; import static org.eclipse.che.commons.lang.StringUtils.trimEnd; -import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import io.fabric8.kubernetes.api.model.LabelSelector; import io.fabric8.kubernetes.api.model.LabelSelectorBuilder; @@ -87,8 +86,8 @@ public class KubernetesPersonalAccessTokenManager implements PersonalAccessToken this.gitCredentialManager = gitCredentialManager; } - @VisibleForTesting - void save(PersonalAccessToken personalAccessToken) + @Override + public void store(PersonalAccessToken personalAccessToken) throws UnsatisfiedScmPreconditionException, ScmConfigurationPersistenceException { try { String namespace = getFirstNamespace(); @@ -136,7 +135,7 @@ public class KubernetesPersonalAccessTokenManager implements PersonalAccessToken ScmUnauthorizedException, ScmCommunicationException, UnknownScmProviderException { PersonalAccessToken personalAccessToken = scmPersonalAccessTokenFetcher.fetchPersonalAccessToken(cheUser, scmServerUrl); - save(personalAccessToken); + store(personalAccessToken); return personalAccessToken; } @@ -291,7 +290,7 @@ public class KubernetesPersonalAccessTokenManager implements PersonalAccessToken } @Override - public void store(String scmServerUrl) + public void storeGitCredentials(String scmServerUrl) throws UnsatisfiedScmPreconditionException, ScmConfigurationPersistenceException, ScmCommunicationException, ScmUnauthorizedException { Subject subject = EnvironmentContext.getCurrent().getSubject(); diff --git a/infrastructures/infrastructure-factory/src/test/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesPersonalAccessTokenManagerTest.java b/infrastructures/infrastructure-factory/src/test/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesPersonalAccessTokenManagerTest.java index 47cfdb423f..e0036151d6 100644 --- a/infrastructures/infrastructure-factory/src/test/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesPersonalAccessTokenManagerTest.java +++ b/infrastructures/infrastructure-factory/src/test/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesPersonalAccessTokenManagerTest.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/ @@ -142,7 +142,7 @@ public class KubernetesPersonalAccessTokenManagerTest { "https://bitbucket.com", "cheUser", "username", "token-name", "tid-24", "token123"); // when - personalAccessTokenManager.save(token); + personalAccessTokenManager.store(token); // then verify(nonNamespaceOperation).createOrReplace(captor.capture()); diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/CredentialsSecretConfigurator.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/CredentialsSecretConfigurator.java index 016df5eeef..cf52218b62 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/CredentialsSecretConfigurator.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/CredentialsSecretConfigurator.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/ @@ -73,7 +73,7 @@ public class CredentialsSecretConfigurator implements NamespaceConfigurator { .forEach( s -> { try { - personalAccessTokenManager.store( + personalAccessTokenManager.storeGitCredentials( s.getMetadata().getAnnotations().get(ANNOTATION_SCM_URL)); } catch (ScmCommunicationException | ScmConfigurationPersistenceException diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/CredentialsSecretConfiguratorTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/CredentialsSecretConfiguratorTest.java index bb12fa96e1..00f0269eae 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/CredentialsSecretConfiguratorTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/CredentialsSecretConfiguratorTest.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/ @@ -91,7 +91,7 @@ public class CredentialsSecretConfiguratorTest { configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME); // then - verify(personalAccessTokenManager).store(eq("test-url")); + verify(personalAccessTokenManager).storeGitCredentials(eq("test-url")); } @Test @@ -129,6 +129,6 @@ public class CredentialsSecretConfiguratorTest { configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME); // then - verify(personalAccessTokenManager, never()).store(anyString()); + verify(personalAccessTokenManager, never()).storeGitCredentials(anyString()); } } diff --git a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/EmbeddedOAuthAPI.java b/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/EmbeddedOAuthAPI.java index 28886d58e3..ec317aa5c3 100644 --- a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/EmbeddedOAuthAPI.java +++ b/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/EmbeddedOAuthAPI.java @@ -14,6 +14,7 @@ package org.eclipse.che.security.oauth; import static com.google.common.base.Strings.isNullOrEmpty; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Collections.emptyList; +import static org.eclipse.che.api.factory.server.scm.PersonalAccessTokenFetcher.OAUTH_2_PREFIX; import static org.eclipse.che.commons.lang.UrlUtils.*; import static org.eclipse.che.commons.lang.UrlUtils.getParameter; import static org.eclipse.che.dto.server.DtoFactory.newDto; @@ -43,7 +44,10 @@ import org.eclipse.che.api.core.util.LinksHelper; 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.ScmConfigurationPersistenceException; +import org.eclipse.che.api.factory.server.scm.exception.UnsatisfiedScmPreconditionException; +import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.commons.env.EnvironmentContext; +import org.eclipse.che.commons.lang.NameGenerator; import org.eclipse.che.commons.subject.Subject; import org.eclipse.che.security.oauth.shared.dto.OAuthAuthenticatorDescriptor; import org.slf4j.Logger; @@ -84,7 +88,8 @@ public class EmbeddedOAuthAPI implements OAuthAPI { } @Override - public Response callback(UriInfo uriInfo, List errorValues) throws NotFoundException { + public Response callback(UriInfo uriInfo, @Nullable List errorValues) + throws NotFoundException { URL requestUrl = getRequestUrl(uriInfo); Map> params = getQueryParametersFromState(getState(requestUrl)); errorValues = errorValues == null ? uriInfo.getQueryParameters().get("error") : errorValues; @@ -97,13 +102,25 @@ public class EmbeddedOAuthAPI implements OAuthAPI { OAuthAuthenticator oauth = getAuthenticator(providerName); final List scopes = params.get("scope"); try { - oauth.callback(requestUrl, scopes == null ? emptyList() : scopes); + String token = oauth.callback(requestUrl, scopes == null ? emptyList() : scopes); + personalAccessTokenManager.store( + new PersonalAccessToken( + oauth.getEndpointUrl(), + EnvironmentContext.getCurrent().getSubject().getUserId(), + null, + null, + NameGenerator.generate(OAUTH_2_PREFIX, 5), + NameGenerator.generate("id-", 5), + token)); } catch (OAuthAuthenticationException e) { return Response.temporaryRedirect( URI.create( getParameter(params, "redirect_after_login") + String.format("&%s=access_denied", ERROR_QUERY_NAME))) .build(); + } catch (UnsatisfiedScmPreconditionException | ScmConfigurationPersistenceException e) { + // Skip exception, the token will be stored in the next request. + LOG.error(e.getMessage(), e); } final String redirectAfterLogin = getParameter(params, "redirect_after_login"); return Response.temporaryRedirect(URI.create(redirectAfterLogin)).build(); diff --git a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/OAuthAuthenticator.java b/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/OAuthAuthenticator.java index 695f35938e..023da7f91b 100644 --- a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/OAuthAuthenticator.java +++ b/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/OAuthAuthenticator.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/ @@ -174,7 +174,7 @@ public abstract class OAuthAuthenticator { * server * @param scopes specify exactly what type of access needed. This list must be exactly the same as * list passed to the method {@link #getAuthenticateUrl(URL, java.util.List)} - * @return id of authenticated user + * @return access token * @throws OAuthAuthenticationException if authentication failed or requestUrl does * not contain required parameters, e.g. 'code' */ @@ -202,7 +202,7 @@ public abstract class OAuthAuthenticator { userId = EnvironmentContext.getCurrent().getSubject().getUserId(); } flow.createAndStoreCredential(tokenResponse, userId); - return userId; + return tokenResponse.getAccessToken(); } catch (IOException ioe) { throw new OAuthAuthenticationException(ioe.getMessage()); } diff --git a/wsmaster/che-core-api-auth/src/test/java/org/eclipse/che/security/oauth/EmbeddedOAuthAPITest.java b/wsmaster/che-core-api-auth/src/test/java/org/eclipse/che/security/oauth/EmbeddedOAuthAPITest.java index 54056d84f4..5a5e6d61c6 100644 --- a/wsmaster/che-core-api-auth/src/test/java/org/eclipse/che/security/oauth/EmbeddedOAuthAPITest.java +++ b/wsmaster/che-core-api-auth/src/test/java/org/eclipse/che/security/oauth/EmbeddedOAuthAPITest.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,23 +11,33 @@ */ package org.eclipse.che.security.oauth; +import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; +import static org.eclipse.che.api.factory.server.scm.PersonalAccessTokenFetcher.OAUTH_2_PREFIX; import static org.eclipse.che.dto.server.DtoFactory.newDto; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriBuilder; import jakarta.ws.rs.core.UriInfo; import java.lang.reflect.Field; import java.net.URI; +import java.net.URL; import java.util.Set; import org.eclipse.che.api.auth.shared.dto.OAuthToken; import org.eclipse.che.api.core.NotFoundException; +import org.eclipse.che.api.factory.server.scm.PersonalAccessToken; +import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenManager; import org.eclipse.che.security.oauth.shared.dto.OAuthAuthenticatorDescriptor; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.testng.MockitoTestNGListener; @@ -40,6 +50,7 @@ public class EmbeddedOAuthAPITest { @Mock OAuthAuthenticatorProvider oauth2Providers; @Mock org.eclipse.che.security.oauth1.OAuthAuthenticatorProvider oauth1Providers; + @Mock PersonalAccessTokenManager personalAccessTokenManager; @InjectMocks EmbeddedOAuthAPI embeddedOAuthAPI; @@ -101,4 +112,32 @@ public class EmbeddedOAuthAPITest { callback.getLocation().toString(), "http://eclipse.che?quary%3Dparam%26error_code%3Daccess_denied"); } + + @Test + public void shouldStoreTokenOnCallback() throws Exception { + // given + UriInfo uriInfo = mock(UriInfo.class); + OAuthAuthenticator authenticator = mock(OAuthAuthenticator.class); + when(authenticator.getEndpointUrl()).thenReturn("http://eclipse.che"); + when(authenticator.callback(any(URL.class), anyList())).thenReturn("token"); + when(uriInfo.getRequestUri()) + .thenReturn( + new URI( + "http://eclipse.che?state=oauth_provider%3Dgithub%26redirect_after_login%3DredirectUrl")); + when(oauth2Providers.getAuthenticator("github")).thenReturn(authenticator); + ArgumentCaptor tokenCapture = + ArgumentCaptor.forClass(PersonalAccessToken.class); + + // when + embeddedOAuthAPI.callback(uriInfo, emptyList()); + + // then + verify(personalAccessTokenManager).store(tokenCapture.capture()); + PersonalAccessToken token = tokenCapture.getValue(); + assertEquals(token.getScmProviderUrl(), "http://eclipse.che"); + assertEquals(token.getCheUserId(), "0000-00-0000"); + assertTrue(token.getScmTokenId().startsWith("id-")); + assertTrue(token.getScmTokenName().startsWith(OAUTH_2_PREFIX)); + assertEquals(token.getToken(), "token"); + } } diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/scm/PersonalAccessTokenManager.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/scm/PersonalAccessTokenManager.java index 43bdb690cb..7e6572a822 100644 --- a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/scm/PersonalAccessTokenManager.java +++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/scm/PersonalAccessTokenManager.java @@ -105,7 +105,18 @@ public interface PersonalAccessTokenManager { * @throws ScmCommunicationException - problem occurred during communication with scm provider. * @throws ScmUnauthorizedException - scm authorization required. */ - void store(String scmServerUrl) + void storeGitCredentials(String scmServerUrl) throws UnsatisfiedScmPreconditionException, ScmConfigurationPersistenceException, ScmCommunicationException, ScmUnauthorizedException; + + /** + * Store {@link PersonalAccessToken} in permanent storage. + * + * @param token personal access token + * @throws UnsatisfiedScmPreconditionException - storage preconditions aren't met. + * @throws ScmConfigurationPersistenceException - problem occurred during communication with + * permanent storage. + */ + void store(PersonalAccessToken token) + throws UnsatisfiedScmPreconditionException, ScmConfigurationPersistenceException; }