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
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
@ -12,6 +12,7 @@
package org.eclipse.che.security.oauth;
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.util.store.MemoryDataStoreFactory;
@ -77,7 +78,7 @@ public class BitbucketOAuthAuthenticator extends OAuthAuthenticator {
private String getTestRequestUrl() {
return "https://bitbucket.org".equals(bitbucketEndpoint)
? "https://api.bitbucket.org/2.0/user"
: bitbucketEndpoint + "/rest/api/1.0/application-properties";
: bitbucketEndpoint + "/plugins/servlet/applinks/whoami";
}
@Override
@ -89,11 +90,12 @@ public class BitbucketOAuthAuthenticator extends OAuthAuthenticator {
throws OAuthAuthenticationException {
HttpURLConnection urlConnection = null;
InputStream urlInputStream = null;
String result;
try {
urlConnection = (HttpURLConnection) new URL(requestUrl).openConnection();
urlConnection.setRequestProperty("Authorization", "Bearer " + accessToken);
urlInputStream = urlConnection.getInputStream();
result = new String(urlInputStream.readAllBytes(), UTF_8);
} catch (IOException e) {
throw new OAuthAuthenticationException(e.getMessage(), e);
} finally {
@ -108,5 +110,8 @@ public class BitbucketOAuthAuthenticator extends OAuthAuthenticator {
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 {
BitbucketPersonalAccessToken bitbucketPersonalAccessToken =
bitbucketServerApiClient.getPersonalAccessToken(personalAccessToken.getScmTokenId());
bitbucketServerApiClient.getPersonalAccessToken(
personalAccessToken.getScmTokenId(), personalAccessToken.getToken());
return Optional.of(DEFAULT_TOKEN_SCOPE.equals(bitbucketPersonalAccessToken.getPermissions()));
} catch (ScmItemNotFoundException e) {
return Optional.of(Boolean.FALSE);
@ -169,7 +170,8 @@ public class BitbucketServerPersonalAccessTokenFetcher implements PersonalAccess
}
// Token is added by OAuth. Token id is available.
BitbucketPersonalAccessToken bitbucketPersonalAccessToken =
bitbucketServerApiClient.getPersonalAccessToken(params.getScmTokenId());
bitbucketServerApiClient.getPersonalAccessToken(
params.getScmTokenId(), params.getToken());
return Optional.of(
Pair.of(
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
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
@ -145,7 +145,9 @@ public class HttpBitbucketServerApiClient implements BitbucketServerApiClient {
@Override
public void deletePersonalAccessTokens(String tokenId)
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.newBuilder(uri)
.DELETE()
@ -185,7 +187,7 @@ public class HttpBitbucketServerApiClient implements BitbucketServerApiClient {
ScmItemNotFoundException {
BitbucketPersonalAccessToken token =
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 {
HttpRequest request =
@ -229,7 +231,7 @@ public class HttpBitbucketServerApiClient implements BitbucketServerApiClient {
return doGetItems(
Optional.empty(),
BitbucketPersonalAccessToken.class,
"./rest/access-tokens/1.0/users/" + getUserSlug(),
"./rest/access-tokens/1.0/users/" + getUserSlug(Optional.empty()),
null);
} catch (ScmBadRequestException e) {
throw new ScmCommunicationException(e.getMessage(), e);
@ -237,14 +239,19 @@ public class HttpBitbucketServerApiClient implements BitbucketServerApiClient {
}
@Override
public BitbucketPersonalAccessToken getPersonalAccessToken(String tokenId)
public BitbucketPersonalAccessToken getPersonalAccessToken(String tokenId, String oauthToken)
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.newBuilder(uri)
.headers(
"Authorization",
computeAuthorizationHeader("GET", uri.toString()),
"Bearer " + oauthToken,
HttpHeaders.ACCEPT,
MediaType.APPLICATION_JSON)
.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 {
return getUser(Optional.empty()).getSlug();
return getUser(token).getSlug();
}
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
* available under the terms of the Eclipse Public License 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 oauthToken - bitbucket oauth token.
* @return - Bitbucket personal access token.
* @throws ScmCommunicationException
*/
BitbucketPersonalAccessToken getPersonalAccessToken(String tokenId)
BitbucketPersonalAccessToken getPersonalAccessToken(String tokenId, String oauthToken)
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
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
@ -77,7 +77,7 @@ public class NoopBitbucketServerApiClient implements BitbucketServerApiClient {
}
@Override
public BitbucketPersonalAccessToken getPersonalAccessToken(String tokenId)
public BitbucketPersonalAccessToken getPersonalAccessToken(String tokenId, String oauthToken)
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
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
* available under the terms of the Eclipse Public License 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 {
// given
when(personalAccessTokenParams.getScmProviderUrl()).thenReturn(someBitbucketURL);
when(personalAccessTokenParams.getToken()).thenReturn(bitbucketPersonalAccessToken.getToken());
when(personalAccessTokenParams.getScmTokenId())
.thenReturn(bitbucketPersonalAccessToken.getId());
when(bitbucketServerApiClient.isConnected(eq(someBitbucketURL))).thenReturn(true);
when(bitbucketServerApiClient.getPersonalAccessToken(eq(bitbucketPersonalAccessToken.getId())))
when(bitbucketServerApiClient.getPersonalAccessToken(
eq(bitbucketPersonalAccessToken.getId()), eq(bitbucketPersonalAccessToken.getToken())))
.thenReturn(bitbucketPersonalAccessToken);
// when
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
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
@ -312,14 +312,14 @@ public class HttpBitbucketServerApiClientTest {
// given
stubFor(
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))
.willReturn(
ok().withBodyFile("bitbucket/rest/access-tokens/1.0/users/ksmster/newtoken.json")));
stubFor(
get(urlPathEqualTo("/rest/api/1.0/users"))
.withHeader(HttpHeaders.AUTHORIZATION, equalTo(AUTHORIZATION_TOKEN))
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer token"))
.withQueryParam("start", equalTo("0"))
.withQueryParam("limit", equalTo("25"))
.willReturn(
@ -328,7 +328,7 @@ public class HttpBitbucketServerApiClientTest {
.withBodyFile("bitbucket/rest/api/1.0/users/filtered/response.json")));
// when
BitbucketPersonalAccessToken result = bitbucketServer.getPersonalAccessToken("5");
BitbucketPersonalAccessToken result = bitbucketServer.getPersonalAccessToken("5", "token");
// then
assertNotNull(result);
assertEquals(result.getToken(), "MTU4OTEwNTMyOTA5Ohc88HcY8k7gWOzl2mP5TtdtY5Qs");
@ -346,7 +346,7 @@ public class HttpBitbucketServerApiClientTest {
.willReturn(notFound()));
// when
bitbucketServer.getPersonalAccessToken("5");
bitbucketServer.getPersonalAccessToken("5", "token");
}
@Test(expectedExceptions = ScmUnauthorizedException.class)
@ -361,18 +361,18 @@ public class HttpBitbucketServerApiClientTest {
}
@Test(expectedExceptions = ScmUnauthorizedException.class)
public void shouldBeAbleToThrowScmUnauthorizedExceptionOnGePAT()
public void shouldBeAbleToThrowScmUnauthorizedExceptionOnGetPAT()
throws ScmCommunicationException, ScmUnauthorizedException, ScmItemNotFoundException {
// given
stubFor(
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))
.willReturn(unauthorized()));
stubFor(
get(urlPathEqualTo("/rest/api/1.0/users"))
.withHeader(HttpHeaders.AUTHORIZATION, equalTo(AUTHORIZATION_TOKEN))
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer token"))
.withQueryParam("start", equalTo("0"))
.withQueryParam("limit", equalTo("25"))
.willReturn(
@ -381,7 +381,7 @@ public class HttpBitbucketServerApiClientTest {
.withBodyFile("bitbucket/rest/api/1.0/users/filtered/response.json")));
// when
bitbucketServer.getPersonalAccessToken("5");
bitbucketServer.getPersonalAccessToken("5", "token");
}
@Test(
@ -400,7 +400,7 @@ public class HttpBitbucketServerApiClientTest {
wireMockServer.url("/"), new NoopOAuthAuthenticator(), oAuthAPI, apiEndpoint);
// when
localServer.getPersonalAccessToken("5");
localServer.getUser();
}
@Test