Rework the Bitbucket Server oauth token validation (#673)
- Change the validation API request to get current user request. The /rest/api/1.0/application-properties request is irrelevant as it does not require a token. - Pass oath token to the getPersonalAccessToken() API request in order to avoid circular getToken() request.pull/677/head
parent
c51919d1e3
commit
2cfce0568a
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ public class BitbucketServerPersonalAccessTokenFetcher implements PersonalAccess
|
|||
scmServerUrl,
|
||||
OAUTH_PROVIDER_NAME,
|
||||
EnvironmentContext.getCurrent().getSubject().getUserId(),
|
||||
user.getName(),
|
||||
null,
|
||||
user.getSlug(),
|
||||
token.getName(),
|
||||
valueOf(token.getId()),
|
||||
|
|
@ -111,30 +111,34 @@ public class BitbucketServerPersonalAccessTokenFetcher implements PersonalAccess
|
|||
}
|
||||
|
||||
@Override
|
||||
public Optional<Boolean> isValid(PersonalAccessToken personalAccessToken)
|
||||
public Optional<Boolean> isValid(PersonalAccessToken accessToken)
|
||||
throws ScmCommunicationException, ScmUnauthorizedException {
|
||||
if (!bitbucketServerApiClient.isConnected(personalAccessToken.getScmProviderUrl())) {
|
||||
if (!bitbucketServerApiClient.isConnected(accessToken.getScmProviderUrl())) {
|
||||
// If BitBucket oAuth is not configured check the manually added user namespace token.
|
||||
HttpBitbucketServerApiClient apiClient =
|
||||
new HttpBitbucketServerApiClient(
|
||||
personalAccessToken.getScmProviderUrl(),
|
||||
accessToken.getScmProviderUrl(),
|
||||
new NoopOAuthAuthenticator(),
|
||||
oAuthAPI,
|
||||
apiEndpoint.toString());
|
||||
try {
|
||||
apiClient.getUser(personalAccessToken.getToken());
|
||||
apiClient.getUser(accessToken.getToken());
|
||||
return Optional.of(Boolean.TRUE);
|
||||
} catch (ScmItemNotFoundException
|
||||
| ScmUnauthorizedException
|
||||
| ScmCommunicationException exception) {
|
||||
LOG.debug(
|
||||
"not a valid url {} for current fetcher ", personalAccessToken.getScmProviderUrl());
|
||||
LOG.debug("not a valid url {} for current fetcher ", accessToken.getScmProviderUrl());
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
try {
|
||||
BitbucketPersonalAccessToken bitbucketPersonalAccessToken =
|
||||
bitbucketServerApiClient.getPersonalAccessToken(personalAccessToken.getScmTokenId());
|
||||
bitbucketServerApiClient.getPersonalAccessToken(
|
||||
accessToken.getScmTokenId(),
|
||||
// Pass oauth token to fetch personal access token
|
||||
// TODO: rename the PersonalAccessToken interface to more generic name, so both OAuth
|
||||
// and personal access token implementations would be suitable.
|
||||
accessToken.getToken());
|
||||
return Optional.of(DEFAULT_TOKEN_SCOPE.equals(bitbucketPersonalAccessToken.getPermissions()));
|
||||
} catch (ScmItemNotFoundException e) {
|
||||
return Optional.of(Boolean.FALSE);
|
||||
|
|
@ -169,7 +173,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())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
@ -156,7 +156,7 @@ public class BitbucketServerPersonalAccessTokenFetcherTest {
|
|||
assertNotNull(result);
|
||||
assertEquals(result.getScmProviderUrl(), someBitbucketURL);
|
||||
assertEquals(result.getCheUserId(), subject.getUserId());
|
||||
assertEquals(result.getScmOrganization(), bitbucketUser.getName());
|
||||
assertNull(result.getScmOrganization(), bitbucketUser.getName());
|
||||
assertEquals(result.getScmTokenId(), valueOf(bitbucketPersonalAccessToken.getId()));
|
||||
assertEquals(result.getToken(), bitbucketPersonalAccessToken.getToken());
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue