Rework the Bitbucket Server oauth token validation

Signed-off-by: ivinokur <ivinokur@redhat.com>
pull/673/head
ivinokur 2024-03-27 14:54:50 +02:00
parent 2e27c47f2f
commit 1baf3aff63
7 changed files with 47 additions and 30 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2012-2023 Red Hat, Inc. * Copyright (c) 2012-2024 Red Hat, Inc.
* This program and the accompanying materials are made * This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0 * available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/ * which is available at https://www.eclipse.org/legal/epl-2.0/
@ -12,6 +12,7 @@
package org.eclipse.che.security.oauth; package org.eclipse.che.security.oauth;
import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.base.Strings.isNullOrEmpty;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl; import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl;
import com.google.api.client.util.store.MemoryDataStoreFactory; import com.google.api.client.util.store.MemoryDataStoreFactory;
@ -77,7 +78,7 @@ public class BitbucketOAuthAuthenticator extends OAuthAuthenticator {
private String getTestRequestUrl() { private String getTestRequestUrl() {
return "https://bitbucket.org".equals(bitbucketEndpoint) return "https://bitbucket.org".equals(bitbucketEndpoint)
? "https://api.bitbucket.org/2.0/user" ? "https://api.bitbucket.org/2.0/user"
: bitbucketEndpoint + "/rest/api/1.0/application-properties"; : bitbucketEndpoint + "/plugins/servlet/applinks/whoami";
} }
@Override @Override
@ -89,11 +90,12 @@ public class BitbucketOAuthAuthenticator extends OAuthAuthenticator {
throws OAuthAuthenticationException { throws OAuthAuthenticationException {
HttpURLConnection urlConnection = null; HttpURLConnection urlConnection = null;
InputStream urlInputStream = null; InputStream urlInputStream = null;
String result;
try { try {
urlConnection = (HttpURLConnection) new URL(requestUrl).openConnection(); urlConnection = (HttpURLConnection) new URL(requestUrl).openConnection();
urlConnection.setRequestProperty("Authorization", "Bearer " + accessToken); urlConnection.setRequestProperty("Authorization", "Bearer " + accessToken);
urlInputStream = urlConnection.getInputStream(); urlInputStream = urlConnection.getInputStream();
result = new String(urlInputStream.readAllBytes(), UTF_8);
} catch (IOException e) { } catch (IOException e) {
throw new OAuthAuthenticationException(e.getMessage(), e); throw new OAuthAuthenticationException(e.getMessage(), e);
} finally { } finally {
@ -108,5 +110,8 @@ public class BitbucketOAuthAuthenticator extends OAuthAuthenticator {
urlConnection.disconnect(); urlConnection.disconnect();
} }
} }
if (isNullOrEmpty(result)) {
throw new OAuthAuthenticationException("Empty response from Bitbucket Server API");
}
} }
} }

View File

@ -134,7 +134,8 @@ public class BitbucketServerPersonalAccessTokenFetcher implements PersonalAccess
} }
try { try {
BitbucketPersonalAccessToken bitbucketPersonalAccessToken = BitbucketPersonalAccessToken bitbucketPersonalAccessToken =
bitbucketServerApiClient.getPersonalAccessToken(personalAccessToken.getScmTokenId()); bitbucketServerApiClient.getPersonalAccessToken(
personalAccessToken.getScmTokenId(), personalAccessToken.getToken());
return Optional.of(DEFAULT_TOKEN_SCOPE.equals(bitbucketPersonalAccessToken.getPermissions())); return Optional.of(DEFAULT_TOKEN_SCOPE.equals(bitbucketPersonalAccessToken.getPermissions()));
} catch (ScmItemNotFoundException e) { } catch (ScmItemNotFoundException e) {
return Optional.of(Boolean.FALSE); return Optional.of(Boolean.FALSE);
@ -169,7 +170,8 @@ public class BitbucketServerPersonalAccessTokenFetcher implements PersonalAccess
} }
// Token is added by OAuth. Token id is available. // Token is added by OAuth. Token id is available.
BitbucketPersonalAccessToken bitbucketPersonalAccessToken = BitbucketPersonalAccessToken bitbucketPersonalAccessToken =
bitbucketServerApiClient.getPersonalAccessToken(params.getScmTokenId()); bitbucketServerApiClient.getPersonalAccessToken(
params.getScmTokenId(), params.getToken());
return Optional.of( return Optional.of(
Pair.of( Pair.of(
DEFAULT_TOKEN_SCOPE.equals(bitbucketPersonalAccessToken.getPermissions()) DEFAULT_TOKEN_SCOPE.equals(bitbucketPersonalAccessToken.getPermissions())

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2012-2023 Red Hat, Inc. * Copyright (c) 2012-2024 Red Hat, Inc.
* This program and the accompanying materials are made * This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0 * available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/ * which is available at https://www.eclipse.org/legal/epl-2.0/
@ -145,7 +145,9 @@ public class HttpBitbucketServerApiClient implements BitbucketServerApiClient {
@Override @Override
public void deletePersonalAccessTokens(String tokenId) public void deletePersonalAccessTokens(String tokenId)
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException { throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
URI uri = serverUri.resolve("./rest/access-tokens/1.0/users/" + getUserSlug() + "/" + tokenId); URI uri =
serverUri.resolve(
"./rest/access-tokens/1.0/users/" + getUserSlug(Optional.empty()) + "/" + tokenId);
HttpRequest request = HttpRequest request =
HttpRequest.newBuilder(uri) HttpRequest.newBuilder(uri)
.DELETE() .DELETE()
@ -185,7 +187,7 @@ public class HttpBitbucketServerApiClient implements BitbucketServerApiClient {
ScmItemNotFoundException { ScmItemNotFoundException {
BitbucketPersonalAccessToken token = BitbucketPersonalAccessToken token =
new BitbucketPersonalAccessToken(tokenName, permissions, 90); new BitbucketPersonalAccessToken(tokenName, permissions, 90);
URI uri = serverUri.resolve("./rest/access-tokens/1.0/users/" + getUserSlug()); URI uri = serverUri.resolve("./rest/access-tokens/1.0/users/" + getUserSlug(Optional.empty()));
try { try {
HttpRequest request = HttpRequest request =
@ -229,7 +231,7 @@ public class HttpBitbucketServerApiClient implements BitbucketServerApiClient {
return doGetItems( return doGetItems(
Optional.empty(), Optional.empty(),
BitbucketPersonalAccessToken.class, BitbucketPersonalAccessToken.class,
"./rest/access-tokens/1.0/users/" + getUserSlug(), "./rest/access-tokens/1.0/users/" + getUserSlug(Optional.empty()),
null); null);
} catch (ScmBadRequestException e) { } catch (ScmBadRequestException e) {
throw new ScmCommunicationException(e.getMessage(), e); throw new ScmCommunicationException(e.getMessage(), e);
@ -237,14 +239,19 @@ public class HttpBitbucketServerApiClient implements BitbucketServerApiClient {
} }
@Override @Override
public BitbucketPersonalAccessToken getPersonalAccessToken(String tokenId) public BitbucketPersonalAccessToken getPersonalAccessToken(String tokenId, String oauthToken)
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException { throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
URI uri = serverUri.resolve("./rest/access-tokens/1.0/users/" + getUserSlug() + "/" + tokenId); URI uri =
serverUri.resolve(
"./rest/access-tokens/1.0/users/"
+ getUserSlug(Optional.of(oauthToken))
+ "/"
+ tokenId);
HttpRequest request = HttpRequest request =
HttpRequest.newBuilder(uri) HttpRequest.newBuilder(uri)
.headers( .headers(
"Authorization", "Authorization",
computeAuthorizationHeader("GET", uri.toString()), "Bearer " + oauthToken,
HttpHeaders.ACCEPT, HttpHeaders.ACCEPT,
MediaType.APPLICATION_JSON) MediaType.APPLICATION_JSON)
.timeout(DEFAULT_HTTP_TIMEOUT) .timeout(DEFAULT_HTTP_TIMEOUT)
@ -269,9 +276,9 @@ public class HttpBitbucketServerApiClient implements BitbucketServerApiClient {
} }
} }
private String getUserSlug() private String getUserSlug(Optional<String> token)
throws ScmItemNotFoundException, ScmCommunicationException, ScmUnauthorizedException { throws ScmItemNotFoundException, ScmCommunicationException, ScmUnauthorizedException {
return getUser(Optional.empty()).getSlug(); return getUser(token).getSlug();
} }
private BitbucketUser getUser(Optional<String> token) private BitbucketUser getUser(Optional<String> token)

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2012-2023 Red Hat, Inc. * Copyright (c) 2012-2024 Red Hat, Inc.
* This program and the accompanying materials are made * This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0 * available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/ * which is available at https://www.eclipse.org/legal/epl-2.0/
@ -100,9 +100,10 @@ public interface BitbucketServerApiClient {
/** /**
* @param tokenId - bitbucket personal access token id. * @param tokenId - bitbucket personal access token id.
* @param oauthToken - bitbucket oauth token.
* @return - Bitbucket personal access token. * @return - Bitbucket personal access token.
* @throws ScmCommunicationException * @throws ScmCommunicationException
*/ */
BitbucketPersonalAccessToken getPersonalAccessToken(String tokenId) BitbucketPersonalAccessToken getPersonalAccessToken(String tokenId, String oauthToken)
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException; throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException;
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2012-2023 Red Hat, Inc. * Copyright (c) 2012-2024 Red Hat, Inc.
* This program and the accompanying materials are made * This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0 * available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/ * which is available at https://www.eclipse.org/legal/epl-2.0/
@ -77,7 +77,7 @@ public class NoopBitbucketServerApiClient implements BitbucketServerApiClient {
} }
@Override @Override
public BitbucketPersonalAccessToken getPersonalAccessToken(String tokenId) public BitbucketPersonalAccessToken getPersonalAccessToken(String tokenId, String oauthToken)
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException { throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
throw new RuntimeException("Invalid usage of BitbucketServerApi"); throw new RuntimeException("Invalid usage of BitbucketServerApi");
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2012-2023 Red Hat, Inc. * Copyright (c) 2012-2024 Red Hat, Inc.
* This program and the accompanying materials are made * This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0 * available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/ * which is available at https://www.eclipse.org/legal/epl-2.0/
@ -221,10 +221,12 @@ public class BitbucketServerPersonalAccessTokenFetcherTest {
throws ScmUnauthorizedException, ScmCommunicationException, ScmItemNotFoundException { throws ScmUnauthorizedException, ScmCommunicationException, ScmItemNotFoundException {
// given // given
when(personalAccessTokenParams.getScmProviderUrl()).thenReturn(someBitbucketURL); when(personalAccessTokenParams.getScmProviderUrl()).thenReturn(someBitbucketURL);
when(personalAccessTokenParams.getToken()).thenReturn(bitbucketPersonalAccessToken.getToken());
when(personalAccessTokenParams.getScmTokenId()) when(personalAccessTokenParams.getScmTokenId())
.thenReturn(bitbucketPersonalAccessToken.getId()); .thenReturn(bitbucketPersonalAccessToken.getId());
when(bitbucketServerApiClient.isConnected(eq(someBitbucketURL))).thenReturn(true); when(bitbucketServerApiClient.isConnected(eq(someBitbucketURL))).thenReturn(true);
when(bitbucketServerApiClient.getPersonalAccessToken(eq(bitbucketPersonalAccessToken.getId()))) when(bitbucketServerApiClient.getPersonalAccessToken(
eq(bitbucketPersonalAccessToken.getId()), eq(bitbucketPersonalAccessToken.getToken())))
.thenReturn(bitbucketPersonalAccessToken); .thenReturn(bitbucketPersonalAccessToken);
// when // when
Optional<Pair<Boolean, String>> result = fetcher.isValid(personalAccessTokenParams); Optional<Pair<Boolean, String>> result = fetcher.isValid(personalAccessTokenParams);

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2012-2023 Red Hat, Inc. * Copyright (c) 2012-2024 Red Hat, Inc.
* This program and the accompanying materials are made * This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0 * available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/ * which is available at https://www.eclipse.org/legal/epl-2.0/
@ -312,14 +312,14 @@ public class HttpBitbucketServerApiClientTest {
// given // given
stubFor( stubFor(
get(urlPathEqualTo("/rest/access-tokens/1.0/users/ksmster/5")) get(urlPathEqualTo("/rest/access-tokens/1.0/users/ksmster/5"))
.withHeader(HttpHeaders.AUTHORIZATION, equalTo(AUTHORIZATION_TOKEN)) .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer token"))
.withHeader(HttpHeaders.ACCEPT, equalTo(MediaType.APPLICATION_JSON)) .withHeader(HttpHeaders.ACCEPT, equalTo(MediaType.APPLICATION_JSON))
.willReturn( .willReturn(
ok().withBodyFile("bitbucket/rest/access-tokens/1.0/users/ksmster/newtoken.json"))); ok().withBodyFile("bitbucket/rest/access-tokens/1.0/users/ksmster/newtoken.json")));
stubFor( stubFor(
get(urlPathEqualTo("/rest/api/1.0/users")) get(urlPathEqualTo("/rest/api/1.0/users"))
.withHeader(HttpHeaders.AUTHORIZATION, equalTo(AUTHORIZATION_TOKEN)) .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer token"))
.withQueryParam("start", equalTo("0")) .withQueryParam("start", equalTo("0"))
.withQueryParam("limit", equalTo("25")) .withQueryParam("limit", equalTo("25"))
.willReturn( .willReturn(
@ -328,7 +328,7 @@ public class HttpBitbucketServerApiClientTest {
.withBodyFile("bitbucket/rest/api/1.0/users/filtered/response.json"))); .withBodyFile("bitbucket/rest/api/1.0/users/filtered/response.json")));
// when // when
BitbucketPersonalAccessToken result = bitbucketServer.getPersonalAccessToken("5"); BitbucketPersonalAccessToken result = bitbucketServer.getPersonalAccessToken("5", "token");
// then // then
assertNotNull(result); assertNotNull(result);
assertEquals(result.getToken(), "MTU4OTEwNTMyOTA5Ohc88HcY8k7gWOzl2mP5TtdtY5Qs"); assertEquals(result.getToken(), "MTU4OTEwNTMyOTA5Ohc88HcY8k7gWOzl2mP5TtdtY5Qs");
@ -346,7 +346,7 @@ public class HttpBitbucketServerApiClientTest {
.willReturn(notFound())); .willReturn(notFound()));
// when // when
bitbucketServer.getPersonalAccessToken("5"); bitbucketServer.getPersonalAccessToken("5", "token");
} }
@Test(expectedExceptions = ScmUnauthorizedException.class) @Test(expectedExceptions = ScmUnauthorizedException.class)
@ -361,18 +361,18 @@ public class HttpBitbucketServerApiClientTest {
} }
@Test(expectedExceptions = ScmUnauthorizedException.class) @Test(expectedExceptions = ScmUnauthorizedException.class)
public void shouldBeAbleToThrowScmUnauthorizedExceptionOnGePAT() public void shouldBeAbleToThrowScmUnauthorizedExceptionOnGetPAT()
throws ScmCommunicationException, ScmUnauthorizedException, ScmItemNotFoundException { throws ScmCommunicationException, ScmUnauthorizedException, ScmItemNotFoundException {
// given // given
stubFor( stubFor(
get(urlPathEqualTo("/rest/access-tokens/1.0/users/ksmster/5")) get(urlPathEqualTo("/rest/access-tokens/1.0/users/ksmster/5"))
.withHeader(HttpHeaders.AUTHORIZATION, equalTo(AUTHORIZATION_TOKEN)) .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer token"))
.withHeader(HttpHeaders.ACCEPT, equalTo(MediaType.APPLICATION_JSON)) .withHeader(HttpHeaders.ACCEPT, equalTo(MediaType.APPLICATION_JSON))
.willReturn(unauthorized())); .willReturn(unauthorized()));
stubFor( stubFor(
get(urlPathEqualTo("/rest/api/1.0/users")) get(urlPathEqualTo("/rest/api/1.0/users"))
.withHeader(HttpHeaders.AUTHORIZATION, equalTo(AUTHORIZATION_TOKEN)) .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer token"))
.withQueryParam("start", equalTo("0")) .withQueryParam("start", equalTo("0"))
.withQueryParam("limit", equalTo("25")) .withQueryParam("limit", equalTo("25"))
.willReturn( .willReturn(
@ -381,7 +381,7 @@ public class HttpBitbucketServerApiClientTest {
.withBodyFile("bitbucket/rest/api/1.0/users/filtered/response.json"))); .withBodyFile("bitbucket/rest/api/1.0/users/filtered/response.json")));
// when // when
bitbucketServer.getPersonalAccessToken("5"); bitbucketServer.getPersonalAccessToken("5", "token");
} }
@Test( @Test(
@ -400,7 +400,7 @@ public class HttpBitbucketServerApiClientTest {
wireMockServer.url("/"), new NoopOAuthAuthenticator(), oAuthAPI, apiEndpoint); wireMockServer.url("/"), new NoopOAuthAuthenticator(), oAuthAPI, apiEndpoint);
// when // when
localServer.getPersonalAccessToken("5"); localServer.getUser();
} }
@Test @Test