Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Improve BitBucket token scopes validation #547

Merged
merged 1 commit into from
Aug 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import com.google.common.collect.Sets;
import java.util.Optional;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import org.eclipse.che.api.auth.shared.dto.OAuthToken;
Expand Down Expand Up @@ -52,7 +53,13 @@ public class BitbucketPersonalAccessTokenFetcher implements PersonalAccessTokenF
private static final String OAUTH_PROVIDER_NAME = "bitbucket";

/** OAuth scope required to make integration with Bitbucket work. */
public static final String DEFAULT_TOKEN_SCOPE = "repository:write";
public static final String DEFAULT_REPOSITORY_WRITE_TOKEN_SCOPE = "repository:write";

public static final String DEFAULT_PULLREQUEST_WRITE_TOKEN_SCOPE = "pullrequest:write";

public static final String DEFAULT_ACCOUNT_READ_TOKEN_SCOPE = "account";

public static final String DEFAULT_ACCOUNT_WRITE_TOKEN_SCOPE = "account:write";

@Inject
public BitbucketPersonalAccessTokenFetcher(
Expand Down Expand Up @@ -101,7 +108,9 @@ public PersonalAccessToken fetchPersonalAccessToken(Subject cheSubject, String s
} 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);
+ DEFAULT_REPOSITORY_WRITE_TOKEN_SCOPE
+ " and "
+ DEFAULT_ACCOUNT_READ_TOKEN_SCOPE);
}
return new PersonalAccessToken(
scmServerUrl,
Expand Down Expand Up @@ -136,7 +145,7 @@ public Optional<Boolean> isValid(PersonalAccessToken personalAccessToken) {

try {
String[] scopes = bitbucketApiClient.getTokenScopes(personalAccessToken.getToken()).second;
return Optional.of(Sets.newHashSet(scopes).contains(DEFAULT_TOKEN_SCOPE));
return Optional.of(isValidScope(Sets.newHashSet(scopes)));
} catch (ScmItemNotFoundException | ScmCommunicationException | ScmBadRequestException e) {
return Optional.of(Boolean.FALSE);
}
Expand All @@ -153,9 +162,7 @@ public Optional<Pair<Boolean, String>> isValid(PersonalAccessTokenParams params)
Pair<String, String[]> pair = bitbucketApiClient.getTokenScopes(params.getToken());
return Optional.of(
Pair.of(
Sets.newHashSet(pair.second).contains(DEFAULT_TOKEN_SCOPE)
? Boolean.TRUE
: Boolean.FALSE,
isValidScope(Sets.newHashSet(pair.second)) ? Boolean.TRUE : Boolean.FALSE,
pair.first));
} catch (ScmItemNotFoundException | ScmCommunicationException | ScmBadRequestException e) {
return Optional.empty();
Expand All @@ -168,4 +175,15 @@ private String getLocalAuthenticateUrl() {
+ OAUTH_PROVIDER_NAME
+ "&scope=repository&request_method=POST&signature_method=rsa";
}

/**
* Checks if the given scopes are valid for Bitbucket. Note: that pullrequest:write is a wider
* scope than repository:write, and account:write is a wider scope than account.
*/
private boolean isValidScope(Set<String> scopes) {
return (scopes.contains(DEFAULT_REPOSITORY_WRITE_TOKEN_SCOPE)
|| scopes.contains(DEFAULT_PULLREQUEST_WRITE_TOKEN_SCOPE))
&& (scopes.contains(DEFAULT_ACCOUNT_READ_TOKEN_SCOPE)
|| scopes.contains(DEFAULT_ACCOUNT_WRITE_TOKEN_SCOPE));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,12 @@ public void testGetTokenScopes() throws Exception {
aResponse()
.withHeader("Content-Type", "application/json; charset=utf-8")
.withHeader(
BitbucketApiClient.BITBUCKET_OAUTH_SCOPES_HEADER, "repository:write")
BitbucketApiClient.BITBUCKET_OAUTH_SCOPES_HEADER,
"repository:write,account")
.withBodyFile("bitbucket/rest/user/response.json")));

String[] scopes = client.getTokenScopes("token1").second;
String[] expectedScopes = {"repository:write"};
String[] expectedScopes = {"repository:write", "account"};
assertNotNull(scopes, "Bitbucket API should have returned a non-null scope array");
assertEqualsNoOrder(
scopes, expectedScopes, "Returned scope array does not match expected values");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public void shouldNotValidateSCMServerWithTrailingSlash() throws Exception {
@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: repository:write")
"Current token doesn't have the necessary privileges. Please make sure Che app scopes are correct and containing at least: repository:write and account")
public void shouldThrowExceptionOnInsufficientTokenScopes() throws Exception {
Subject subject = new SubjectImpl("Username", "id1", "token", false);
OAuthToken oAuthToken = newDto(OAuthToken.class).withToken(bitbucketOauthToken).withScope("");
Expand Down Expand Up @@ -140,7 +140,8 @@ public void shouldReturnToken() throws Exception {
aResponse()
.withHeader("Content-Type", "application/json; charset=utf-8")
.withHeader(
BitbucketApiClient.BITBUCKET_OAUTH_SCOPES_HEADER, "repository:write")
BitbucketApiClient.BITBUCKET_OAUTH_SCOPES_HEADER,
"repository:write,account")
.withBodyFile("bitbucket/rest/user/response.json")));

PersonalAccessToken token =
Expand All @@ -158,7 +159,8 @@ public void shouldValidatePersonalToken() throws Exception {
aResponse()
.withHeader("Content-Type", "application/json; charset=utf-8")
.withHeader(
BitbucketApiClient.BITBUCKET_OAUTH_SCOPES_HEADER, "repository:write")
BitbucketApiClient.BITBUCKET_OAUTH_SCOPES_HEADER,
"repository:write,account")
.withBodyFile("bitbucket/rest/user/response.json")));

PersonalAccessTokenParams params =
Expand All @@ -179,7 +181,8 @@ public void shouldValidateOauthToken() throws Exception {
aResponse()
.withHeader("Content-Type", "application/json; charset=utf-8")
.withHeader(
BitbucketApiClient.BITBUCKET_OAUTH_SCOPES_HEADER, "repository:write")
BitbucketApiClient.BITBUCKET_OAUTH_SCOPES_HEADER,
"repository:write,account")
.withBodyFile("bitbucket/rest/user/response.json")));

PersonalAccessTokenParams params =
Expand Down