diff options
| author | Egor Tensin <egor@tensin.name> | 2026-06-10 11:16:38 +0200 |
|---|---|---|
| committer | Egor Tensin <egor@tensin.name> | 2026-06-10 11:16:38 +0200 |
| commit | 2504aa3f62a993fd4a6ac70e9fa5ce72639f0c4c (patch) | |
| tree | f3b7f24da238165339467cd1dc3590e42fa21b71 | |
| parent | test/unit: more complete Bitbucket tests (diff) | |
| download | cgitize-2504aa3f62a993fd4a6ac70e9fa5ce72639f0c4c.tar.gz cgitize-2504aa3f62a993fd4a6ac70e9fa5ce72639f0c4c.zip | |
bitbucket: switch from app passwords (deprecated) to API tokens
| -rw-r--r-- | .github/workflows/ci.yml | 12 | ||||
| -rw-r--r-- | cgitize/bitbucket.py | 4 | ||||
| -rw-r--r-- | cgitize/config.py | 13 | ||||
| -rw-r--r-- | cgitize/repo.py | 32 | ||||
| -rw-r--r-- | cgitize/utils.py | 14 | ||||
| -rw-r--r-- | docs/api.md | 10 | ||||
| -rw-r--r-- | docs/auth.md | 8 |
7 files changed, 55 insertions, 38 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 947d15d..6c7afd1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,11 +27,11 @@ jobs: python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] name: 'Test / ${{ matrix.python-version }}' env: - CGITIZE_GITHUB_USERNAME: cgitize-test + CGITIZE_GITHUB_USERNAME: '${{ secrets.CGITIZE_GITHUB_USERNAME }}' CGITIZE_GITHUB_TOKEN: '${{ secrets.CGITIZE_GITHUB_TOKEN }}' - CGITIZE_BITBUCKET_USERNAME: cgitize-test + CGITIZE_BITBUCKET_USERNAME: '${{ secrets.CGITIZE_BITBUCKET_USERNAME }}' CGITIZE_BITBUCKET_TOKEN: '${{ secrets.CGITIZE_BITBUCKET_TOKEN }}' - CGITIZE_GITLAB_USERNAME: cgitize-test + CGITIZE_GITLAB_USERNAME: '${{ secrets.CGITIZE_GITLAB_USERNAME }}' CGITIZE_GITLAB_TOKEN: '${{ secrets.CGITIZE_GITLAB_TOKEN }}' steps: - name: Checkout @@ -64,11 +64,11 @@ jobs: runs-on: ubuntu-latest name: 'Test / example config' env: - CGITIZE_GITHUB_USERNAME: cgitize-test + CGITIZE_GITHUB_USERNAME: '${{ secrets.CGITIZE_GITHUB_USERNAME }}' CGITIZE_GITHUB_TOKEN: '${{ secrets.CGITIZE_GITHUB_TOKEN }}' - CGITIZE_BITBUCKET_USERNAME: cgitize-test + CGITIZE_BITBUCKET_USERNAME: '${{ secrets.CGITIZE_BITBUCKET_USERNAME }}' CGITIZE_BITBUCKET_TOKEN: '${{ secrets.CGITIZE_BITBUCKET_TOKEN }}' - CGITIZE_GITLAB_USERNAME: cgitize-test + CGITIZE_GITLAB_USERNAME: '${{ secrets.CGITIZE_GITLAB_USERNAME }}' CGITIZE_GITLAB_TOKEN: '${{ secrets.CGITIZE_GITLAB_TOKEN }}' steps: - name: Checkout diff --git a/cgitize/bitbucket.py b/cgitize/bitbucket.py index 851c4b2..975e255 100644 --- a/cgitize/bitbucket.py +++ b/cgitize/bitbucket.py @@ -12,8 +12,8 @@ from cgitize.repo import Repo, Visibility class Bitbucket: - def __init__(self, username=None, password=None): - self._impl = Cloud(username=username, password=password, cloud=True) + def __init__(self, email=None, token=None): + self._impl = Cloud(username=email, password=token, cloud=True) def get_repo(self, repo): try: diff --git a/cgitize/config.py b/cgitize/config.py index 7afe9e5..6d304ac 100644 --- a/cgitize/config.py +++ b/cgitize/config.py @@ -5,6 +5,7 @@ from abc import ABC, abstractmethod import os +from urllib.parse import quote import tomli @@ -110,7 +111,7 @@ class GitHubSection(ServiceSection): @property def url_auth(self): - return self.token + return (self.token, None) def _enum_org_repositories(self, cfg, api, org): for repo in api.get_org_repos(org): @@ -135,12 +136,6 @@ class GitHubSection(ServiceSection): return GitHub(username, token) -def two_part_url_auth(username, password): - if username is None or password is None: - return None - return f'{username}:{password}' - - class BitbucketSection(ServiceSection): @property def token(self): @@ -152,7 +147,7 @@ class BitbucketSection(ServiceSection): @property def url_auth(self): - return two_part_url_auth(self.username, self.token) + return (self.username, self.token) def connect_to_service(self): username = self.username @@ -175,7 +170,7 @@ class GitLabSection(ServiceSection): @property def url_auth(self): - return two_part_url_auth(self.username, self.token) + return (self.username, self.token) def connect_to_service(self): username = self.username diff --git a/cgitize/repo.py b/cgitize/repo.py index b3e5ab3..5982247 100644 --- a/cgitize/repo.py +++ b/cgitize/repo.py @@ -87,8 +87,12 @@ class Repo: https_url = src.clone_url ssh_url = src.ssh_url - clone_url = ssh_url if config.main.clone_via_ssh else https_url - url_auth = None if config.main.clone_via_ssh else config.github.url_auth + + clone_url = ssh_url + url_auth = None + if not config.main.clone_via_ssh: + clone_url = https_url + url_auth = config.github.url_auth return Repo( name, @@ -113,7 +117,7 @@ class Repo: if len(https_urls) != 1: raise RuntimeError(f"no https:// clone URL for repository '{name}'?!") # Bitbucket leaves the username in the URL... Sigh. - https_url = url_remove_auth(https_urls[0]['href']) + api_auth, https_url = url_remove_auth(https_urls[0]['href']) ssh_urls = [ link for link in src.data['links']['clone'] if link['name'] == 'ssh' @@ -122,8 +126,18 @@ class Repo: raise RuntimeError(f"no ssh:// clone URL for repository '{name}'?!") ssh_url = ssh_urls[0]['href'] - clone_url = ssh_url if config.main.clone_via_ssh else https_url - url_auth = None if config.main.clone_via_ssh else config.bitbucket.url_auth + clone_url = ssh_url + url_auth = None + if not config.main.clone_via_ssh: + clone_url = https_url + url_auth = config.bitbucket.url_auth + # Bitbucket, since deprecating app passwords, now requires that + # users authenticate with their Atlassian email & API token. + # Unfortunately (sigh...), if you want to clone a repo via HTTPS, + # you can't use the very same email + token combo in the URL, you + # have to use your username (double sigh...). Luckily, for some + # reason, Bitbucket leaves the relevant username in the API response. + url_auth = api_auth[0], url_auth[1] return Repo( name, @@ -144,8 +158,12 @@ class Repo: https_url = src.http_url_to_repo ssh_url = src.ssh_url_to_repo - clone_url = ssh_url if config.main.clone_via_ssh else https_url - url_auth = None if config.main.clone_via_ssh else config.gitlab.url_auth + + clone_url = ssh_url + url_auth = None + if not config.main.clone_via_ssh: + clone_url = https_url + url_auth = config.gitlab.url_auth return Repo( name, diff --git a/cgitize/utils.py b/cgitize/utils.py index dd27f14..37a8fcf 100644 --- a/cgitize/utils.py +++ b/cgitize/utils.py @@ -9,7 +9,7 @@ import os import stat import subprocess import sys -from urllib.parse import urlsplit, urlunsplit +from urllib.parse import quote, urlsplit, urlunsplit @contextmanager @@ -97,11 +97,12 @@ def protected_file(path): os.unlink(path) -def url_replace_auth(url, username, password=None): +def url_replace_auth(url, auth): + username, password = auth parts = urlsplit(url) - netloc = username + netloc = quote(username) if password is not None: - netloc += f':{password}' + netloc += f':{quote(password)}' netloc += f'@{parts.hostname}' if parts.port is not None: netloc += f':{parts.port}' @@ -111,6 +112,9 @@ def url_replace_auth(url, username, password=None): def url_remove_auth(url): parts = urlsplit(url) + auth = parts.username, parts.password netloc = parts.hostname + if parts.port is not None: + netloc += f':{parts.port}' parts = parts._replace(netloc=netloc) - return urlunsplit(parts) + return auth, urlunsplit(parts) diff --git a/docs/api.md b/docs/api.md index ebf4520..f2ff319 100644 --- a/docs/api.md +++ b/docs/api.md @@ -4,10 +4,9 @@ unfortunately, peculiar in their own way. Tokens ------ -All of the forges support access tokens, of course (although they're called -"app passwords" on Bitbucket). They can be used to do `git clone https://` -with authentication info in the URL. For GitHub, it's enough to just specify -the token: +All of the forges support access tokens, of course. They can be used to do +`git clone https://` with authentication info in the URL. For GitHub, it's +enough to just specify the token: git clone https://TOKEN@github.com/USER/REPO.git @@ -16,6 +15,9 @@ token in the URL: git clone https://USER:TOKEN@bitbucket.org/USER/REPO.git +(For Bitbucket, since they deprecated their "app passwords", you need to +specify not the username, but rather the Atlassian account email.) + Terminology ----------- diff --git a/docs/auth.md b/docs/auth.md index 4eabcd5..e6ecccc 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -22,11 +22,9 @@ When using HTTPS, there're multiple options to access the private repositories. 2. Use OAuth. I thought this was too complicated to implement quickly. -3. Use per-application passwords. This option is pretty cool. Both GitHub (in - the form of "personal access tokens") and Bitbucket (calls them "app - passwords") support creating one-purpose passwords that are supposed to be - used by a single app only. They allow to bypass the 2FA also. Let's do - this! +3. Use application access tokens. This option is pretty cool. Both GitHub + and Bitbucket support creating read-only tokens. They allow to bypass the + 2FA also. Let's do this! Passing the access tokens to Git is tricky though. First, simply including them in the clone URL would be incredibly stupid, since that URL is going to be |
