revert: Bitbucket OAuth 1.0 → 2.0 migration (PR #772)#773
revert: Bitbucket OAuth 1.0 → 2.0 migration (PR #772)#773drazisil-codecov wants to merge 11 commits intomainfrom
Conversation
…eover Generate a cryptographically random state on redirect, store it in a signed httponly cookie, and validate it matches before exchanging the authorization code. Also updates tests for the OAuth 2.0 flow. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove OAuth 1.0 query params (oauth_consumer_key, oauth_token, oauth_version) from cassette URIs to match the new Bearer token request format. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Access tokens expire in ~2h; refresh using stored refresh_token. Removes the no-op early returns in the token refresh callbacks for both API and worker that were left over from OAuth 1.0. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…h errors - Bitbucket Server still uses OAuth 1.0 so restore the return None guard that was incorrectly removed alongside the BITBUCKET guard - Catch TorngitClientGeneralError/5xx from refresh_token() in api() so an expired/revoked refresh token raises the original 401 instead of a confusing refresh error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Set secure=settings.SESSION_COOKIE_SECURE on _bb_oauth_state cookie so it isn't transmitted over plain HTTP - Remove unused original_url param from refresh_token() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents state cookie from being sent on cross-site requests when SameSite=None is configured (e.g. staging). Also caps cookie lifetime at 300s to limit the window for state reuse. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract HTTP call into _send_request() helper and make the 401 token refresh retry explicit instead of using while/continue. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reverts all changes from the Bitbucket OAuth 2.0 migration in case rollback is needed. Restores OAuth 1.0 signing and original token refresh behavior. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Unrelated documentation included in Bitbucket OAuth revert PR
- Removed the accidentally committed owner-deletion-chunked-in-filter.md documentation file from the revert commit and amended the commit.
Or push these changes by commenting:
@cursor push c0b7aa2b21
Preview (c0b7aa2b21)
diff --git a/apps/codecov-api/codecov_auth/tests/unit/views/test_bitbucket.py b/apps/codecov-api/codecov_auth/tests/unit/views/test_bitbucket.py
--- a/apps/codecov-api/codecov_auth/tests/unit/views/test_bitbucket.py
+++ b/apps/codecov-api/codecov_auth/tests/unit/views/test_bitbucket.py
@@ -8,16 +8,18 @@
from codecov_auth.models import Owner
from codecov_auth.views.bitbucket import BitbucketLoginView
from shared.torngit.bitbucket import Bitbucket
-from shared.torngit.exceptions import (
- TorngitClientGeneralError,
-)
+from shared.torngit.exceptions import TorngitServer5xxCodeError
+from utils.encryption import encryptor
def test_get_bitbucket_redirect(client, settings, mocker):
- mocked_generate = mocker.patch.object(
+ mocked_get = mocker.patch.object(
Bitbucket,
- "generate_redirect_url",
- return_value="https://bitbucket.org/site/oauth2/authorize?client_id=testqmo19ebdkseoby&response_type=code&redirect_uri=http%3A%2F%2Flocalhost&state=teststate",
+ "generate_request_token",
+ return_value={
+ "oauth_token": "testy6r2of6ajkmrub",
+ "oauth_token_secret": "testzibw5q01scpl8qeeupzh8u9yu8hz",
+ },
)
settings.BITBUCKET_REDIRECT_URI = "http://localhost"
settings.BITBUCKET_CLIENT_ID = "testqmo19ebdkseoby"
@@ -26,24 +28,20 @@
res = client.get(url, SERVER_NAME="localhost:8000")
assert res.status_code == 302
- assert "_bb_oauth_state" in res.cookies
- cookie = res.cookies["_bb_oauth_state"]
+ assert "_oauth_request_token" in res.cookies
+ cookie = res.cookies["_oauth_request_token"]
assert cookie.value
assert cookie.get("domain") == settings.COOKIES_DOMAIN
- assert cookie.get("secure")
- assert cookie.get("samesite") == settings.COOKIE_SAME_SITE
- assert cookie.get("max-age") == 300
- assert mocked_generate.call_count == 1
- # state kwarg was passed through
- _, kwargs = mocked_generate.call_args
- assert kwargs.get("state") is not None
+ assert (
+ res.url
+ == "https://bitbucket.org/api/1.0/oauth/authenticate?oauth_token=testy6r2of6ajkmrub"
+ )
+ mocked_get.assert_called_with(settings.BITBUCKET_REDIRECT_URI)
-def test_get_bitbucket_redirect_bitbucket_error(client, settings, mocker):
- mocker.patch.object(
- Bitbucket,
- "generate_redirect_url",
- side_effect=TorngitClientGeneralError(400, {}, "bad request"),
+def test_get_bitbucket_redirect_bitbucket_unavailable(client, settings, mocker):
+ mocked_get = mocker.patch.object(
+ Bitbucket, "generate_request_token", side_effect=TorngitServer5xxCodeError()
)
settings.BITBUCKET_REDIRECT_URI = "http://localhost"
settings.BITBUCKET_CLIENT_ID = "testqmo19ebdkseoby"
@@ -51,8 +49,9 @@
url = reverse("bitbucket-login")
res = client.get(url, SERVER_NAME="localhost:8000")
assert res.status_code == 302
- assert "_bb_oauth_state" not in res.cookies
+ assert "_oauth_request_token" not in res.cookies
assert res.url == url
+ mocked_get.assert_called_with(settings.BITBUCKET_REDIRECT_URI)
async def fake_get_authenticated_user():
@@ -111,7 +110,7 @@
"generate_access_token",
return_value={
"key": "test6tl3evq7c8vuyn",
- "secret": "testrefreshtoken",
+ "secret": "testdm61tppb5x0tam7nae3qajhcepzz",
},
)
settings.BITBUCKET_REDIRECT_URI = "http://localhost"
@@ -119,14 +118,15 @@
settings.BITBUCKET_CLIENT_SECRET = "testfi8hzehvz453qj8mhv21ca4rf83f"
settings.CODECOV_DASHBOARD_URL = "dashboard.value"
settings.COOKIE_SECRET = "aaaaa"
-
- state = "test_state_value_abc123"
url = reverse("bitbucket-login")
+ oauth_request_token = (
+ "dGVzdDZ0bDNldnE3Yzh2dXlu|dGVzdGRtNjF0cHBiNXgwdGFtN25hZTNxYWpoY2Vweno="
+ )
client.cookies = SimpleCookie(
{
- "_bb_oauth_state": signing.get_cookie_signer(salt="_bb_oauth_state").sign(
- state
- )
+ "_oauth_request_token": signing.get_cookie_signer(
+ salt="_oauth_request_token"
+ ).sign(encryptor.encode(oauth_request_token).decode())
}
)
mock_create_user_onboarding_metric = mocker.patch(
@@ -135,17 +135,17 @@
res = client.get(
url,
- {"code": "auth_code_from_bitbucket", "state": state},
+ {"oauth_verifier": 8519288973, "oauth_token": "test1daxl4jnhegoh4"},
SERVER_NAME="localhost:8000",
)
assert res.status_code == 302
assert res.url == "dashboard.value/bb"
- assert "_bb_oauth_state" in res.cookies
- cookie = res.cookies["_bb_oauth_state"]
+ assert "_oauth_request_token" in res.cookies
+ cookie = res.cookies["_oauth_request_token"]
assert cookie.value == ""
assert cookie.get("domain") == settings.COOKIES_DOMAIN
mocked_get.assert_called_with(
- "auth_code_from_bitbucket", settings.BITBUCKET_REDIRECT_URI
+ "test6tl3evq7c8vuyn", "testdm61tppb5x0tam7nae3qajhcepzz", "8519288973"
)
owner = Owner.objects.get(username="ThiagoCodecov", service="bitbucket")
expected_call = call(
@@ -155,8 +155,13 @@
)
assert mock_create_user_onboarding_metric.call_args_list == [expected_call]
+ assert (
+ encryptor.decode(owner.oauth_token)
+ == "test6tl3evq7c8vuyn:testdm61tppb5x0tam7nae3qajhcepzz"
+ )
-def test_get_bitbucket_already_token_no_state_cookie(
+
+def test_get_bitbucket_already_token_no_cookie(
client, settings, mocker, db, mock_redis
):
mocker.patch(
@@ -170,7 +175,7 @@
"generate_access_token",
return_value={
"key": "test6tl3evq7c8vuyn",
- "secret": "testrefreshtoken",
+ "secret": "testdm61tppb5x0tam7nae3qajhcepzz",
},
)
settings.BITBUCKET_REDIRECT_URI = "http://localhost"
@@ -179,7 +184,7 @@
url = reverse("bitbucket-login")
res = client.get(
url,
- {"code": "auth_code_from_bitbucket", "state": "some_state"},
+ {"oauth_verifier": 8519288973, "oauth_token": "test1daxl4jnhegoh4"},
SERVER_NAME="localhost:8000",
)
assert res.status_code == 302
@@ -187,38 +192,6 @@
assert not mocked_get.called
-def test_get_bitbucket_state_mismatch(client, settings, mocker, db, mock_redis):
- mocked_get = mocker.patch.object(
- Bitbucket,
- "generate_access_token",
- return_value={
- "key": "test6tl3evq7c8vuyn",
- "secret": "testrefreshtoken",
- },
- )
- settings.BITBUCKET_REDIRECT_URI = "http://localhost"
- settings.BITBUCKET_CLIENT_ID = "testqmo19ebdkseoby"
- settings.BITBUCKET_CLIENT_SECRET = "testfi8hzehvz453qj8mhv21ca4rf83f"
- settings.COOKIE_SECRET = "aaaaa"
-
- url = reverse("bitbucket-login")
- client.cookies = SimpleCookie(
- {
- "_bb_oauth_state": signing.get_cookie_signer(salt="_bb_oauth_state").sign(
- "legit_state"
- )
- }
- )
- res = client.get(
- url,
- {"code": "auth_code_from_bitbucket", "state": "attacker_injected_state"},
- SERVER_NAME="localhost:8000",
- )
- assert res.status_code == 302
- assert res.url == "/login/bitbucket"
- assert not mocked_get.called
-
-
class TestBitbucketLoginView(TestCase):
def test_fetch_user_data(self):
async def fake_list_teams():
diff --git a/apps/codecov-api/codecov_auth/views/bitbucket.py b/apps/codecov-api/codecov_auth/views/bitbucket.py
--- a/apps/codecov-api/codecov_auth/views/bitbucket.py
+++ b/apps/codecov-api/codecov_auth/views/bitbucket.py
@@ -1,5 +1,6 @@
+import base64
import logging
-import secrets
+from urllib.parse import urlencode
from asgiref.sync import async_to_sync
from django.conf import settings
@@ -12,10 +13,8 @@
UserOnboardingMetricsService,
)
from shared.torngit import Bitbucket
-from shared.torngit.exceptions import (
- TorngitClientGeneralError,
- TorngitServerFailureError,
-)
+from shared.torngit.exceptions import TorngitServerFailureError
+from utils.encryption import encryptor
log = logging.getLogger(__name__)
@@ -54,19 +53,23 @@
"secret": settings.BITBUCKET_CLIENT_SECRET,
}
)
- state = secrets.token_urlsafe(32)
- url_to_redirect = repo_service.generate_redirect_url(
- settings.BITBUCKET_REDIRECT_URI, state=state
+ oauth_token_pair = repo_service.generate_request_token(
+ settings.BITBUCKET_REDIRECT_URI
)
+ oauth_token = oauth_token_pair["oauth_token"]
+ oauth_token_secret = oauth_token_pair["oauth_token_secret"]
+ url_params = urlencode({"oauth_token": oauth_token})
+ url_to_redirect = f"{Bitbucket._OAUTH_AUTHORIZE_URL}?{url_params}"
response = redirect(url_to_redirect)
+ data = (
+ base64.b64encode(oauth_token.encode())
+ + b"|"
+ + base64.b64encode(oauth_token_secret.encode())
+ ).decode()
response.set_signed_cookie(
- "_bb_oauth_state",
- state,
+ "_oauth_request_token",
+ encryptor.encode(data).decode(),
domain=settings.COOKIES_DOMAIN,
- httponly=True,
- secure=settings.SESSION_COOKIE_SECURE,
- samesite=settings.COOKIE_SAME_SITE,
- max_age=300,
)
self.store_to_cookie_utm_tags(response)
return response
@@ -78,13 +81,19 @@
"secret": settings.BITBUCKET_CLIENT_SECRET,
}
)
- expected_state = request.get_signed_cookie("_bb_oauth_state", default=None)
- if not expected_state or request.GET.get("state") != expected_state:
- log.warning("Bitbucket OAuth state mismatch — possible CSRF attempt")
+ oauth_verifier = request.GET.get("oauth_verifier")
+ request_cookie = request.get_signed_cookie("_oauth_request_token", default=None)
+ if not request_cookie:
+ log.warning(
+ "Request arrived with proper url params but not the proper cookies"
+ )
return redirect(reverse("bitbucket-login"))
- code = request.GET.get("code")
+ request_cookie = encryptor.decode(request_cookie)
+ cookie_key, cookie_secret = [
+ base64.b64decode(i).decode() for i in request_cookie.split("|")
+ ]
token = repo_service.generate_access_token(
- code, settings.BITBUCKET_REDIRECT_URI
+ cookie_key, cookie_secret, oauth_verifier
)
user_dict = self.fetch_user_data(token)
user = self.get_and_modify_owner(user_dict, request)
@@ -93,7 +102,7 @@
redirection_url, user
)
response = redirect(redirection_url)
- response.delete_cookie("_bb_oauth_state", domain=settings.COOKIES_DOMAIN)
+ response.delete_cookie("_oauth_request_token", domain=settings.COOKIES_DOMAIN)
self.login_owner(user, request, response)
log.info("User successfully logged in", extra={"ownerid": user.ownerid})
UserOnboardingMetricsService.create_user_onboarding_metric(
@@ -106,7 +115,7 @@
return redirect(f"{settings.CODECOV_DASHBOARD_URL}/login")
try:
- if request.GET.get("code"):
+ if request.GET.get("oauth_verifier"):
log.info("Logging into bitbucket after authorization")
return self.actual_login_step(request)
else:
@@ -115,6 +124,3 @@
except TorngitServerFailureError:
log.warning("Bitbucket not available for login")
return redirect(reverse("bitbucket-login"))
- except TorngitClientGeneralError:
- log.warning("Bitbucket OAuth error during login")
- return redirect(reverse("bitbucket-login"))
diff --git a/apps/codecov-api/services/repo_providers.py b/apps/codecov-api/services/repo_providers.py
--- a/apps/codecov-api/services/repo_providers.py
+++ b/apps/codecov-api/services/repo_providers.py
@@ -42,7 +42,7 @@
"""
if owner is None:
return None
- if service == Service.BITBUCKET_SERVER:
+ if service == Service.BITBUCKET or service == Service.BITBUCKET_SERVER:
return None
@sync_to_async
diff --git a/apps/codecov-api/services/tests/test_repo_providers.py b/apps/codecov-api/services/tests/test_repo_providers.py
--- a/apps/codecov-api/services/tests/test_repo_providers.py
+++ b/apps/codecov-api/services/tests/test_repo_providers.py
@@ -111,6 +111,7 @@
"should_have_owner,service",
[
(False, Service.GITHUB.value),
+ (True, Service.BITBUCKET.value),
(True, Service.BITBUCKET_SERVER.value),
],
)
@@ -121,13 +122,6 @@
assert get_token_refresh_callback(owner, service) is None
-def test_token_refresh_callback_bitbucket(db):
- owner = OwnerFactory(service=Service.BITBUCKET.value)
- callback = get_token_refresh_callback(owner, Service.BITBUCKET)
- assert callback is not None
- assert inspect.iscoroutinefunction(callback)
-
-
GITHUB_SENTRY_APP_ID = 4321
diff --git a/apps/worker/helpers/token_refresh.py b/apps/worker/helpers/token_refresh.py
--- a/apps/worker/helpers/token_refresh.py
+++ b/apps/worker/helpers/token_refresh.py
@@ -19,7 +19,7 @@
return None
service = owner.service
- if service == "bitbucket_server":
+ if service == "bitbucket" or service == "bitbucket_server":
return None
async def callback(new_token: dict) -> None:
diff --git a/apps/worker/services/tests/test_repository_service.py b/apps/worker/services/tests/test_repository_service.py
--- a/apps/worker/services/tests/test_repository_service.py
+++ b/apps/worker/services/tests/test_repository_service.py
@@ -224,7 +224,7 @@
}
assert res.data == expected_data
assert repo.author.service == "bitbucket"
- assert res._on_token_refresh is not None
+ assert res._on_token_refresh is None
assert res.token == {
"username": repo.author.username,
"key": "testyftq3ovzkb3zmt823u3t04lkrt9w",
diff --git a/libs/shared/shared/torngit/bitbucket.py b/libs/shared/shared/torngit/bitbucket.py
--- a/libs/shared/shared/torngit/bitbucket.py
+++ b/libs/shared/shared/torngit/bitbucket.py
@@ -3,6 +3,7 @@
import urllib.parse as urllib_parse
import httpx
+from oauthlib import oauth1
from shared.torngit.base import TokenType, TorngitBaseAdapter
from shared.torngit.enums import Endpoints
@@ -23,8 +24,9 @@
class Bitbucket(TorngitBaseAdapter):
- _OAUTH_AUTHORIZE_URL = "https://bitbucket.org/site/oauth2/authorize"
- _OAUTH_ACCESS_TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token"
+ _OAUTH_REQUEST_TOKEN_URL = "https://bitbucket.org/api/1.0/oauth/request_token"
+ _OAUTH_ACCESS_TOKEN_URL = "https://bitbucket.org/api/1.0/oauth/access_token"
+ _OAUTH_AUTHORIZE_URL = "https://bitbucket.org/api/1.0/oauth/authenticate"
service = "bitbucket"
api_url = "https://bitbucket.org"
service_url = "https://bitbucket.org"
@@ -43,22 +45,6 @@
"compare": "{username}/{name}",
}
- async def _send_request(self, client, method, url, kwargs, log_dict):
- try:
- res = await client.request(method.upper(), url, **kwargs)
- logged_body = None
- if res.status_code >= 300 and res.text is not None:
- logged_body = res.text
- log.log(
- logging.WARNING if res.status_code >= 300 else logging.INFO,
- "Bitbucket HTTP %s",
- res.status_code,
- extra=dict(body=logged_body, **log_dict),
- )
- return res
- except (httpx.NetworkError, httpx.TimeoutException):
- raise TorngitServerUnreachableError("Bitbucket was not able to be reached.")
-
async def api(
self, client, version, method, path, json=False, body=None, token=None, **kwargs
):
@@ -68,19 +54,30 @@
"User-Agent": os.getenv("USER_AGENT", "Default"),
}
+ oauth_body = None
url = url_concat(url, kwargs)
if json:
headers["Content-Type"] = "application/json"
elif body is not None:
headers["Content-Type"] = "application/x-www-form-urlencoded"
+ oauth_body = body
token_to_use = token or self.token
- headers["Authorization"] = f"Bearer {token_to_use['key']}"
+ oauth_client = oauth1.Client(
+ self._oauth_consumer_token()["key"],
+ client_secret=self._oauth_consumer_token()["secret"],
+ resource_owner_key=token_to_use["key"],
+ resource_owner_secret=token_to_use["secret"],
+ signature_type=oauth1.SIGNATURE_TYPE_QUERY,
+ )
+ url, headers, oauth_body = oauth_client.sign(
+ url, http_method=method, body=oauth_body, headers=headers
+ )
kwargs = {
"json": body if body is not None and json else None,
- "data": body if body is not None and not json else None,
+ "data": oauth_body if not json else None,
"headers": headers,
}
log_dict = {
@@ -90,24 +87,19 @@
"bot": token_to_use.get("username"),
"repo_slug": self.slug,
}
-
- res = await self._send_request(client, method, url, kwargs, log_dict)
-
- if res.status_code == 401 and callable(self._on_token_refresh):
- new_token = None
- try:
- new_token = await self.refresh_token(client)
- except (TorngitClientGeneralError, TorngitServer5xxCodeError):
- log.warning(
- "Bitbucket token refresh failed, raising original 401",
- extra=log_dict,
- )
- if new_token is not None:
- headers["Authorization"] = f"Bearer {new_token['key']}"
- kwargs["headers"] = headers
- await self._on_token_refresh(new_token)
- res = await self._send_request(client, method, url, kwargs, log_dict)
-
+ try:
+ res = await client.request(method.upper(), url, **kwargs)
+ logged_body = None
+ if res.status_code >= 300 and res.text is not None:
+ logged_body = res.text
+ log.log(
+ logging.WARNING if res.status_code >= 300 else logging.INFO,
+ "Bitbucket HTTP %s",
+ res.status_code,
+ extra=dict(body=logged_body, **log_dict),
+ )
+ except (httpx.NetworkError, httpx.TimeoutException):
+ raise TorngitServerUnreachableError("Bitbucket was not able to be reached.")
if res.status_code == 599:
raise TorngitServerUnreachableError(
"Bitbucket was not able to be reached, server timed out."
@@ -117,9 +109,7 @@
elif res.status_code >= 300:
message = f"Bitbucket API: {res.reason_phrase}"
raise TorngitClientGeneralError(
- res.status_code,
- response_data={"content": res.content},
- message=message,
+ res.status_code, response_data={"content": res.content}, message=message
)
if res.status_code == 204:
return None
@@ -128,83 +118,34 @@
else:
return res.text
- async def refresh_token(self, client):
- """
- Exchanges the stored refresh token for a new access + refresh token pair.
- Returns the new token dict, or None if no refresh token is stored.
-
- ! side effect: updates self._token
- """
- current_token = self.token
- if not current_token.get("secret"):
- log.warning("Trying to refresh Bitbucket token with no refresh_token saved")
- return None
-
- res = await client.request(
- "POST",
- self._OAUTH_ACCESS_TOKEN_URL,
- data={
- "grant_type": "refresh_token",
- "refresh_token": current_token["secret"],
- },
- auth=(self._oauth["key"], self._oauth["secret"]),
+ def generate_request_token(self, redirect_url):
+ client = oauth1.Client(
+ self._oauth["key"],
+ client_secret=self._oauth["secret"],
+ callback_uri=redirect_url,
)
- if res.status_code >= 500:
- raise TorngitServer5xxCodeError("Bitbucket is having 5xx issues")
- if res.status_code >= 400:
- raise TorngitClientGeneralError(
- res.status_code,
- response_data={"content": res.content},
- message="Bitbucket token refresh failed",
- )
- data = res.json()
- new_token = {
- "key": data["access_token"],
- "secret": data.get("refresh_token", ""),
- }
- self.set_token(new_token)
- return new_token
+ uri, headers, body = client.sign(self._OAUTH_REQUEST_TOKEN_URL)
+ r = httpx.get(uri, headers=headers)
+ oauth_token = urllib_parse.parse_qs(r.text)["oauth_token"][0]
+ oauth_token_secret = urllib_parse.parse_qs(r.text)["oauth_token_secret"][0]
+ return {"oauth_token": oauth_token, "oauth_token_secret": oauth_token_secret}
- def generate_redirect_url(self, redirect_url, state=None):
- """
- Returns the Bitbucket OAuth 2.0 authorization URL to redirect the user to.
- """
- params: dict = {
- "client_id": self._oauth["key"],
- "response_type": "code",
- "redirect_uri": redirect_url,
- }
- if state is not None:
- params["state"] = state
- return f"{self._OAUTH_AUTHORIZE_URL}?{urllib_parse.urlencode(params)}"
-
- def generate_access_token(self, code, redirect_url):
- """
- Exchanges an OAuth 2.0 authorization code for an access token.
- Returns a dict with 'key' (access token) and 'secret' (refresh token).
- """
- r = httpx.post(
- self._OAUTH_ACCESS_TOKEN_URL,
- data={
- "grant_type": "authorization_code",
- "code": code,
- "redirect_uri": redirect_url,
- },
- auth=(self._oauth["key"], self._oauth["secret"]),
+ def generate_access_token(
+ self, resource_owner_key, resource_owner_secret, verifier
+ ):
+ client = oauth1.Client(
+ self._oauth["key"],
+ client_secret=self._oauth["secret"],
+ resource_owner_key=resource_owner_key,
+ resource_owner_secret=resource_owner_secret,
+ verifier=verifier,
)
- if r.status_code >= 500:
- raise TorngitServer5xxCodeError("Bitbucket is having 5xx issues")
- elif r.status_code >= 400:
- message = f"Bitbucket OAuth2: {r.reason_phrase}"
- raise TorngitClientGeneralError(
- r.status_code,
- response_data={"content": r.content},
- message=message,
- )
- data = r.json()
+ uri, headers, body = client.sign(self._OAUTH_ACCESS_TOKEN_URL)
+ r = httpx.get(uri, headers=headers)
+ resp_args = urllib_parse.parse_qs(r.text)
return {
- "key": data["access_token"],
- "secret": data.get("refresh_token", ""),
+ "key": resp_args["oauth_token"][0],
+ "secret": resp_args["oauth_token_secret"][0],
}
async def get_authenticated_user(self):
diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_comment.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_comment.yaml
--- a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_comment.yaml
+++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_comment.yaml
@@ -7,7 +7,7 @@
User-Agent:
- Default
method: DELETE
- uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/107383471
+ uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/107383471?oauth_version=1.0&oauth_token=testss3hxhcfqf1h6g&oauth_nonce=0441f5ae229a4884801e97004003b6a8&oauth_timestamp=1561670312&oauth_signature=bRwiaWHiDGhoJQQuFG9%2BNwbERAg%3D&oauth_consumer_key=arubajamaicaohiwan&oauth_signature_method=HMAC-SHA1
response:
content: ''
headers:
diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_comment_not_found.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_comment_not_found.yaml
--- a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_comment_not_found.yaml
+++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_comment_not_found.yaml
@@ -7,7 +7,7 @@
User-Agent:
- Default
method: DELETE
- uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/113977999
+ uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/113977999?oauth_version=1.0&oauth_token=testss3hxhcfqf1h6g&oauth_nonce=331c6e3852554f5497b04e564dc673c2&oauth_timestamp=1561668838&oauth_signature=Z9fmZ6u%2FG%2Bimv4BHN1PNy7v3WkE%3D&oauth_consumer_key=arubajamaicaohiwan&oauth_signature_method=HMAC-SHA1
response:
content: '{"type": "error", "error": {"message": "113977999"}}'
headers:
diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_webhook.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_webhook.yaml
--- a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_webhook.yaml
+++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_webhook.yaml
@@ -7,7 +7,7 @@
User-Agent:
- Default
method: DELETE
- uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/hooks/4742f092-8397-4677-8876-5e9a06f10f98
+ uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/hooks/4742f092-8397-4677-8876-5e9a06f10f98?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541602115&oauth_nonce=ab985dba77534c24929b8d013d2cb876&oauth_version=1.0&oauth_signature=BCk5R%2Bbn4a3MUcDiyTQ3HF2X%2BW8%3D
response:
content: ''
headers:
diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_webhook_not_found.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_webhook_not_found.yaml
--- a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_webhook_not_found.yaml
+++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_webhook_not_found.yaml
@@ -7,7 +7,7 @@
User-Agent:
- Default
method: DELETE
- uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/hooks/4742f011-8397-aa77-8876-5e9a06f10f98
+ uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/hooks/4742f011-8397-aa77-8876-5e9a06f10f98?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541602198&oauth_nonce=fc8ef877776d441bab406f7a47396073&oauth_version=1.0&oauth_signature=A%2F46Dp0fgbZI9lkjovmcu4LfGiw%3D
response:
content: '{"type": "error", "error": {"message": "example-python is not a valid
hook"}}'
diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_comment.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_comment.yaml
--- a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_comment.yaml
+++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_comment.yaml
@@ -9,7 +9,7 @@
User-Agent:
- Default
method: PUT
- uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/114320127
+ uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/114320127?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1566631440&oauth_nonce=5aa764bebf044e64beb9d9df2912cc08&oauth_version=1.0&oauth_signature=2mI6UpQrKaN8s0sL9N%2F9ItZaF7o%3D
response:
content: '{"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/114320127"},
"html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/pull-requests/1/_/diff#comment-114320127"}},
diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_comment_not_found.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_comment_not_found.yaml
--- a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_comment_not_found.yaml
+++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_comment_not_found.yaml
@@ -9,7 +9,7 @@
User-Agent:
- Default
method: PUT
- uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/113979999
+ uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/113979999?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1566631150&oauth_nonce=5aeac543acea436d9f0594d750a98dd3&oauth_version=1.0&oauth_signature=16DIGrfTGyeo8k90tViVI%2BcqBPw%3D
response:
content: '{"type": "error", "error": {"message": "113979999"}}'
headers:
diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_webhook.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_webhook.yaml
--- a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_webhook.yaml
+++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_webhook.yaml
@@ -10,7 +10,7 @@
User-Agent:
- Default
method: PUT
- uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/hooks/4742f092-8397-4677-8876-5e9a06f10f98
+ uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/hooks/4742f092-8397-4677-8876-5e9a06f10f98?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541602041&oauth_nonce=ddb0335319f246d98933af48a59371e5&oauth_version=1.0&oauth_signature=widTrBs%2BlgaUlwKjY2YTvYMpWlc%3D
response:
content: '{"read_only": null, "description": "new_name", "links": {"self": {"href":
"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/hooks/%7B4742f092-8397-4677-8876-5e9a06f10f98%7D"}},
diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_find_pull_request_nothing_found.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_find_pull_request_nothing_found.yaml
--- a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_find_pull_request_nothing_found.yaml
+++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_find_pull_request_nothing_found.yaml
@@ -7,7 +7,7 @@
User-Agent:
- Default
method: GET
- uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests?state=OPEN&page=1
+ uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests?state=OPEN&page=1&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541516646&oauth_nonce=15af96bfd6e546acaec32b3dac09fe42&oauth_version=1.0&oauth_signature=4E9qt33md6cz0B%2BnnL62C2hs7jA%3D
response:
content: '{"pagelen": 10, "values": [], "page": 1, "size": 0}'
headers:
diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_ancestors_tree.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_ancestors_tree.yaml
--- a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_ancestors_tree.yaml
+++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_ancestors_tree.yaml
@@ -7,7 +7,7 @@
User-Agent:
- Default
method: GET
- uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/commits?include=6ae5f17
+ uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/commits?include=6ae5f17&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1568980339&oauth_nonce=1dcb7a2eedd544b395184b18231388a8&oauth_version=1.0&oauth_signature=XdUUtgGHNy2XcsbYlW%2Ftq3Od4qo%3D
response:
content: "{\"pagelen\": 30, \"values\": [{\"rendered\": {\"message\": {\"raw\"\
: \"Update README.rst\", \"markup\": \"markdown\", \"html\": \"<p>Update README.rst</p>\"\
diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_authenticated.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_authenticated.yaml
--- a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_authenticated.yaml
+++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_authenticated.yaml
@@ -13,7 +13,7 @@
user-agent:
- Default
method: GET
- uri: https://bitbucket.org/api/2.0/user/permissions/repositories?q=repository.full_name%3D%22ThiagoCodecov%2Fexample-python%22+AND+%28permission%3D%22admin%22+OR+permission%3D%22write%22%29
+ uri: https://bitbucket.org/api/2.0/user/permissions/repositories?q=repository.full_name%3D%22ThiagoCodecov%2Fexample-python%22+AND+%28permission%3D%22admin%22+OR+permission%3D%22write%22%29&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1617377623&oauth_nonce=ac91652521bc4867af3ec36b448a016c&oauth_version=1.0&oauth_signature=BQrCWoBGobnzvY0aRJ51OBJqyPk%3D
response:
content: '{"pagelen": 10, "values": [{"type": "repository_permission", "user":
{"display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}",
diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_authenticated_no_edit_permission.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_authenticated_no_edit_permission.yaml
--- a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_authenticated_no_edit_permission.yaml
+++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_authenticated_no_edit_permission.yaml
@@ -13,7 +13,7 @@
user-agent:
- Default
method: GET
- uri: https://bitbucket.org/api/2.0/user/permissions/repositories?q=repository.full_name%3D%22atlassian%2Fstash-example-plugin%22+AND+%28permission%3D%22admin%22+OR+permission%3D%22write%22%29
+ uri: https://bitbucket.org/api/2.0/user/permissions/repositories?q=repository.full_name%3D%22atlassian%2Fstash-example-plugin%22+AND+%28permission%3D%22admin%22+OR+permission%3D%22write%22%29&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1617377940&oauth_nonce=54b366ce70c843e38c6f5e2d08b7d25b&oauth_version=1.0&oauth_signature=lihQwQ9cT1CJGybvYG1NAYvWuYw%3D
response:
content: '{"pagelen": 10, "values": [], "page": 1}'
headers:
diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_branches.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_branches.yaml
--- a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_branches.yaml
+++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_branches.yaml
@@ -7,7 +7,7 @@
User-Agent:
- Default
method: GET
- uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/refs/branches?pagelen=100
+ uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/refs/branches?oauth_nonce=68196bfc35d5473cba07ec92748644c1&oauth_timestamp=1561674319&oauth_consumer_key=arubajamaicaohiwan&oauth_signature_method=HMAC-SHA1&oauth_version=1.0&oauth_token=testss3hxhcfqf1h6g&oauth_signature=7%2BIvkm9uQV%2FozMt2tOdrE4Hf5dY%3D&pagelen=100
response:
content: '{"pagelen": 100, "values": [{"name": "f/new-branch", "links": {"commits":
{"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commits/f/new-branch"},
diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit.yaml
--- a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit.yaml
+++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit.yaml
@@ -7,7 +7,7 @@
User-Agent:
- Default
method: GET
- uri: https://bitbucket.org/api/2.0/repositories/codecov/private/commit/6a45b83
+ uri: https://bitbucket.org/api/2.0/repositories/codecov/private/commit/6a45b83?oauth_consumer_key=arubajamaicaohiwan&oauth_token=waydowntokokomo&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1575576386&oauth_nonce=7487fd483bfd4daaa38904b8d9feca99&oauth_version=1.0&oauth_signature=QBngQxqDm8r4CVe5UnZK6KUEF60%3D
response:
content: '{"rendered": {"message": {"raw": "wip\n", "markup": "markdown", "html":
"<p>wip</p>", "type": "rendered"}}, "hash": "6a45b838ae4fe22953c93aa17cc41b4b4216eb93",
@@ -94,7 +94,7 @@
User-Agent:
- Default
method: GET
- uri: https://bitbucket.org/api/2.0/users/stevepeak
+ uri: https://bitbucket.org/api/2.0/users/stevepeak?oauth_consumer_key=arubajamaicaohiwan&oauth_token=waydowntokokomo&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1575576388&oauth_nonce=9b743f0720d74655a3b083aa8d756bdf&oauth_version=1.0&oauth_signature=o69UGzzNxIMf6CZ%2B1BdnMIclt5I%3D
response:
content: '{"display_name": "Steve Peak", "uuid": "{test6y9pl15lzivhmkgsk67k10x53n04i85o}",
"links": {"hooks": {"href": "https://bitbucket.org/!api/2.0/users/%7Btest6y9pl15lzivhmkgsk67k10x53n04i85o%7D/hooks"},
diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit_diff.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit_diff.yaml
--- a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit_diff.yaml
+++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit_diff.yaml
@@ -7,7 +7,7 @@
User-Agent:
- Default
method: GET
- uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/diff/3017d53
+ uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/diff/3017d53?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541599673&oauth_nonce=7f08d97c0f8f4826b4e9c79f9969f788&oauth_version=1.0&oauth_signature=NQpb%2B4E%2BtSRDp9XufM9d6ha1QVk%3D
response:
content: 'diff --git a/awesome/code_fib.py b/awesome/code_fib.py
... diff truncated: showing 800 of 1492 linesThis Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
| - More database round-trips per cleanup run — 50,000 rows that were one DELETE are now five. For very large owners this makes cleanup slower. | ||
| - Deletes within a single model are no longer atomic — if the process crashes between chunks, some rows will be deleted and others won't. This was already a property of the broader cleanup design, not a new regression. | ||
|
|
||
| The trade-off is acceptable: cleanup is a background job where slower-but-reliable is preferable to fast-but-crashes. |
There was a problem hiding this comment.
Unrelated documentation included in Bitbucket OAuth revert PR
Low Severity
The file docs/adr-or-notes/owner-deletion-chunked-in-filter.md documents PR #731 (chunked IN filter for owner deletion), which is entirely unrelated to the Bitbucket OAuth 1.0 → 2.0 revert (PR #772). This documentation file appears to have been accidentally included in the revert PR and would be confusing in the commit history when trying to understand the revert.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #773 +/- ##
=======================================
Coverage 92.27% 92.27%
=======================================
Files 1305 1305
Lines 47938 47938
Branches 1628 1628
=======================================
Hits 44233 44233
Misses 3396 3396
Partials 309 309
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |



Summary
Emergency revert for PR #772 (Bitbucket OAuth 1.0 → 2.0 migration).
bitbucket.pyto OAuth 1.0 signingWhen to use
Only merge this if PR #772 needs to be rolled back in production. This PR will show no diff until #772 is merged to main.
Do not merge unless rolling back #772.
Note
Low Risk
Adds documentation and a new Bitbucket integration test cassette; no production code paths are modified, so behavioral risk is minimal aside from potential test fixture mismatch.
Overview
Adds a new ADR-style note (
docs/adr-or-notes/owner-deletion-chunked-in-filter.md) documenting a prior fix to chunk large materializedIN (...)deletes during owner cleanup.Adds a Bitbucket VCR cassette for
test_list_repos_generator, capturing aGET /api/2.0/repositories/codecov?page=1response to stabilize the integration test fixture.Written by Cursor Bugbot for commit 711a3a9. This will update automatically on new commits. Configure here.