diff --git a/infrastructures/infrastructure-factory/src/main/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesGitCredentialManager.java b/infrastructures/infrastructure-factory/src/main/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesGitCredentialManager.java index b5c3a2fd834..3f31c451c64 100644 --- a/infrastructures/infrastructure-factory/src/main/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesGitCredentialManager.java +++ b/infrastructures/infrastructure-factory/src/main/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesGitCredentialManager.java @@ -126,11 +126,7 @@ public void createOrReplace(PersonalAccessToken personalAccessToken) '/')) && personalAccessToken .getCheUserId() - .equals(s.getMetadata().getAnnotations().get(ANNOTATION_CHE_USERID)) - && personalAccessToken - .getScmUserName() - .equals( - s.getMetadata().getAnnotations().get(ANNOTATION_SCM_USERNAME))) + .equals(s.getMetadata().getAnnotations().get(ANNOTATION_CHE_USERID))) .findFirst(); Secret secret = @@ -138,7 +134,6 @@ public void createOrReplace(PersonalAccessToken personalAccessToken) () -> { Map annotations = new HashMap<>(DEFAULT_SECRET_ANNOTATIONS); annotations.put(ANNOTATION_SCM_URL, personalAccessToken.getScmProviderUrl()); - annotations.put(ANNOTATION_SCM_USERNAME, personalAccessToken.getScmUserName()); annotations.put(ANNOTATION_CHE_USERID, personalAccessToken.getCheUserId()); ObjectMeta meta = new ObjectMetaBuilder() 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 497123d3c50..fb71b293ae7 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 @@ -11,6 +11,8 @@ */ package org.eclipse.che.api.factory.server.scm.kubernetes; +import static com.google.common.base.Strings.isNullOrEmpty; + import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import io.fabric8.kubernetes.api.model.LabelSelector; @@ -30,6 +32,7 @@ import org.eclipse.che.api.factory.server.scm.GitCredentialManager; 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.PersonalAccessTokenParams; import org.eclipse.che.api.factory.server.scm.ScmPersonalAccessTokenFetcher; import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException; import org.eclipse.che.api.factory.server.scm.exception.ScmConfigurationPersistenceException; @@ -59,7 +62,6 @@ public class KubernetesPersonalAccessTokenManager implements PersonalAccessToken public static final String NAME_PATTERN = "personal-access-token-"; public static final String ANNOTATION_CHE_USERID = "che.eclipse.org/che-userid"; - public static final String ANNOTATION_SCM_USERNAME = "che.eclipse.org/scm-username"; public static final String ANNOTATION_SCM_ORGANIZATION = "che.eclipse.org/scm-organization"; public static final String ANNOTATION_SCM_PERSONAL_ACCESS_TOKEN_ID = "che.eclipse.org/scm-personal-access-token-id"; @@ -96,7 +98,6 @@ void save(PersonalAccessToken personalAccessToken) .withAnnotations( new ImmutableMap.Builder() .put(ANNOTATION_CHE_USERID, personalAccessToken.getCheUserId()) - .put(ANNOTATION_SCM_USERNAME, personalAccessToken.getScmUserName()) .put(ANNOTATION_SCM_URL, personalAccessToken.getScmProviderUrl()) .put( ANNOTATION_SCM_PERSONAL_ACCESS_TOKEN_ID, @@ -191,28 +192,34 @@ private Optional doGetPersonalAccessToken( || trimmedUrl.equals(StringUtils.trimEnd(scmServerUrl, '/')))) { String token = new String(Base64.getDecoder().decode(secret.getData().get("token"))).trim(); - PersonalAccessToken personalAccessToken = - new PersonalAccessToken( - trimmedUrl, - annotations.get(ANNOTATION_CHE_USERID), - annotations.get(ANNOTATION_SCM_ORGANIZATION), - annotations.get(ANNOTATION_SCM_USERNAME), - annotations.get(ANNOTATION_SCM_PERSONAL_ACCESS_TOKEN_NAME), - annotations.get(ANNOTATION_SCM_PERSONAL_ACCESS_TOKEN_ID), - token); - if (scmPersonalAccessTokenFetcher.isValid(personalAccessToken)) { + String providerName = annotations.get(ANNOTATION_SCM_PERSONAL_ACCESS_TOKEN_NAME); + String tokenId = annotations.get(ANNOTATION_SCM_PERSONAL_ACCESS_TOKEN_ID); + String organization = annotations.get(ANNOTATION_SCM_ORGANIZATION); + String scmUsername = + scmPersonalAccessTokenFetcher.isValid( + new PersonalAccessTokenParams( + trimmedUrl, providerName, tokenId, token, organization)); + if (!isNullOrEmpty(scmUsername)) { + PersonalAccessToken personalAccessToken = + new PersonalAccessToken( + trimmedUrl, + annotations.get(ANNOTATION_CHE_USERID), + organization, + scmUsername, + providerName, + tokenId, + token); return Optional.of(personalAccessToken); - } else { - // Removing token that is no longer valid. If several tokens exist the next one could - // be valid. If no valid token can be found, the caller should react in the same way - // as it reacts if no token exists. Usually, that means that process of new token - // retrieval would be initiated. - cheServerKubernetesClientFactory - .create() - .secrets() - .inNamespace(namespaceMeta.getName()) - .delete(secret); } + // Removing token that is no longer valid. If several tokens exist the next one could + // be valid. If no valid token can be found, the caller should react in the same way + // as it reacts if no token exists. Usually, that means that process of new token + // retrieval would be initiated. + cheServerKubernetesClientFactory + .create() + .secrets() + .inNamespace(namespaceMeta.getName()) + .delete(secret); } } } diff --git a/infrastructures/infrastructure-factory/src/test/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesGitCredentialManagerTest.java b/infrastructures/infrastructure-factory/src/test/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesGitCredentialManagerTest.java index 5ac8e79bdef..07e93a20400 100644 --- a/infrastructures/infrastructure-factory/src/test/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesGitCredentialManagerTest.java +++ b/infrastructures/infrastructure-factory/src/test/java/org/eclipse/che/api/factory/server/scm/kubernetes/KubernetesGitCredentialManagerTest.java @@ -15,7 +15,6 @@ import static java.util.Collections.singletonList; import static org.eclipse.che.api.factory.server.scm.kubernetes.KubernetesGitCredentialManager.ANNOTATION_CHE_USERID; import static org.eclipse.che.api.factory.server.scm.kubernetes.KubernetesGitCredentialManager.ANNOTATION_SCM_URL; -import static org.eclipse.che.api.factory.server.scm.kubernetes.KubernetesGitCredentialManager.ANNOTATION_SCM_USERNAME; import static org.eclipse.che.api.factory.server.scm.kubernetes.KubernetesGitCredentialManager.DEFAULT_SECRET_ANNOTATIONS; import static org.eclipse.che.api.factory.server.scm.kubernetes.KubernetesGitCredentialManager.NAME_PATTERN; import static org.mockito.ArgumentMatchers.anyMap; @@ -125,7 +124,6 @@ public void shouldUseHardcodedUsernameIfScmOrganizationIsDefined() throws Except Map annotations = new HashMap<>(DEFAULT_SECRET_ANNOTATIONS); annotations.put(ANNOTATION_SCM_URL, token.getScmProviderUrl() + "/"); - annotations.put(ANNOTATION_SCM_USERNAME, token.getScmUserName()); annotations.put(ANNOTATION_CHE_USERID, token.getCheUserId()); ObjectMeta objectMeta = new ObjectMetaBuilder() @@ -210,7 +208,6 @@ public void testUpdateTokenInExistingCredential() throws Exception { Map annotations = new HashMap<>(DEFAULT_SECRET_ANNOTATIONS); annotations.put(ANNOTATION_SCM_URL, token.getScmProviderUrl() + "/"); - annotations.put(ANNOTATION_SCM_USERNAME, token.getScmUserName()); annotations.put(ANNOTATION_CHE_USERID, token.getCheUserId()); ObjectMeta objectMeta = new ObjectMetaBuilder() 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 f8207607db3..547cfef5442 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 @@ -43,6 +43,7 @@ import java.util.Optional; import org.eclipse.che.api.factory.server.scm.GitCredentialManager; import org.eclipse.che.api.factory.server.scm.PersonalAccessToken; +import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenParams; import org.eclipse.che.api.factory.server.scm.ScmPersonalAccessTokenFetcher; import org.eclipse.che.commons.subject.SubjectImpl; import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory; @@ -95,7 +96,8 @@ public void shouldTrimBlankCharsInToken() throws Exception { KubernetesSecrets secrets = Mockito.mock(KubernetesSecrets.class); when(namespaceFactory.access(eq(null), eq(meta.getName()))).thenReturn(kubernetesnamespace); when(kubernetesnamespace.secrets()).thenReturn(secrets); - when(scmPersonalAccessTokenFetcher.isValid(any(PersonalAccessToken.class))).thenReturn(true); + when(scmPersonalAccessTokenFetcher.isValid(any(PersonalAccessTokenParams.class))) + .thenReturn("user"); Map data = Map.of("token", Base64.getEncoder().encodeToString(" token_value \n".getBytes(UTF_8))); @@ -161,7 +163,8 @@ public void testGetTokenFromNamespace() throws Exception { KubernetesSecrets secrets = Mockito.mock(KubernetesSecrets.class); when(namespaceFactory.access(eq(null), eq(meta.getName()))).thenReturn(kubernetesnamespace); when(kubernetesnamespace.secrets()).thenReturn(secrets); - when(scmPersonalAccessTokenFetcher.isValid(any(PersonalAccessToken.class))).thenReturn(true); + when(scmPersonalAccessTokenFetcher.isValid(any(PersonalAccessTokenParams.class))) + .thenReturn("user"); Map data1 = Map.of("token", Base64.getEncoder().encodeToString("token1".getBytes(UTF_8))); @@ -214,7 +217,8 @@ public void testGetTokenFromNamespaceWithTrailingSlashMismatch() throws Exceptio KubernetesSecrets secrets = Mockito.mock(KubernetesSecrets.class); when(namespaceFactory.access(eq(null), eq(meta.getName()))).thenReturn(kubernetesnamespace); when(kubernetesnamespace.secrets()).thenReturn(secrets); - when(scmPersonalAccessTokenFetcher.isValid(any(PersonalAccessToken.class))).thenReturn(true); + when(scmPersonalAccessTokenFetcher.isValid(any(PersonalAccessTokenParams.class))) + .thenReturn("user"); Map data1 = Map.of("token", Base64.getEncoder().encodeToString("token1".getBytes(UTF_8))); @@ -261,7 +265,8 @@ public void shouldDeleteInvalidTokensOnGet() throws Exception { KubernetesSecrets secrets = Mockito.mock(KubernetesSecrets.class); when(namespaceFactory.access(eq(null), eq(meta.getName()))).thenReturn(kubernetesnamespace); when(kubernetesnamespace.secrets()).thenReturn(secrets); - when(scmPersonalAccessTokenFetcher.isValid(any(PersonalAccessToken.class))).thenReturn(false); + when(scmPersonalAccessTokenFetcher.isValid(any(PersonalAccessTokenParams.class))) + .thenReturn(null); when(cheServerKubernetesClientFactory.create()).thenReturn(kubeClient); when(kubeClient.secrets()).thenReturn(secretsMixedOperation); when(secretsMixedOperation.inNamespace(eq(meta.getName()))).thenReturn(nonNamespaceOperation); @@ -292,12 +297,12 @@ public void shouldReturnFirstValidToken() throws Exception { KubernetesSecrets secrets = Mockito.mock(KubernetesSecrets.class); when(namespaceFactory.access(eq(null), eq(meta.getName()))).thenReturn(kubernetesnamespace); when(kubernetesnamespace.secrets()).thenReturn(secrets); - when(scmPersonalAccessTokenFetcher.isValid(any(PersonalAccessToken.class))) + when(scmPersonalAccessTokenFetcher.isValid(any(PersonalAccessTokenParams.class))) .thenAnswer( - (Answer) + (Answer) invocation -> { - PersonalAccessToken token = invocation.getArgument(0); - return "id2".equals(token.getScmTokenId()); + PersonalAccessTokenParams params = invocation.getArgument(0); + return "id2".equals(params.getScmTokenId()) ? "user" : null; }); when(cheServerKubernetesClientFactory.create()).thenReturn(kubeClient); when(kubeClient.secrets()).thenReturn(secretsMixedOperation); diff --git a/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsPersonalAccessTokenFetcher.java b/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsPersonalAccessTokenFetcher.java index b028ff77e07..14aa9f62dc9 100644 --- a/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsPersonalAccessTokenFetcher.java +++ b/wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsPersonalAccessTokenFetcher.java @@ -27,12 +27,14 @@ 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; @@ -80,30 +82,31 @@ public PersonalAccessToken fetchPersonalAccessToken(Subject cheSubject, String s try { oAuthToken = oAuthAPI.getToken(AzureDevOps.PROVIDER_NAME); - // Find the user associated to the OAuth token by querying the Azure DevOps API. - AzureDevOpsUser user = azureDevOpsApiClient.getUserWithOAuthToken(oAuthToken.getToken()); - PersonalAccessToken token = - new PersonalAccessToken( - scmServerUrl, - cheSubject.getUserId(), - user.getEmailAddress(), - NameGenerator.generate(OAUTH_2_PREFIX, 5), - NameGenerator.generate("id-", 5), - oAuthToken.getToken()); - Optional valid = isValid(token); + String tokenName = NameGenerator.generate(OAUTH_2_PREFIX, 5); + String tokenId = NameGenerator.generate("id-", 5); + Optional> valid = + isValid( + new PersonalAccessTokenParams( + scmServerUrl, tokenName, tokenId, oAuthToken.getToken(), null)); if (valid.isEmpty()) { throw new ScmCommunicationException( "Unable to verify if current token is a valid Azure DevOps token. Token's scm-url needs to be '" + azureDevOpsScmApiEndpoint + "' and was '" - + token.getScmProviderUrl() + + scmServerUrl + "'"); - } else if (!valid.get()) { + } 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: " + Arrays.toString(scopes)); } - return token; + return new PersonalAccessToken( + scmServerUrl, + cheSubject.getUserId(), + valid.get().second, + tokenName, + tokenId, + oAuthToken.getToken()); } catch (UnauthorizedException e) { throw new ScmUnauthorizedException( cheSubject.getUserName() @@ -115,12 +118,7 @@ public PersonalAccessToken fetchPersonalAccessToken(Subject cheSubject, String s getLocalAuthenticateUrl()); } catch (NotFoundException nfe) { throw new UnknownScmProviderException(nfe.getMessage(), scmServerUrl); - } catch (ServerException - | ForbiddenException - | BadRequestException - | ScmItemNotFoundException - | ScmBadRequestException - | ConflictException e) { + } catch (ServerException | ForbiddenException | BadRequestException | ConflictException e) { LOG.error(e.getMessage()); throw new ScmCommunicationException(e.getMessage(), e); } @@ -149,6 +147,26 @@ public Optional isValid(PersonalAccessToken personalAccessToken) { } } + @Override + public Optional> isValid(PersonalAccessTokenParams params) { + if (!isValidScmServerUrl(params.getScmProviderUrl())) { + LOG.debug("not a valid url {} for current fetcher ", params.getScmProviderUrl()); + return Optional.empty(); + } + + try { + AzureDevOpsUser user; + if (params.getScmTokenName() != null && params.getScmTokenName().startsWith(OAUTH_2_PREFIX)) { + user = azureDevOpsApiClient.getUserWithOAuthToken(params.getToken()); + } else { + user = azureDevOpsApiClient.getUserWithPAT(params.getToken(), params.getOrganization()); + } + return Optional.of(Pair.of(Boolean.TRUE, user.getEmailAddress())); + } catch (ScmItemNotFoundException | ScmCommunicationException | ScmBadRequestException e) { + return Optional.empty(); + } + } + private String getLocalAuthenticateUrl() { return cheApiEndpoint + getAuthenticateUrlPath(scopes); } diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerPersonalAccessTokenFetcher.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerPersonalAccessTokenFetcher.java index 50ae813f7bc..81b83a2cec1 100644 --- a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerPersonalAccessTokenFetcher.java +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerPersonalAccessTokenFetcher.java @@ -27,11 +27,13 @@ import org.eclipse.che.api.factory.server.bitbucket.server.BitbucketUser; import org.eclipse.che.api.factory.server.scm.PersonalAccessToken; import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenFetcher; +import org.eclipse.che.api.factory.server.scm.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.commons.env.EnvironmentContext; +import org.eclipse.che.commons.lang.Pair; import org.eclipse.che.commons.subject.Subject; import org.eclipse.che.security.oauth.OAuthAPI; import org.eclipse.che.security.oauth1.NoopOAuthAuthenticator; @@ -76,22 +78,20 @@ public PersonalAccessToken fetchPersonalAccessToken(Subject cheUser, String scmS final String tokenName = format(TOKEN_NAME_TEMPLATE, cheUser.getUserId(), apiEndpoint.getHost()); try { - BitbucketUser user = - bitbucketServerApiClient.getUser(EnvironmentContext.getCurrent().getSubject()); + BitbucketUser user = bitbucketServerApiClient.getUser(null); LOG.debug("Current bitbucket user {} ", user); // cleanup existed List existingTokens = - bitbucketServerApiClient.getPersonalAccessTokens(user.getSlug()).stream() + bitbucketServerApiClient.getPersonalAccessTokens().stream() .filter(p -> p.getName().equals(tokenName)) .collect(Collectors.toList()); for (BitbucketPersonalAccessToken existedToken : existingTokens) { LOG.debug("Deleting existed che token {} {}", existedToken.getId(), existedToken.getName()); - bitbucketServerApiClient.deletePersonalAccessTokens(user.getSlug(), existedToken.getId()); + bitbucketServerApiClient.deletePersonalAccessTokens(existedToken.getId()); } BitbucketPersonalAccessToken token = - bitbucketServerApiClient.createPersonalAccessTokens( - user.getSlug(), tokenName, DEFAULT_TOKEN_SCOPE); + bitbucketServerApiClient.createPersonalAccessTokens(tokenName, DEFAULT_TOKEN_SCOPE); LOG.debug("Token created = {} for {}", token.getId(), token.getUser()); return new PersonalAccessToken( scmServerUrl, @@ -118,7 +118,7 @@ public Optional isValid(PersonalAccessToken personalAccessToken) oAuthAPI, apiEndpoint.toString()); try { - apiClient.getUser(personalAccessToken.getScmUserName(), personalAccessToken.getToken()); + apiClient.getUser(personalAccessToken.getToken()); return Optional.of(Boolean.TRUE); } catch (ScmItemNotFoundException | ScmUnauthorizedException @@ -131,11 +131,44 @@ public Optional isValid(PersonalAccessToken personalAccessToken) try { BitbucketPersonalAccessToken bitbucketPersonalAccessToken = bitbucketServerApiClient.getPersonalAccessToken( - personalAccessToken.getScmUserName(), Long.valueOf(personalAccessToken.getScmTokenId())); return Optional.of(DEFAULT_TOKEN_SCOPE.equals(bitbucketPersonalAccessToken.getPermissions())); } catch (ScmItemNotFoundException e) { return Optional.of(Boolean.FALSE); } } + + @Override + public Optional> isValid(PersonalAccessTokenParams params) { + if (!bitbucketServerApiClient.isConnected(params.getScmProviderUrl())) { + // If BitBucket oAuth is not configured check the manually added user namespace token. + HttpBitbucketServerApiClient apiClient = + new HttpBitbucketServerApiClient( + params.getScmProviderUrl(), + new NoopOAuthAuthenticator(), + oAuthAPI, + apiEndpoint.toString()); + try { + BitbucketUser user = apiClient.getUser(params.getToken()); + return Optional.of(Pair.of(Boolean.TRUE, user.getName())); + } catch (ScmItemNotFoundException + | ScmUnauthorizedException + | ScmCommunicationException exception) { + LOG.debug("not a valid url {} for current fetcher ", params.getScmProviderUrl()); + return Optional.empty(); + } + } + try { + BitbucketPersonalAccessToken bitbucketPersonalAccessToken = + bitbucketServerApiClient.getPersonalAccessToken(Long.valueOf(params.getScmTokenId())); + return Optional.of( + Pair.of( + DEFAULT_TOKEN_SCOPE.equals(bitbucketPersonalAccessToken.getPermissions()) + ? Boolean.TRUE + : Boolean.FALSE, + bitbucketPersonalAccessToken.getUser().getName())); + } catch (ScmItemNotFoundException | ScmUnauthorizedException | ScmCommunicationException e) { + return Optional.empty(); + } + } } diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerURLParser.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerURLParser.java index 2115245272d..7fcea295dbd 100644 --- a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerURLParser.java +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerURLParser.java @@ -114,9 +114,9 @@ private boolean isApiRequestRelevant(String repositoryUrl) { new BitbucketServerOAuthAuthenticator("", "", "", ""), oAuthAPI, ""); - // If the token request catches the unauthorised error, it means that the provided url + // If the user request catches the unauthorised error, it means that the provided url // belongs to Bitbucket. - bitbucketServerApiClient.getPersonalAccessToken("", 0L); + bitbucketServerApiClient.getUser(null); } catch (ScmItemNotFoundException | ScmCommunicationException e) { return false; } catch (ScmUnauthorizedException e) { diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerUserDataFetcher.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerUserDataFetcher.java index 1af89417945..282a08b0932 100644 --- a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerUserDataFetcher.java +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerUserDataFetcher.java @@ -80,7 +80,7 @@ public GitUserData fetchGitUserData() for (String bitbucketServerEndpoint : this.registeredBitbucketEndpoints) { if (bitbucketServerApiClient.isConnected(bitbucketServerEndpoint)) { try { - BitbucketUser user = bitbucketServerApiClient.getUser(cheSubject); + BitbucketUser user = bitbucketServerApiClient.getUser(null); return new GitUserData(user.getDisplayName(), user.getEmailAddress()); } catch (ScmItemNotFoundException e) { throw new ScmCommunicationException(e.getMessage(), e); @@ -100,8 +100,7 @@ public GitUserData fetchGitUserData() oAuthAPI, this.apiEndpoint); - BitbucketUser user = - httpBitbucketServerApiClient.getUser(token.getScmUserName(), token.getToken()); + BitbucketUser user = httpBitbucketServerApiClient.getUser(token.getToken()); return new GitUserData(user.getDisplayName(), user.getEmailAddress()); } diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/HttpBitbucketServerApiClient.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/HttpBitbucketServerApiClient.java index 9ee5da4f731..e09f3c4b2c8 100644 --- a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/HttpBitbucketServerApiClient.java +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/HttpBitbucketServerApiClient.java @@ -37,11 +37,9 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.concurrent.Executors; import java.util.function.Function; -import java.util.stream.Collectors; import org.eclipse.che.api.auth.shared.dto.OAuthToken; import org.eclipse.che.api.core.BadRequestException; import org.eclipse.che.api.core.ConflictException; @@ -112,44 +110,49 @@ public boolean isConnected(String bitbucketServerUrl) { } @Override - public BitbucketUser getUser(Subject cheUser) - throws ScmUnauthorizedException, ScmCommunicationException, ScmItemNotFoundException { + public BitbucketUser getUser(@Nullable String token) + throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException { + return getUser(getUserSlug(token), token); + } + + private String getUserSlug(@Nullable String token) + throws ScmCommunicationException, ScmUnauthorizedException, ScmItemNotFoundException { + URI uri; try { - // Since Bitbucket server API doesn't provide a way to get an account profile currently - // authenticated user we will try to find it and by iterating over the list available to the - // current user Bitbucket users and attempting to get their personal access tokens. To speed - // up this process first of all we will search among users that contain(somewhere in Bitbucket - // user - // entity) Che's user username. At the second step, we will search against all visible(to the - // current Che's user) bitbucket users that are not included in the first list. - Set usersByName = - getUsers(cheUser.getUserName()).stream() - .map(BitbucketUser::getSlug) - .collect(Collectors.toSet()); + uri = serverUri.resolve("./plugins/servlet/applinks/whoami"); + } catch (IllegalArgumentException e) { + // if the slug contains invalid characters (space for example) then the URI will be invalid + throw new ScmCommunicationException(e.getMessage(), e); + } - Optional currentUser = findCurrentUser(usersByName); - if (currentUser.isPresent()) { - return currentUser.get(); - } - Set usersAllExceptByName = - getUsers().stream() - .map(BitbucketUser::getSlug) - .filter(s -> !usersByName.contains(s)) - .collect(Collectors.toSet()); - currentUser = findCurrentUser(usersAllExceptByName); - if (currentUser.isPresent()) { - return currentUser.get(); - } - } catch (ScmBadRequestException | ScmItemNotFoundException scmException) { - throw new ScmCommunicationException(scmException.getMessage(), scmException); + HttpRequest request = + HttpRequest.newBuilder(uri) + .headers( + "Authorization", + token != null + ? "Bearer " + token + : computeAuthorizationHeader("GET", uri.toString())) + .timeout(DEFAULT_HTTP_TIMEOUT) + .build(); + + try { + LOG.trace("executeRequest={}", request); + return executeRequest( + httpClient, + request, + inputStream -> { + try { + return CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } catch (ScmBadRequestException e) { + throw new ScmCommunicationException(e.getMessage(), e); } - throw new ScmItemNotFoundException( - "Current user not found. That is possible only if user are not authorized against " - + serverUri); } - @Override - public BitbucketUser getUser(String slug, @Nullable String token) + private BitbucketUser getUser(String slug, @Nullable String token) throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException { URI uri; try { @@ -209,9 +212,10 @@ public List getUsers(String filter) } @Override - public void deletePersonalAccessTokens(String userSlug, Long tokenId) + public void deletePersonalAccessTokens(Long tokenId) throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException { - URI uri = serverUri.resolve("./rest/access-tokens/1.0/users/" + userSlug + "/" + tokenId); + URI uri = + serverUri.resolve("./rest/access-tokens/1.0/users/" + getUserSlug(null) + "/" + tokenId); HttpRequest request = HttpRequest.newBuilder(uri) .DELETE() @@ -246,11 +250,12 @@ public void deletePersonalAccessTokens(String userSlug, Long tokenId) @Override public BitbucketPersonalAccessToken createPersonalAccessTokens( - String userSlug, String tokenName, Set permissions) - throws ScmBadRequestException, ScmUnauthorizedException, ScmCommunicationException { + String tokenName, Set permissions) + throws ScmBadRequestException, ScmUnauthorizedException, ScmCommunicationException, + ScmItemNotFoundException { BitbucketPersonalAccessToken token = new BitbucketPersonalAccessToken(tokenName, permissions, 90); - URI uri = serverUri.resolve("./rest/access-tokens/1.0/users/" + userSlug); + URI uri = serverUri.resolve("./rest/access-tokens/1.0/users/" + getUserSlug(null)); try { HttpRequest request = @@ -288,20 +293,23 @@ public BitbucketPersonalAccessToken createPersonalAccessTokens( } @Override - public List getPersonalAccessTokens(String userSlug) + public List getPersonalAccessTokens() throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException { try { return doGetItems( - BitbucketPersonalAccessToken.class, "./rest/access-tokens/1.0/users/" + userSlug, null); + BitbucketPersonalAccessToken.class, + "./rest/access-tokens/1.0/users/" + getUserSlug(null), + null); } catch (ScmBadRequestException e) { throw new ScmCommunicationException(e.getMessage(), e); } } @Override - public BitbucketPersonalAccessToken getPersonalAccessToken(String userSlug, Long tokenId) + public BitbucketPersonalAccessToken getPersonalAccessToken(Long tokenId) throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException { - URI uri = serverUri.resolve("./rest/access-tokens/1.0/users/" + userSlug + "/" + tokenId); + URI uri = + serverUri.resolve("./rest/access-tokens/1.0/users/" + getUserSlug(null) + "/" + tokenId); HttpRequest request = HttpRequest.newBuilder(uri) .headers( @@ -331,37 +339,6 @@ public BitbucketPersonalAccessToken getPersonalAccessToken(String userSlug, Long } } - /** - * This method is testing provided collection of user's `slug`s if contains the `slug` of the - * currently authenticated user and return it. The major method to test that condition is to get - * the list of personal access tokens. Current Che user that is associated with Bitbucket user - * should not be able to get someone else list of personal access tokens except his own. - * - * @param userSlugs set of user's `slug`s to test if it contains currently authenticated user. - * @return Bitbucket user from the given set that is associated with the current user. Or - * Optional.empty if the given set doesn't contain that user. - * @throws ScmCommunicationException can happen if communication between che server and bitbucket - * server is failed. - * @throws ScmUnauthorizedException can happen if currently authenticated che user is not - * associated with bitbucket server. - * @throws ScmItemNotFoundException can happen if provided `slug` to test is not associated with - * any user on Bitbucket server - */ - private Optional findCurrentUser(Set userSlugs) - throws ScmCommunicationException, ScmUnauthorizedException, ScmItemNotFoundException { - - for (String userSlug : userSlugs) { - BitbucketUser user = getUser(userSlug, null); - try { - getPersonalAccessTokens(userSlug); - return Optional.of(user); - } catch (ScmItemNotFoundException | ScmUnauthorizedException e) { - // ok - } - } - return Optional.empty(); - } - private List doGetItems(Class tClass, String api, String filter) throws ScmUnauthorizedException, ScmCommunicationException, ScmBadRequestException, ScmItemNotFoundException { diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/BitbucketServerApiClient.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/BitbucketServerApiClient.java index cba69d318d5..fcf4d8c7119 100644 --- a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/BitbucketServerApiClient.java +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/BitbucketServerApiClient.java @@ -18,7 +18,6 @@ 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.subject.Subject; /** Bitbucket Server API client. */ public interface BitbucketServerApiClient { @@ -27,24 +26,15 @@ public interface BitbucketServerApiClient { * @return - true if client is connected to the given bitbucket server. */ boolean isConnected(String bitbucketServerUrl); - /** - * @param cheUser - Che user. - * @return - {@link BitbucketUser} that is linked with given {@link Subject} - * @throws ScmUnauthorizedException - in case if {@link Subject} is not linked to any {@link - * BitbucketUser} - */ - BitbucketUser getUser(Subject cheUser) - throws ScmUnauthorizedException, ScmCommunicationException, ScmItemNotFoundException; /** - * @param slug scm username. * @param token token to override. Pass {@code null} to use token from the authentication flow. * @return - Retrieve the {@link BitbucketUser} matching the supplied userSlug. * @throws ScmItemNotFoundException * @throws ScmUnauthorizedException * @throws ScmCommunicationException */ - BitbucketUser getUser(String slug, @Nullable String token) + BitbucketUser getUser(@Nullable String token) throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException; /** @@ -71,19 +61,17 @@ List getUsers(String filter) * Modify an access token for the user according to the given request. Any fields not specified * will not be altered * - * @param userSlug * @param tokenId - the token id * @throws ScmItemNotFoundException * @throws ScmUnauthorizedException * @throws ScmCommunicationException */ - void deletePersonalAccessTokens(String userSlug, Long tokenId) + void deletePersonalAccessTokens(Long tokenId) throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException; /** * Create an access token for the user according to the given request. * - * @param userSlug * @param tokenName * @param permissions * @return @@ -91,29 +79,27 @@ void deletePersonalAccessTokens(String userSlug, Long tokenId) * @throws ScmUnauthorizedException * @throws ScmCommunicationException */ - BitbucketPersonalAccessToken createPersonalAccessTokens( - String userSlug, String tokenName, Set permissions) - throws ScmBadRequestException, ScmUnauthorizedException, ScmCommunicationException; + BitbucketPersonalAccessToken createPersonalAccessTokens(String tokenName, Set permissions) + throws ScmBadRequestException, ScmUnauthorizedException, ScmCommunicationException, + ScmItemNotFoundException; /** * Get all personal access tokens associated with the given user * - * @param userSlug * @return * @throws ScmItemNotFoundException * @throws ScmUnauthorizedException * @throws ScmBadRequestException * @throws ScmCommunicationException */ - List getPersonalAccessTokens(String userSlug) + List getPersonalAccessTokens() throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException; /** - * @param userSlug - user's slug. * @param tokenId - bitbucket personal access token id. * @return - Bitbucket personal access token. * @throws ScmCommunicationException */ - BitbucketPersonalAccessToken getPersonalAccessToken(String userSlug, Long tokenId) + BitbucketPersonalAccessToken getPersonalAccessToken(Long tokenId) throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException; } diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/NoopBitbucketServerApiClient.java b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/NoopBitbucketServerApiClient.java index d058308a2d8..447ab5bae76 100644 --- a/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/NoopBitbucketServerApiClient.java +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/main/java/org/eclipse/che/api/factory/server/bitbucket/server/NoopBitbucketServerApiClient.java @@ -18,7 +18,6 @@ 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.subject.Subject; /** * Implementation of @{@link BitbucketServerApiClient} that is going to be deployed in container in @@ -31,14 +30,7 @@ public boolean isConnected(String bitbucketServerUrl) { } @Override - public BitbucketUser getUser(Subject cheUser) - throws ScmUnauthorizedException, ScmCommunicationException { - throw new RuntimeException( - "The fallback noop api client cannot be used for real operation. Make sure Bitbucket OAuth1 is properly configured."); - } - - @Override - public BitbucketUser getUser(String slug, @Nullable String token) + public BitbucketUser getUser(@Nullable String token) throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException { throw new RuntimeException( "The fallback noop api client cannot be used for real operation. Make sure Bitbucket OAuth1 is properly configured."); @@ -59,7 +51,7 @@ public List getUsers(String filter) } @Override - public void deletePersonalAccessTokens(String userSlug, Long tokenId) + public void deletePersonalAccessTokens(Long tokenId) throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException { throw new RuntimeException( "The fallback noop api client cannot be used for real operation. Make sure Bitbucket OAuth1 is properly configured."); @@ -67,19 +59,19 @@ public void deletePersonalAccessTokens(String userSlug, Long tokenId) @Override public BitbucketPersonalAccessToken createPersonalAccessTokens( - String userSlug, String tokenName, Set permissions) + String tokenName, Set permissions) throws ScmBadRequestException, ScmUnauthorizedException, ScmCommunicationException { throw new RuntimeException("Invalid usage of BitbucketServerApi"); } @Override - public List getPersonalAccessTokens(String userSlug) + public List getPersonalAccessTokens() throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException { throw new RuntimeException("Invalid usage of BitbucketServerApi"); } @Override - public BitbucketPersonalAccessToken getPersonalAccessToken(String userSlug, Long tokenId) + public BitbucketPersonalAccessToken getPersonalAccessToken(Long tokenId) throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException { throw new RuntimeException("Invalid usage of BitbucketServerApi"); } diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerPersonalAccessTokenFetcherTest.java b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerPersonalAccessTokenFetcherTest.java index ad2843634d4..59b7090afc6 100644 --- a/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerPersonalAccessTokenFetcherTest.java +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerPersonalAccessTokenFetcherTest.java @@ -40,11 +40,13 @@ 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.PersonalAccessToken; +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.commons.env.EnvironmentContext; +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.security.oauth.OAuthAPI; @@ -61,7 +63,7 @@ public class BitbucketServerPersonalAccessTokenFetcherTest { String someBitbucketURL = "https://some.bitbucketserver.com"; Subject subject; @Mock BitbucketServerApiClient bitbucketServerApiClient; - @Mock PersonalAccessToken personalAccessToken; + @Mock PersonalAccessTokenParams personalAccessToken; @Mock OAuthAPI oAuthAPI; BitbucketUser bitbucketUser; BitbucketServerPersonalAccessTokenFetcher fetcher; @@ -132,7 +134,7 @@ public void shouldRethrowBasicExceptionsOnGetUserStep(Class throws ScmUnauthorizedException, ScmCommunicationException, ScmItemNotFoundException { // given when(bitbucketServerApiClient.isConnected(eq(someNotBitbucketURL))).thenReturn(true); - doThrow(exception).when(bitbucketServerApiClient).getUser(eq(subject)); + doThrow(exception).when(bitbucketServerApiClient).getUser(null); // when fetcher.fetchPersonalAccessToken(subject, someNotBitbucketURL); } @@ -143,12 +145,10 @@ public void shouldBeAbleToFetchPersonalAccessToken() ScmBadRequestException { // given when(bitbucketServerApiClient.isConnected(eq(someBitbucketURL))).thenReturn(true); - when(bitbucketServerApiClient.getUser(eq(subject))).thenReturn(bitbucketUser); - when(bitbucketServerApiClient.getPersonalAccessTokens(eq(bitbucketUser.getSlug()))) - .thenReturn(Collections.emptyList()); + when(bitbucketServerApiClient.getUser(null)).thenReturn(bitbucketUser); + when(bitbucketServerApiClient.getPersonalAccessTokens()).thenReturn(Collections.emptyList()); when(bitbucketServerApiClient.createPersonalAccessTokens( - eq(bitbucketUser.getSlug()), eq("che-token--"), eq(ImmutableSet.of("PROJECT_WRITE", "REPO_WRITE")))) .thenReturn(bitbucketPersonalAccessToken); @@ -159,7 +159,6 @@ public void shouldBeAbleToFetchPersonalAccessToken() assertEquals(result.getScmProviderUrl(), someBitbucketURL); assertEquals(result.getCheUserId(), subject.getUserId()); assertEquals(result.getScmOrganization(), bitbucketUser.getName()); - assertEquals(result.getScmUserName(), bitbucketUser.getSlug()); assertEquals(result.getScmTokenId(), valueOf(bitbucketPersonalAccessToken.getId())); assertEquals(result.getToken(), bitbucketPersonalAccessToken.getToken()); } @@ -169,11 +168,10 @@ public void shouldDeleteExistedCheTokenBeforeCreatingNew() throws ScmUnauthorizedException, ScmCommunicationException, ScmItemNotFoundException, ScmBadRequestException { when(bitbucketServerApiClient.isConnected(eq(someBitbucketURL))).thenReturn(true); - when(bitbucketServerApiClient.getUser(eq(subject))).thenReturn(bitbucketUser); - when(bitbucketServerApiClient.getPersonalAccessTokens(eq(bitbucketUser.getSlug()))) + when(bitbucketServerApiClient.getUser(null)).thenReturn(bitbucketUser); + when(bitbucketServerApiClient.getPersonalAccessTokens()) .thenReturn(ImmutableList.of(bitbucketPersonalAccessToken, bitbucketPersonalAccessToken2)); when(bitbucketServerApiClient.createPersonalAccessTokens( - eq(bitbucketUser.getSlug()), eq("che-token--"), eq(ImmutableSet.of("PROJECT_WRITE", "REPO_WRITE")))) .thenReturn(bitbucketPersonalAccessToken3); @@ -182,11 +180,9 @@ public void shouldDeleteExistedCheTokenBeforeCreatingNew() // then assertNotNull(result); verify(bitbucketServerApiClient) - .deletePersonalAccessTokens( - eq(bitbucketUser.getSlug()), eq(bitbucketPersonalAccessToken.getId())); + .deletePersonalAccessTokens(eq(bitbucketPersonalAccessToken.getId())); verify(bitbucketServerApiClient) - .deletePersonalAccessTokens( - eq(bitbucketUser.getSlug()), eq(bitbucketPersonalAccessToken2.getId())); + .deletePersonalAccessTokens(eq(bitbucketPersonalAccessToken2.getId())); } @Test(expectedExceptions = {ScmCommunicationException.class}) @@ -195,13 +191,11 @@ public void shouldRethrowUnExceptionsOnCreatePersonalAccessTokens() ScmBadRequestException { // given when(bitbucketServerApiClient.isConnected(eq(someBitbucketURL))).thenReturn(true); - when(bitbucketServerApiClient.getUser(eq(subject))).thenReturn(bitbucketUser); - when(bitbucketServerApiClient.getPersonalAccessTokens(eq(bitbucketUser.getSlug()))) - .thenReturn(Collections.emptyList()); + when(bitbucketServerApiClient.getUser(null)).thenReturn(bitbucketUser); + when(bitbucketServerApiClient.getPersonalAccessTokens()).thenReturn(Collections.emptyList()); doThrow(ScmBadRequestException.class) .when(bitbucketServerApiClient) .createPersonalAccessTokens( - eq(bitbucketUser.getSlug()), eq("che-token--"), eq(ImmutableSet.of("PROJECT_WRITE", "REPO_WRITE"))); // when @@ -219,7 +213,7 @@ public void shouldSkipToValidateTokensWithUnknownUrls() when(personalAccessToken.getScmProviderUrl()).thenReturn(someNotBitbucketURL); when(bitbucketServerApiClient.isConnected(eq(someNotBitbucketURL))).thenReturn(false); // when - Optional result = fetcher.isValid(personalAccessToken); + Optional> result = fetcher.isValid(personalAccessToken); // then assertTrue(result.isEmpty()); } @@ -231,18 +225,14 @@ public void shouldBeAbleToValidateToken() when(personalAccessToken.getScmProviderUrl()).thenReturn(someBitbucketURL); when(personalAccessToken.getScmTokenId()) .thenReturn(Long.toString(bitbucketPersonalAccessToken.getId())); - when(personalAccessToken.getScmUserName()) - .thenReturn((bitbucketPersonalAccessToken.getUser().getSlug())); when(bitbucketServerApiClient.isConnected(eq(someBitbucketURL))).thenReturn(true); - when(bitbucketServerApiClient.getPersonalAccessToken( - eq(bitbucketPersonalAccessToken.getUser().getSlug()), - eq(bitbucketPersonalAccessToken.getId()))) + when(bitbucketServerApiClient.getPersonalAccessToken(eq(bitbucketPersonalAccessToken.getId()))) .thenReturn(bitbucketPersonalAccessToken); // when - Optional result = fetcher.isValid(personalAccessToken); + Optional> result = fetcher.isValid(personalAccessToken); // then assertFalse(result.isEmpty()); - assertTrue(result.get()); + assertTrue(result.get().first); } @DataProvider diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerURLParserTest.java b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerURLParserTest.java index 31c67aa0b63..4af35e693fe 100644 --- a/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerURLParserTest.java +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerURLParserTest.java @@ -99,7 +99,8 @@ public void shouldValidateUrlByApiRequest() { null, devfileFilenamesProvider, oAuthAPI, mock(PersonalAccessTokenManager.class)); String url = wireMockServer.url("/users/user/repos/repo"); stubFor( - get(urlEqualTo("/rest/access-tokens/1.0/users/0")).willReturn(aResponse().withStatus(401))); + get(urlEqualTo("/plugins/servlet/applinks/whoami")) + .willReturn(aResponse().withStatus(401))); // when boolean result = bitbucketURLParser.isValid(url); @@ -113,7 +114,8 @@ public void shouldNotValidateUrlByApiRequest() { // given String url = wireMockServer.url("/users/user/repos/repo"); stubFor( - get(urlEqualTo("/rest/access-tokens/1.0/users/0")).willReturn(aResponse().withStatus(500))); + get(urlEqualTo("/plugins/servlet/applinks/whoami")) + .willReturn(aResponse().withStatus(500))); // when boolean result = bitbucketURLParser.isValid(url); diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerUserDataFetcherTest.java b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerUserDataFetcherTest.java index c4654c3be85..25d6179972e 100644 --- a/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerUserDataFetcherTest.java +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketServerUserDataFetcherTest.java @@ -64,7 +64,7 @@ public void shouldBeAbleToFetchPersonalAccessToken() ScmBadRequestException, ScmConfigurationPersistenceException { // given when(bitbucketServerApiClient.isConnected(eq(someBitbucketURL))).thenReturn(true); - when(bitbucketServerApiClient.getUser(eq(subject))).thenReturn(bitbucketUser); + when(bitbucketServerApiClient.getUser(null)).thenReturn(bitbucketUser); // when GitUserData gitUserData = fetcher.fetchGitUserData(); // then diff --git a/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/HttpBitbucketServerApiClientTest.java b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/HttpBitbucketServerApiClientTest.java index cc19888e622..6d663df17da 100644 --- a/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/HttpBitbucketServerApiClientTest.java +++ b/wsmaster/che-core-api-factory-bitbucket-server/src/test/java/org/eclipse/che/api/factory/server/bitbucket/HttpBitbucketServerApiClientTest.java @@ -24,6 +24,7 @@ import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; @@ -99,6 +100,9 @@ public String computeAuthorizationHeader( }, oAuthAPI, apiEndpoint); + stubFor( + get(urlEqualTo("/plugins/servlet/applinks/whoami")) + .willReturn(aResponse().withBody("ksmster"))); } @AfterMethod @@ -117,7 +121,7 @@ public void testGetUser() .withHeader("Content-Type", "application/json; charset=utf-8") .withBodyFile("bitbucket/rest/api/1.0/users/ksmster/response.json"))); - BitbucketUser user = bitbucketServer.getUser("ksmster", null); + BitbucketUser user = bitbucketServer.getUser(null); assertNotNull(user); } @@ -215,7 +219,7 @@ public void testGetPersonalAccessTokens() .withBodyFile("bitbucket/rest/access-tokens/1.0/users/ksmster/response.json"))); List page = - bitbucketServer.getPersonalAccessTokens("ksmster").stream() + bitbucketServer.getPersonalAccessTokens().stream() .map(BitbucketPersonalAccessToken::getName) .collect(Collectors.toList()); assertEquals(page, ImmutableList.of("che", "t2")); @@ -223,7 +227,8 @@ public void testGetPersonalAccessTokens() @Test public void shouldBeAbleToCreatePAT() - throws ScmCommunicationException, ScmBadRequestException, ScmUnauthorizedException { + throws ScmCommunicationException, ScmBadRequestException, ScmUnauthorizedException, + ScmItemNotFoundException { // given stubFor( @@ -238,7 +243,7 @@ public void shouldBeAbleToCreatePAT() // when BitbucketPersonalAccessToken result = bitbucketServer.createPersonalAccessTokens( - "ksmster", "myToKen", ImmutableSet.of("PROJECT_WRITE", "REPO_WRITE")); + "myToKen", ImmutableSet.of("PROJECT_WRITE", "REPO_WRITE")); // then assertNotNull(result); assertEquals(result.getToken(), "MTU4OTEwNTMyOTA5Ohc88HcY8k7gWOzl2mP5TtdtY5Qs"); @@ -257,7 +262,7 @@ public void shouldBeAbleToGetExistedPAT() ok().withBodyFile("bitbucket/rest/access-tokens/1.0/users/ksmster/newtoken.json"))); // when - BitbucketPersonalAccessToken result = bitbucketServer.getPersonalAccessToken("ksmster", 5L); + BitbucketPersonalAccessToken result = bitbucketServer.getPersonalAccessToken(5L); // then assertNotNull(result); assertEquals(result.getToken(), "MTU4OTEwNTMyOTA5Ohc88HcY8k7gWOzl2mP5TtdtY5Qs"); @@ -275,7 +280,7 @@ public void shouldBeAbleToThrowNotFoundOnGePAT() .willReturn(notFound())); // when - bitbucketServer.getPersonalAccessToken("ksmster", 5L); + bitbucketServer.getPersonalAccessToken(5L); } @Test(expectedExceptions = ScmUnauthorizedException.class) @@ -290,7 +295,7 @@ public void shouldBeAbleToThrowScmUnauthorizedExceptionOnGePAT() .willReturn(unauthorized())); // when - bitbucketServer.getPersonalAccessToken("ksmster", 5L); + bitbucketServer.getPersonalAccessToken(5L); } @Test( @@ -309,7 +314,7 @@ public void shouldThrowScmCommunicationExceptionInNoOauthAuthenticator() wireMockServer.url("/"), new NoopOAuthAuthenticator(), oAuthAPI, apiEndpoint); // when - localServer.getPersonalAccessToken("ksmster", 5L); + localServer.getPersonalAccessToken(5L); } @Test @@ -325,7 +330,7 @@ public void shouldGetOauth2Token() new HttpBitbucketServerApiClient( wireMockServer.url("/"), new NoopOAuthAuthenticator(), oAuthAPI, apiEndpoint); stubFor( - get(urlEqualTo("/rest/api/1.0/users/user")) + get(urlEqualTo("/rest/api/1.0/users/ksmster")) .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer token")) .willReturn( aResponse() @@ -333,9 +338,9 @@ public void shouldGetOauth2Token() .withBodyFile("bitbucket/rest/api/1.0/users/ksmster/response.json"))); // when - bitbucketServer.getUser("user", null); + bitbucketServer.getUser(null); // then - verify(oAuthAPI).getToken(eq("bitbucket")); + verify(oAuthAPI, times(2)).getToken(eq("bitbucket")); } } diff --git a/wsmaster/che-core-api-factory-bitbucket/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketApiClient.java b/wsmaster/che-core-api-factory-bitbucket/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketApiClient.java index b0d025d7c55..1b0d72c2a0f 100644 --- a/wsmaster/che-core-api-factory-bitbucket/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketApiClient.java +++ b/wsmaster/che-core-api-factory-bitbucket/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketApiClient.java @@ -37,6 +37,7 @@ 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.commons.lang.Pair; import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -166,15 +167,13 @@ public BitbucketUserEmail getEmail(String authenticationToken) } /** - * Returns the scopes of the OAuth token. + * Returns a pair of the username and array of scopes of the OAuth token. * * @param authenticationToken The OAuth token to inspect. - * @return Array of scopes from the supplied token, empty array if no scope. - * @throws ScmItemNotFoundException - * @throws ScmCommunicationException - * @throws ScmBadRequestException + * @return A pair of the username and array of scopes from the supplied token, empty array if no + * scopes. */ - public String[] getTokenScopes(String authenticationToken) + public Pair getTokenScopes(String authenticationToken) throws ScmItemNotFoundException, ScmCommunicationException, ScmBadRequestException { final URI uri = apiServerUrl.resolve("user"); HttpRequest request = buildBitbucketApiRequest(uri, authenticationToken); @@ -183,12 +182,22 @@ public String[] getTokenScopes(String authenticationToken) httpClient, request, response -> { - Optional scopes = response.headers().firstValue(BITBUCKET_OAUTH_SCOPES_HEADER); - return Splitter.on(',') - .trimResults() - .omitEmptyStrings() - .splitToList(scopes.orElse("")) - .toArray(String[]::new); + try { + String result = + CharStreams.toString(new InputStreamReader(response.body(), Charsets.UTF_8)); + BitbucketUser user = OBJECT_MAPPER.readValue(result, BitbucketUser.class); + Optional responseScopes = + response.headers().firstValue(BITBUCKET_OAUTH_SCOPES_HEADER); + String[] scopes = + Splitter.on(',') + .trimResults() + .omitEmptyStrings() + .splitToList(responseScopes.orElse("")) + .toArray(String[]::new); + return Pair.of(user.getName(), scopes); + } catch (IOException e) { + throw new UncheckedIOException(e); + } }); } diff --git a/wsmaster/che-core-api-factory-bitbucket/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketPersonalAccessTokenFetcher.java b/wsmaster/che-core-api-factory-bitbucket/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketPersonalAccessTokenFetcher.java index 046bce6a7d6..e9e6835fc7e 100644 --- a/wsmaster/che-core-api-factory-bitbucket/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketPersonalAccessTokenFetcher.java +++ b/wsmaster/che-core-api-factory-bitbucket/src/main/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketPersonalAccessTokenFetcher.java @@ -24,12 +24,14 @@ 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; @@ -83,30 +85,31 @@ public PersonalAccessToken fetchPersonalAccessToken(Subject cheSubject, String s } try { oAuthToken = oAuthAPI.getToken(OAUTH_PROVIDER_NAME); - // Find the user associated to the OAuth token by querying the Bitbucket API. - BitbucketUser user = bitbucketApiClient.getUser(oAuthToken.getToken()); - PersonalAccessToken token = - new PersonalAccessToken( - scmServerUrl, - cheSubject.getUserId(), - user.getName(), - NameGenerator.generate(OAUTH_PROVIDER_NAME, 5), - NameGenerator.generate("id-", 5), - oAuthToken.getToken()); - Optional valid = isValid(token); + String tokenName = NameGenerator.generate(OAUTH_PROVIDER_NAME, 5); + String tokenId = NameGenerator.generate("id-", 5); + Optional> valid = + isValid( + new PersonalAccessTokenParams( + scmServerUrl, tokenName, tokenId, oAuthToken.getToken(), null)); if (valid.isEmpty()) { throw new ScmCommunicationException( "Unable to verify if current token is a valid Bitbucket token. Token's scm-url needs to be '" + BitbucketApiClient.BITBUCKET_SERVER + "' and was '" - + token.getScmProviderUrl() + + scmServerUrl + "'"); - } else if (!valid.get()) { + } 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_SCOPE); } - return token; + return new PersonalAccessToken( + scmServerUrl, + cheSubject.getUserId(), + valid.get().second, + tokenName, + tokenId, + oAuthToken.getToken()); } catch (UnauthorizedException e) { throw new ScmUnauthorizedException( cheSubject.getUserName() @@ -118,12 +121,7 @@ public PersonalAccessToken fetchPersonalAccessToken(Subject cheSubject, String s getLocalAuthenticateUrl()); } catch (NotFoundException nfe) { throw new UnknownScmProviderException(nfe.getMessage(), scmServerUrl); - } catch (ServerException - | ForbiddenException - | BadRequestException - | ScmItemNotFoundException - | ScmBadRequestException - | ConflictException e) { + } catch (ServerException | ForbiddenException | BadRequestException | ConflictException e) { LOG.error(e.getMessage()); throw new ScmCommunicationException(e.getMessage(), e); } @@ -137,13 +135,33 @@ public Optional isValid(PersonalAccessToken personalAccessToken) { } try { - String[] scopes = bitbucketApiClient.getTokenScopes(personalAccessToken.getToken()); + String[] scopes = bitbucketApiClient.getTokenScopes(personalAccessToken.getToken()).second; return Optional.of(Sets.newHashSet(scopes).contains(DEFAULT_TOKEN_SCOPE)); } catch (ScmItemNotFoundException | ScmCommunicationException | ScmBadRequestException e) { return Optional.of(Boolean.FALSE); } } + @Override + public Optional> isValid(PersonalAccessTokenParams params) { + if (!bitbucketApiClient.isConnected(params.getScmProviderUrl())) { + LOG.debug("not a valid url {} for current fetcher ", params.getScmProviderUrl()); + return Optional.empty(); + } + + try { + Pair pair = bitbucketApiClient.getTokenScopes(params.getToken()); + return Optional.of( + Pair.of( + Sets.newHashSet(pair.second).contains(DEFAULT_TOKEN_SCOPE) + ? Boolean.TRUE + : Boolean.FALSE, + pair.first)); + } catch (ScmItemNotFoundException | ScmCommunicationException | ScmBadRequestException e) { + return Optional.empty(); + } + } + private String getLocalAuthenticateUrl() { return apiEndpoint + "/oauth/authenticate?oauth_provider=" diff --git a/wsmaster/che-core-api-factory-bitbucket/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketApiClientTest.java b/wsmaster/che-core-api-factory-bitbucket/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketApiClientTest.java index f2ddfca57b0..41ec29586c0 100644 --- a/wsmaster/che-core-api-factory-bitbucket/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketApiClientTest.java +++ b/wsmaster/che-core-api-factory-bitbucket/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketApiClientTest.java @@ -90,7 +90,7 @@ public void testGetTokenScopes() throws Exception { BitbucketApiClient.BITBUCKET_OAUTH_SCOPES_HEADER, "repository:write") .withBodyFile("bitbucket/rest/user/response.json"))); - String[] scopes = client.getTokenScopes("token1"); + String[] scopes = client.getTokenScopes("token1").second; String[] expectedScopes = {"repository:write"}; assertNotNull(scopes, "Bitbucket API should have returned a non-null scope array"); assertEqualsNoOrder( @@ -107,7 +107,7 @@ public void testGetTokenScopesWithNoScopeHeader() throws Exception { .withHeader("Content-Type", "application/json; charset=utf-8") .withBodyFile("bitbucket/rest/user/response.json"))); - String[] scopes = client.getTokenScopes("token1"); + String[] scopes = client.getTokenScopes("token1").second; assertNotNull(scopes, "Bitbucket API should have returned a non-null scope array"); assertEquals( scopes.length, @@ -128,7 +128,7 @@ public void testGetTokenScopesWithNoScope() throws Exception { .withHeader(BitbucketApiClient.BITBUCKET_OAUTH_SCOPES_HEADER, "") .withBodyFile("bitbucket/rest/user/response.json"))); - String[] scopes = client.getTokenScopes("token1"); + String[] scopes = client.getTokenScopes("token1").second; assertNotNull(scopes, "Bitbucket API should have returned a non-null scope array"); assertEquals( scopes.length, diff --git a/wsmaster/che-core-api-factory-bitbucket/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketPersonalAccessTokenFetcherTest.java b/wsmaster/che-core-api-factory-bitbucket/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketPersonalAccessTokenFetcherTest.java index 8cd3a1b382d..49d06e00fa9 100644 --- a/wsmaster/che-core-api-factory-bitbucket/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketPersonalAccessTokenFetcherTest.java +++ b/wsmaster/che-core-api-factory-bitbucket/src/test/java/org/eclipse/che/api/factory/server/bitbucket/BitbucketPersonalAccessTokenFetcherTest.java @@ -28,11 +28,14 @@ import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.common.Slf4jNotifier; import com.google.common.net.HttpHeaders; +import java.util.Optional; import org.eclipse.che.api.auth.shared.dto.OAuthToken; import org.eclipse.che.api.core.UnauthorizedException; import org.eclipse.che.api.factory.server.scm.PersonalAccessToken; +import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenParams; import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException; import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException; +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.security.oauth.OAuthAPI; @@ -82,16 +85,11 @@ public void shouldNotValidateSCMServerWithTrailingSlash() throws Exception { aResponse() .withHeader("Content-Type", "application/json; charset=utf-8") .withBodyFile("bitbucket/rest/user/response.json"))); - PersonalAccessToken personalAccessToken = - new PersonalAccessToken( - "https://bitbucket.org/", - "cheUserId", - "scmUserName", - "scmTokenName", - "scmTokenId", - bitbucketOauthToken); + PersonalAccessTokenParams personalAccessTokenParams = + new PersonalAccessTokenParams( + "https://bitbucket.org/", "scmTokenName", "scmTokenId", bitbucketOauthToken, null); assertTrue( - bitbucketPersonalAccessTokenFetcher.isValid(personalAccessToken).isEmpty(), + bitbucketPersonalAccessTokenFetcher.isValid(personalAccessTokenParams).isEmpty(), "Should not validate SCM server with trailing /"); } @@ -163,16 +161,13 @@ public void shouldValidatePersonalToken() throws Exception { BitbucketApiClient.BITBUCKET_OAUTH_SCOPES_HEADER, "repository:write") .withBodyFile("bitbucket/rest/user/response.json"))); - PersonalAccessToken token = - new PersonalAccessToken( - "https://bitbucket.org", - "cheUser", - "username", - "token-name", - "tid-23434", - bitbucketOauthToken); + PersonalAccessTokenParams params = + new PersonalAccessTokenParams( + "https://bitbucket.org", "params-name", "tid-23434", bitbucketOauthToken, null); - assertTrue(bitbucketPersonalAccessTokenFetcher.isValid(token).get()); + Optional> valid = bitbucketPersonalAccessTokenFetcher.isValid(params); + assertTrue(valid.isPresent()); + assertTrue(valid.get().first); } @Test @@ -187,31 +182,31 @@ public void shouldValidateOauthToken() throws Exception { BitbucketApiClient.BITBUCKET_OAUTH_SCOPES_HEADER, "repository:write") .withBodyFile("bitbucket/rest/user/response.json"))); - PersonalAccessToken token = - new PersonalAccessToken( + PersonalAccessTokenParams params = + new PersonalAccessTokenParams( "https://bitbucket.org", - "cheUser", - "username", - OAUTH_2_PREFIX + "-token-name", + OAUTH_2_PREFIX + "-params-name", "tid-23434", - bitbucketOauthToken); + bitbucketOauthToken, + null); - assertTrue(bitbucketPersonalAccessTokenFetcher.isValid(token).get()); + Optional> valid = bitbucketPersonalAccessTokenFetcher.isValid(params); + assertTrue(valid.isPresent()); + assertTrue(valid.get().first); } @Test public void shouldNotValidateExpiredOauthToken() throws Exception { stubFor(get(urlEqualTo("/user")).willReturn(aResponse().withStatus(HTTP_FORBIDDEN))); - PersonalAccessToken token = - new PersonalAccessToken( + PersonalAccessTokenParams params = + new PersonalAccessTokenParams( "https://bitbucket.org", - "cheUser", - "username", OAUTH_2_PREFIX + "-token-name", "tid-23434", - bitbucketOauthToken); + bitbucketOauthToken, + null); - assertFalse(bitbucketPersonalAccessTokenFetcher.isValid(token).get()); + assertFalse(bitbucketPersonalAccessTokenFetcher.isValid(params).isPresent()); } } diff --git a/wsmaster/che-core-api-factory-github/src/main/java/org/eclipse/che/api/factory/server/github/GithubApiClient.java b/wsmaster/che-core-api-factory-github/src/main/java/org/eclipse/che/api/factory/server/github/GithubApiClient.java index 67d14525e85..b503da409d1 100644 --- a/wsmaster/che-core-api-factory-github/src/main/java/org/eclipse/che/api/factory/server/github/GithubApiClient.java +++ b/wsmaster/che-core-api-factory-github/src/main/java/org/eclipse/che/api/factory/server/github/GithubApiClient.java @@ -41,6 +41,7 @@ import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException; import org.eclipse.che.api.factory.server.scm.exception.ScmItemNotFoundException; import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.commons.lang.Pair; import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -206,7 +207,7 @@ public GithubCommit getLatestCommit( * @throws ScmCommunicationException * @throws ScmBadRequestException */ - public String[] getTokenScopes(String authenticationToken) + public Pair getTokenScopes(String authenticationToken) throws ScmItemNotFoundException, ScmCommunicationException, ScmBadRequestException { final URI uri = apiServerUrl.resolve("./user"); HttpRequest request = buildGithubApiRequest(uri, authenticationToken); @@ -215,12 +216,22 @@ public String[] getTokenScopes(String authenticationToken) httpClient, request, response -> { - Optional scopes = response.headers().firstValue(GITHUB_OAUTH_SCOPES_HEADER); - return Splitter.on(',') - .trimResults() - .omitEmptyStrings() - .splitToList(scopes.orElse("")) - .toArray(String[]::new); + Optional responseScopes = + response.headers().firstValue(GITHUB_OAUTH_SCOPES_HEADER); + String[] scopes = + Splitter.on(',') + .trimResults() + .omitEmptyStrings() + .splitToList(responseScopes.orElse("")) + .toArray(String[]::new); + try { + String result = + CharStreams.toString(new InputStreamReader(response.body(), Charsets.UTF_8)); + GithubUser user = OBJECT_MAPPER.readValue(result, GithubUser.class); + return Pair.of(user.getName(), scopes); + } catch (IOException e) { + throw new UncheckedIOException(e); + } }); } diff --git a/wsmaster/che-core-api-factory-github/src/main/java/org/eclipse/che/api/factory/server/github/GithubPersonalAccessTokenFetcher.java b/wsmaster/che-core-api-factory-github/src/main/java/org/eclipse/che/api/factory/server/github/GithubPersonalAccessTokenFetcher.java index a5dd1ba535a..763d9795a13 100644 --- a/wsmaster/che-core-api-factory-github/src/main/java/org/eclipse/che/api/factory/server/github/GithubPersonalAccessTokenFetcher.java +++ b/wsmaster/che-core-api-factory-github/src/main/java/org/eclipse/che/api/factory/server/github/GithubPersonalAccessTokenFetcher.java @@ -29,6 +29,7 @@ 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; @@ -36,6 +37,7 @@ 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.subject.Subject; import org.eclipse.che.security.oauth.OAuthAPI; import org.slf4j.Logger; @@ -150,30 +152,31 @@ public PersonalAccessToken fetchPersonalAccessToken(Subject cheSubject, String s } 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()); - PersonalAccessToken token = - new PersonalAccessToken( - scmServerUrl, - cheSubject.getUserId(), - user.getLogin(), - NameGenerator.generate(OAUTH_2_PREFIX, 5), - NameGenerator.generate("id-", 5), - oAuthToken.getToken()); - Optional valid = isValid(token); + String tokenName = NameGenerator.generate(OAUTH_2_PREFIX, 5); + String tokenId = NameGenerator.generate("id-", 5); + Optional> valid = + isValid( + new PersonalAccessTokenParams( + scmServerUrl, tokenName, tokenId, oAuthToken.getToken(), null)); if (valid.isEmpty()) { throw new ScmCommunicationException( "Unable to verify if current token is a valid GitHub token. Token's scm-url needs to be '" + GithubApiClient.GITHUB_SAAS_ENDPOINT + "' and was '" - + token.getScmProviderUrl() + + scmServerUrl + "'"); - } else if (!valid.get()) { + } 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.toString()); } - return token; + return new PersonalAccessToken( + scmServerUrl, + cheSubject.getUserId(), + valid.get().second, + tokenName, + tokenId, + oAuthToken.getToken()); } catch (UnauthorizedException e) { throw new ScmUnauthorizedException( cheSubject.getUserName() @@ -185,18 +188,14 @@ public PersonalAccessToken fetchPersonalAccessToken(Subject cheSubject, String s getLocalAuthenticateUrl()); } catch (NotFoundException nfe) { throw new UnknownScmProviderException(nfe.getMessage(), scmServerUrl); - } catch (ServerException - | ForbiddenException - | BadRequestException - | ScmItemNotFoundException - | ScmBadRequestException - | ConflictException e) { + } catch (ServerException | ForbiddenException | BadRequestException | ConflictException e) { LOG.error(e.getMessage()); throw new ScmCommunicationException(e.getMessage(), e); } } @Override + @Deprecated public Optional isValid(PersonalAccessToken personalAccessToken) { if (!githubApiClient.isConnected(personalAccessToken.getScmProviderUrl())) { LOG.debug("not a valid url {} for current fetcher ", personalAccessToken.getScmProviderUrl()); @@ -204,24 +203,30 @@ public Optional isValid(PersonalAccessToken personalAccessToken) { } try { - if (personalAccessToken.getScmTokenName() != null - && personalAccessToken.getScmTokenName().startsWith(OAUTH_2_PREFIX)) { - String[] scopes = githubApiClient.getTokenScopes(personalAccessToken.getToken()); - return Optional.of(containsScopes(scopes, DEFAULT_TOKEN_SCOPES)); - } else { - // No REST API for PAT-s in Github found yet. Just try to do some action. - GithubUser user = githubApiClient.getUser(personalAccessToken.getToken()); - if (personalAccessToken.getScmUserName().equals(user.getLogin())) { - return Optional.of(Boolean.TRUE); - } else { - return Optional.of(Boolean.FALSE); - } - } + String[] scopes = githubApiClient.getTokenScopes(personalAccessToken.getToken()).second; + return Optional.of(containsScopes(scopes, DEFAULT_TOKEN_SCOPES)); } catch (ScmItemNotFoundException | ScmCommunicationException | ScmBadRequestException e) { return Optional.of(Boolean.FALSE); } } + @Override + public Optional> isValid(PersonalAccessTokenParams params) { + if (!githubApiClient.isConnected(params.getScmProviderUrl())) { + LOG.debug("not a valid url {} for current fetcher ", params.getScmProviderUrl()); + return Optional.empty(); + } + try { + Pair pair = githubApiClient.getTokenScopes(params.getToken()); + return Optional.of( + Pair.of( + containsScopes(pair.second, DEFAULT_TOKEN_SCOPES) ? Boolean.TRUE : Boolean.FALSE, + pair.first)); + } catch (ScmItemNotFoundException | ScmCommunicationException | ScmBadRequestException e) { + return Optional.empty(); + } + } + /** * Checks if the tokenScopes array contains the requiredScopes. * diff --git a/wsmaster/che-core-api-factory-github/src/test/java/org/eclipse/che/api/factory/server/github/GithubApiClientTest.java b/wsmaster/che-core-api-factory-github/src/test/java/org/eclipse/che/api/factory/server/github/GithubApiClientTest.java index ed792acd03e..ebeaca3ff52 100644 --- a/wsmaster/che-core-api-factory-github/src/test/java/org/eclipse/che/api/factory/server/github/GithubApiClientTest.java +++ b/wsmaster/che-core-api-factory-github/src/test/java/org/eclipse/che/api/factory/server/github/GithubApiClientTest.java @@ -165,7 +165,7 @@ public void testGetTokenScopes() throws Exception { .withHeader(GithubApiClient.GITHUB_OAUTH_SCOPES_HEADER, "repo, user:email") .withBodyFile("github/rest/user/response.json"))); - String[] scopes = client.getTokenScopes("token1"); + String[] scopes = client.getTokenScopes("token1").second; String[] expectedScopes = {"repo", "user:email"}; assertNotNull(scopes, "GitHub API should have returned a non-null scope array"); assertEqualsNoOrder( @@ -182,7 +182,7 @@ public void testGetTokenScopesWithNoScopeHeader() throws Exception { .withHeader("Content-Type", "application/json; charset=utf-8") .withBodyFile("github/rest/user/response.json"))); - String[] scopes = client.getTokenScopes("token1"); + String[] scopes = client.getTokenScopes("token1").second; assertNotNull(scopes, "GitHub API should have returned a non-null scope array"); assertEquals( scopes.length, @@ -203,7 +203,7 @@ public void testGetTokenScopesWithNoScope() throws Exception { .withHeader(GithubApiClient.GITHUB_OAUTH_SCOPES_HEADER, "") .withBodyFile("github/rest/user/response.json"))); - String[] scopes = client.getTokenScopes("token1"); + String[] scopes = client.getTokenScopes("token1").second; assertNotNull(scopes, "GitHub API should have returned a non-null scope array"); assertEquals( scopes.length, diff --git a/wsmaster/che-core-api-factory-github/src/test/java/org/eclipse/che/api/factory/server/github/GithubPersonalAccessTokenFetcherTest.java b/wsmaster/che-core-api-factory-github/src/test/java/org/eclipse/che/api/factory/server/github/GithubPersonalAccessTokenFetcherTest.java index 9084bc40d2c..48149f62ecb 100644 --- a/wsmaster/che-core-api-factory-github/src/test/java/org/eclipse/che/api/factory/server/github/GithubPersonalAccessTokenFetcherTest.java +++ b/wsmaster/che-core-api-factory-github/src/test/java/org/eclipse/che/api/factory/server/github/GithubPersonalAccessTokenFetcherTest.java @@ -31,11 +31,14 @@ import com.google.common.collect.ImmutableSet; import com.google.common.net.HttpHeaders; import java.util.Collections; +import java.util.Optional; import org.eclipse.che.api.auth.shared.dto.OAuthToken; import org.eclipse.che.api.core.UnauthorizedException; import org.eclipse.che.api.factory.server.scm.PersonalAccessToken; +import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenParams; import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException; import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException; +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.security.oauth.OAuthAPI; @@ -86,16 +89,11 @@ public void shouldNotValidateSCMServerWithTrailingSlash() throws Exception { .withHeader("Content-Type", "application/json; charset=utf-8") .withHeader(GithubApiClient.GITHUB_OAUTH_SCOPES_HEADER, "repo") .withBodyFile("github/rest/user/response.json"))); - PersonalAccessToken personalAccessToken = - new PersonalAccessToken( - "https://github.com/", - "cheUserId", - "scmUserName", - "scmTokenName", - "scmTokenId", - githubOauthToken); + PersonalAccessTokenParams personalAccessTokenParams = + new PersonalAccessTokenParams( + "https://github.com/", "scmTokenName", "scmTokenId", githubOauthToken, null); assertTrue( - githubPATFetcher.isValid(personalAccessToken).isEmpty(), + githubPATFetcher.isValid(personalAccessTokenParams).isEmpty(), "Should not validate SCM server with trailing /"); } @@ -198,16 +196,13 @@ public void shouldValidatePersonalToken() throws Exception { DEFAULT_TOKEN_SCOPES.toString().replace("[", "").replace("]", "")) .withBodyFile("github/rest/user/response.json"))); - PersonalAccessToken token = - new PersonalAccessToken( - wireMockServer.url("/"), - "cheUser", - "github-user", - "token-name", - "tid-23434", - githubOauthToken); + PersonalAccessTokenParams params = + new PersonalAccessTokenParams( + wireMockServer.url("/"), "token-name", "tid-23434", githubOauthToken, null); - assertTrue(githubPATFetcher.isValid(token).get()); + Optional> valid = githubPATFetcher.isValid(params); + assertTrue(valid.isPresent()); + assertTrue(valid.get().first); } @Test @@ -223,31 +218,31 @@ public void shouldValidateOauthToken() throws Exception { DEFAULT_TOKEN_SCOPES.toString().replace("[", "").replace("]", "")) .withBodyFile("github/rest/user/response.json"))); - PersonalAccessToken token = - new PersonalAccessToken( + PersonalAccessTokenParams params = + new PersonalAccessTokenParams( wireMockServer.url("/"), - "cheUser", - "username", - OAUTH_2_PREFIX + "-token-name", + OAUTH_2_PREFIX + "-params-name", "tid-23434", - githubOauthToken); + githubOauthToken, + null); - assertTrue(githubPATFetcher.isValid(token).get()); + Optional> valid = githubPATFetcher.isValid(params); + assertTrue(valid.isPresent()); + assertTrue(valid.get().first); } @Test public void shouldNotValidateExpiredOauthToken() throws Exception { stubFor(get(urlEqualTo("/api/v3/user")).willReturn(aResponse().withStatus(HTTP_FORBIDDEN))); - PersonalAccessToken token = - new PersonalAccessToken( + PersonalAccessTokenParams params = + new PersonalAccessTokenParams( wireMockServer.url("/"), - "cheUser", - "username", OAUTH_2_PREFIX + "-token-name", "tid-23434", - githubOauthToken); + githubOauthToken, + null); - assertFalse(githubPATFetcher.isValid(token).get()); + assertFalse(githubPATFetcher.isValid(params).isPresent()); } } 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/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabApiClient.java index a687198e79d..6f2a902e9bf 100644 --- 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/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabApiClient.java @@ -87,7 +87,34 @@ public GitlabUser getUser(String authenticationToken) }); } - public GitlabOauthTokenInfo getTokenInfo(String authenticationToken) + public GitlabPersonalAccessTokenInfo getPersonalAccessTokenInfo(String authenticationToken) + throws ScmItemNotFoundException, ScmCommunicationException { + final URI uri = serverUrl.resolve("/api/v4/personal_access_tokens/self"); + HttpRequest request = + HttpRequest.newBuilder(uri) + .headers("Authorization", "Bearer " + authenticationToken) + .timeout(DEFAULT_HTTP_TIMEOUT) + .build(); + LOG.trace("executeRequest={}", request); + try { + return executeRequest( + httpClient, + request, + inputStream -> { + try { + String result = + CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8)); + return OBJECT_MAPPER.readValue(result, GitlabPersonalAccessTokenInfo.class); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } catch (ScmBadRequestException e) { + throw new ScmCommunicationException(e.getMessage(), e); + } + } + + public GitlabOauthTokenInfo getOAuthTokenInfo(String authenticationToken) throws ScmItemNotFoundException, ScmCommunicationException { final URI uri = serverUrl.resolve("/oauth/token/info"); HttpRequest request = 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 091146a7bd3..ca513c8c870 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 @@ -33,6 +33,7 @@ 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; @@ -40,6 +41,7 @@ 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; @@ -52,8 +54,7 @@ public class GitlabOAuthTokenFetcher implements PersonalAccessTokenFetcher { 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", "openid"); + public static final Set DEFAULT_TOKEN_SCOPES = ImmutableSet.of("api", "write_repository"); private final List registeredGitlabEndpoints; private final OAuthAPI oAuthAPI; @@ -105,22 +106,24 @@ public PersonalAccessToken fetchPersonalAccessToken(Subject cheSubject, String s OAuthToken oAuthToken; try { oAuthToken = oAuthAPI.getToken(OAUTH_PROVIDER_NAME); - GitlabUser user = gitlabApiClient.getUser(oAuthToken.getToken()); - PersonalAccessToken token = - new PersonalAccessToken( - scmServerUrl, - cheSubject.getUserId(), - user.getUsername(), - NameGenerator.generate(OAUTH_2_PREFIX, 5), - NameGenerator.generate("id-", 5), - oAuthToken.getToken()); - Optional valid = isValid(token); - if (valid.isEmpty() || !valid.get()) { + String tokenName = NameGenerator.generate(OAUTH_2_PREFIX, 5); + String tokenId = NameGenerator.generate("id-", 5); + Optional> valid = + isValid( + new PersonalAccessTokenParams( + scmServerUrl, tokenName, tokenId, oAuthToken.getToken(), null)); + if (valid.isEmpty() || !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.toString()); } - return token; + return new PersonalAccessToken( + scmServerUrl, + cheSubject.getUserId(), + valid.get().second, + tokenName, + tokenId, + oAuthToken.getToken()); } catch (UnauthorizedException e) { throw new ScmUnauthorizedException( cheSubject.getUserName() @@ -132,12 +135,7 @@ public PersonalAccessToken fetchPersonalAccessToken(Subject cheSubject, String s getLocalAuthenticateUrl()); } catch (NotFoundException nfe) { throw new UnknownScmProviderException(nfe.getMessage(), scmServerUrl); - } catch (ServerException - | ForbiddenException - | BadRequestException - | ScmItemNotFoundException - | ScmBadRequestException - | ConflictException e) { + } catch (ServerException | ForbiddenException | BadRequestException | ConflictException e) { LOG.warn(e.getMessage()); throw new ScmCommunicationException(e.getMessage(), e); } @@ -160,7 +158,8 @@ public Optional isValid(PersonalAccessToken personalAccessToken) { && personalAccessToken.getScmTokenName().startsWith(OAUTH_2_PREFIX)) { // validation OAuth token by special API call try { - GitlabOauthTokenInfo info = gitlabApiClient.getTokenInfo(personalAccessToken.getToken()); + GitlabOauthTokenInfo info = + gitlabApiClient.getOAuthTokenInfo(personalAccessToken.getToken()); return Optional.of(Sets.newHashSet(info.getScope()).containsAll(DEFAULT_TOKEN_SCOPES)); } catch (ScmItemNotFoundException | ScmCommunicationException e) { return Optional.of(Boolean.FALSE); @@ -181,6 +180,36 @@ public Optional isValid(PersonalAccessToken personalAccessToken) { } } + @Override + public Optional> isValid(PersonalAccessTokenParams params) { + 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()); + String[] scopes; + if (params.getScmTokenName() != null && params.getScmTokenName().startsWith(OAUTH_2_PREFIX)) { + scopes = gitlabApiClient.getOAuthTokenInfo(params.getToken()).getScope(); + } else { + scopes = gitlabApiClient.getPersonalAccessTokenInfo(params.getToken()).getScopes(); + } + return Optional.of( + Pair.of( + Sets.newHashSet(scopes).containsAll(DEFAULT_TOKEN_SCOPES) + ? Boolean.TRUE + : Boolean.FALSE, + user.getUsername())); + } catch (ScmItemNotFoundException | ScmCommunicationException | ScmBadRequestException e) { + return Optional.empty(); + } + } + private String getLocalAuthenticateUrl() { return apiEndpoint + "/oauth/authenticate?oauth_provider=" 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/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabPersonalAccessTokenInfo.java new file mode 100644 index 00000000000..ebfcf700f59 --- /dev/null +++ b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabPersonalAccessTokenInfo.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2012-2023 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.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.Arrays; +import java.util.Objects; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class GitlabPersonalAccessTokenInfo { + + private int id; + private String[] scopes; + private String expires_at; + private String created_at; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String[] getScopes() { + return scopes; + } + + public void setScopes(String[] scopes) { + this.scopes = scopes; + } + + public String getExpires_at() { + return expires_at; + } + + public void setExpires_at(String expires_at) { + this.expires_at = expires_at; + } + + public String getCreated_at() { + return created_at; + } + + public void setCreated_at(String created_at) { + this.created_at = created_at; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GitlabPersonalAccessTokenInfo info = (GitlabPersonalAccessTokenInfo) o; + return id == info.id + && Objects.equals(expires_at, info.expires_at) + && created_at == info.created_at + && Arrays.equals(scopes, info.scopes); + } + + @Override + public int hashCode() { + int result = Objects.hash(id, expires_at, created_at); + result = 31 * result + Arrays.hashCode(scopes); + return result; + } + + @Override + public String toString() { + return "GitlabOauthTokenInfo{" + + "resource_owner_id=" + + id + + ", scope=" + + Arrays.toString(scopes) + + ", expires_at=" + + expires_at + + ", created_at=" + + created_at + + '}'; + } +} 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 a5618662622..7545ab38eee 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 @@ -109,7 +109,7 @@ private boolean isApiRequestRelevant(String repositoryUrl) { try { // If the token request catches the unauthorised error, it means that the provided url // belongs to Gitlab. - gitlabApiClient.getTokenInfo(""); + gitlabApiClient.getOAuthTokenInfo(""); } catch (ScmCommunicationException e) { return e.getStatusCode() == HTTP_UNAUTHORIZED; } catch (ScmItemNotFoundException e) { diff --git a/wsmaster/che-core-api-factory-gitlab/src/test/java/org/eclipse/che/api/factory/server/gitlab/GitlabApiClientTest.java b/wsmaster/che-core-api-factory-gitlab/src/test/java/org/eclipse/che/api/factory/server/gitlab/GitlabApiClientTest.java index e48c0a8535e..11cd301264b 100644 --- a/wsmaster/che-core-api-factory-gitlab/src/test/java/org/eclipse/che/api/factory/server/gitlab/GitlabApiClientTest.java +++ b/wsmaster/che-core-api-factory-gitlab/src/test/java/org/eclipse/che/api/factory/server/gitlab/GitlabApiClientTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2021 Red Hat, Inc. + * Copyright (c) 2012-2023 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/ @@ -17,12 +17,16 @@ 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.api.factory.server.gitlab.GitlabOAuthTokenFetcher.DEFAULT_TOKEN_SCOPES; +import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.common.Slf4jNotifier; +import com.google.common.collect.Sets; import com.google.common.net.HttpHeaders; import org.mockito.testng.MockitoTestNGListener; import org.testng.annotations.AfterMethod; @@ -66,6 +70,26 @@ public void testGetUser() throws Exception { assertNotNull(user); } + @Test + public void shouldGetPersonalAccessTokenInfo() throws Exception { + // given + stubFor( + get(urlEqualTo("/api/v4/personal_access_tokens/self")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer token1")) + .willReturn( + aResponse() + .withHeader("Content-Type", "application/json; charset=utf-8") + .withBodyFile("gitlab/rest/api/v4/user/PAT_info.json"))); + + // when + GitlabPersonalAccessTokenInfo tokenInfo = client.getPersonalAccessTokenInfo("token1"); + + // then + assertNotNull(tokenInfo); + assertEquals(tokenInfo.getId(), 1); + assertTrue(Sets.newHashSet(tokenInfo.getScopes()).containsAll(DEFAULT_TOKEN_SCOPES)); + } + @Test public void shouldReturnFalseOnConnectedToOtherHost() { assertFalse(client.isConnected("https://other.com")); 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 b61650c490d..3d46072f6f7 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 @@ -27,11 +27,14 @@ import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.common.Slf4jNotifier; import com.google.common.net.HttpHeaders; +import java.util.Optional; import org.eclipse.che.api.auth.shared.dto.OAuthToken; import org.eclipse.che.api.core.UnauthorizedException; import org.eclipse.che.api.factory.server.scm.PersonalAccessToken; +import org.eclipse.che.api.factory.server.scm.PersonalAccessTokenParams; import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException; import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException; +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; @@ -72,7 +75,7 @@ void stop() { @Test( expectedExceptions = ScmCommunicationException.class, expectedExceptionsMessageRegExp = - "Current token doesn't have the necessary privileges. Please make sure Che app scopes are correct and containing at least: \\[api, write_repository, openid\\]") + "Current token doesn't have the necessary privileges. Please make sure Che app scopes are correct and containing at least: \\[api, write_repository\\]") public void shouldThrowExceptionOnInsufficientTokenScopes() throws Exception { Subject subject = new SubjectImpl("Username", "id1", "token", false); OAuthToken oAuthToken = newDto(OAuthToken.class).withToken("oauthtoken").withScope("api repo"); @@ -157,7 +160,7 @@ public void shouldThrowScmCommunicationExceptionWhenNoOauthIsConfigured() throws } @Test - public void shouldValidatePersonalToken() throws Exception { + public void shouldValidateOAuthToken() throws Exception { stubFor( get(urlEqualTo("/api/v4/user")) .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer token123")) @@ -165,16 +168,21 @@ public void shouldValidatePersonalToken() throws Exception { aResponse() .withHeader("Content-Type", "application/json; charset=utf-8") .withBodyFile("gitlab/rest/api/v4/user/response.json"))); + stubFor( + get(urlEqualTo("/oauth/token/info")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer token123")) + .willReturn( + aResponse() + .withHeader("Content-Type", "application/json; charset=utf-8") + .withBodyFile("gitlab/rest/api/v4/user/token_info.json"))); - PersonalAccessToken token = - new PersonalAccessToken( - wireMockServer.baseUrl(), - "cheUser", - "john_smith", - "token-name", - "tid-23434", - "token123"); - - assertTrue(oAuthTokenFetcher.isValid(token).get()); + PersonalAccessTokenParams params = + new PersonalAccessTokenParams( + wireMockServer.baseUrl(), "oauth2-token-name", "tid-23434", "token123", null); + + Optional> valid = oAuthTokenFetcher.isValid(params); + + assertTrue(valid.isPresent()); + assertTrue(valid.get().first); } } diff --git a/wsmaster/che-core-api-factory-gitlab/src/test/resources/__files/gitlab/rest/api/v4/user/PAT_info.json b/wsmaster/che-core-api-factory-gitlab/src/test/resources/__files/gitlab/rest/api/v4/user/PAT_info.json new file mode 100644 index 00000000000..a6d17c59534 --- /dev/null +++ b/wsmaster/che-core-api-factory-gitlab/src/test/resources/__files/gitlab/rest/api/v4/user/PAT_info.json @@ -0,0 +1,7 @@ +{ + "id": 1, + "name": "gitlab", + "scopes": ["api", "write_repository"], + "expires_at": "2024-07-18", + "created_at": "2023-07-19T07:41:19.492Z" +} \ No newline at end of file diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/scm/PersonalAccessTokenFetcher.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/scm/PersonalAccessTokenFetcher.java index 0c46e781caa..70f8bd0a706 100644 --- a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/scm/PersonalAccessTokenFetcher.java +++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/scm/PersonalAccessTokenFetcher.java @@ -15,6 +15,7 @@ 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.factory.server.scm.exception.UnknownScmProviderException; +import org.eclipse.che.commons.lang.Pair; import org.eclipse.che.commons.subject.Subject; public interface PersonalAccessTokenFetcher { @@ -41,6 +42,7 @@ PersonalAccessToken fetchPersonalAccessToken(Subject cheUser, String scmServerUr * Checks whether the provided personal access token is valid and has expected scope of * permissions. * + * @deprecated use {@link #isValid(PersonalAccessTokenParams)} instead. * @param personalAccessToken - personal access token to check. * @return - empty optional if {@link PersonalAccessTokenFetcher} is not able to confirm or deny * that token is valid or {@link Boolean} value if it can. @@ -49,6 +51,19 @@ PersonalAccessToken fetchPersonalAccessToken(Subject cheUser, String scmServerUr * @throws ScmCommunicationException - Some unexpected problem occurred during communication with * scm provider. */ + @Deprecated Optional isValid(PersonalAccessToken personalAccessToken) throws ScmCommunicationException, ScmUnauthorizedException; + + /** + * Checks whether the provided personal access token is valid by fetching user info from the scm + * provider. Also checks whether the token has expected scope of permissions if the provider API + * supports such request. + * + * @return - Optional with a pair of boolean value and scm username. The boolean value is true if + * the token has expected scope of permissions, false if the token scopes does not match the + * expected ones. Empty optional if {@link PersonalAccessTokenFetcher} is not able to confirm + * or deny that token is valid. + */ + Optional> isValid(PersonalAccessTokenParams params); } diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/scm/PersonalAccessTokenParams.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/scm/PersonalAccessTokenParams.java new file mode 100644 index 00000000000..3b803b59e64 --- /dev/null +++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/scm/PersonalAccessTokenParams.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2012-2023 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; + +/** An object to hold parameters for creating a personal access token. */ +public class PersonalAccessTokenParams { + private final String scmProviderUrl; + private final String scmTokenName; + private final String scmTokenId; + private final String token; + private final String organization; + + public PersonalAccessTokenParams( + String scmProviderUrl, + String scmTokenName, + String scmTokenId, + String token, + String organization) { + this.scmProviderUrl = scmProviderUrl; + this.scmTokenName = scmTokenName; + this.scmTokenId = scmTokenId; + this.token = token; + this.organization = organization; + } + + public String getScmProviderUrl() { + return scmProviderUrl; + } + + public String getScmTokenName() { + return scmTokenName; + } + + public String getScmTokenId() { + return scmTokenId; + } + + public String getToken() { + return token; + } + + public String getOrganization() { + return organization; + } +} diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/scm/ScmPersonalAccessTokenFetcher.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/scm/ScmPersonalAccessTokenFetcher.java index 224cf30e27b..6178e00c3d4 100644 --- a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/scm/ScmPersonalAccessTokenFetcher.java +++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/scm/ScmPersonalAccessTokenFetcher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2021 Red Hat, Inc. + * Copyright (c) 2012-2023 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/ @@ -17,6 +17,8 @@ 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.factory.server.scm.exception.UnknownScmProviderException; +import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.commons.lang.Pair; import org.eclipse.che.commons.subject.Subject; /** @@ -55,6 +57,7 @@ public PersonalAccessToken fetchPersonalAccessToken(Subject cheUser, String scmS * Iterate over the Set declared in container and sequentially invoke * {@link PersonalAccessTokenFetcher#isValid(PersonalAccessToken)} method. * + * @deprecated use {@link #isValid(PersonalAccessTokenParams)} instead. * @throws UnknownScmProviderException - if none of PersonalAccessTokenFetchers return a * meaningful result. */ @@ -71,4 +74,21 @@ public boolean isValid(PersonalAccessToken personalAccessToken) "No PersonalAccessTokenFetcher configured for " + personalAccessToken.getScmProviderUrl(), personalAccessToken.getScmProviderUrl()); } + + /** + * Iterate over the Set declared in container and sequentially invoke + * {@link PersonalAccessTokenFetcher#isValid(PersonalAccessTokenParams)} method. If any of the + * fetchers return an scm username, return it. Otherwise, return null. + */ + @Nullable + public String isValid(PersonalAccessTokenParams params) + throws UnknownScmProviderException, ScmUnauthorizedException, ScmCommunicationException { + for (PersonalAccessTokenFetcher fetcher : personalAccessTokenFetchers) { + Optional> isValid = fetcher.isValid(params); + if (isValid.isPresent() && isValid.get().first) { + return isValid.get().second; + } + } + return null; + } }