mirror of
https://github.com/josegonzalez/python-github-backup.git
synced 2026-04-28 03:25:36 +02:00
Add --token-from-gh authentication option
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Unreleased
|
||||
----------
|
||||
- Add ``--token-from-gh`` to read authentication from ``gh auth token``.
|
||||
|
||||
|
||||
0.61.5 (2026-02-18)
|
||||
-------------------
|
||||
------------------------
|
||||
|
||||
@@ -36,8 +36,8 @@ Show the CLI help output::
|
||||
|
||||
CLI Help output::
|
||||
|
||||
github-backup [-h] [-t TOKEN_CLASSIC] [-f TOKEN_FINE] [-q] [--as-app]
|
||||
[-o OUTPUT_DIRECTORY] [-l LOG_LEVEL] [-i]
|
||||
github-backup [-h] [-t TOKEN_CLASSIC] [-f TOKEN_FINE] [--token-from-gh]
|
||||
[-q] [--as-app] [-o OUTPUT_DIRECTORY] [-l LOG_LEVEL] [-i]
|
||||
[--incremental-by-files]
|
||||
[--starred] [--all-starred] [--starred-skip-size-over MB]
|
||||
[--watched] [--followers] [--following] [--all]
|
||||
@@ -71,6 +71,7 @@ CLI Help output::
|
||||
-f, --token-fine TOKEN_FINE
|
||||
fine-grained personal access token (github_pat_....),
|
||||
or path to token (file://...)
|
||||
--token-from-gh read token from GitHub CLI (gh auth token)
|
||||
-q, --quiet supress log messages less severe than warning, e.g.
|
||||
info
|
||||
--as-app authenticate as github app instead of as a user.
|
||||
@@ -171,6 +172,8 @@ The positional argument ``USER`` specifies the user or organization account you
|
||||
|
||||
**Classic tokens** (``-t TOKEN``) are `slightly less secure <https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#personal-access-tokens-classic>`_ as they provide very coarse-grained permissions.
|
||||
|
||||
If you already authenticate with the `GitHub CLI <https://cli.github.com/>`_, you can use ``--token-from-gh`` to read the token with ``gh auth token`` instead of passing a token directly. This avoids placing the token in shell history or process arguments. When ``--github-host`` is set, the token is read with ``gh auth token --hostname HOST``.
|
||||
|
||||
|
||||
Fine Tokens
|
||||
~~~~~~~~~~~
|
||||
|
||||
@@ -167,6 +167,12 @@ def parse_args(args=None):
|
||||
dest="token_fine",
|
||||
help="fine-grained personal access token (github_pat_....), or path to token (file://...)",
|
||||
) # noqa
|
||||
parser.add_argument(
|
||||
"--token-from-gh",
|
||||
action="store_true",
|
||||
dest="token_from_gh",
|
||||
help="read token from GitHub CLI (gh auth token)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-q",
|
||||
"--quiet",
|
||||
@@ -537,8 +543,14 @@ def get_auth(args, encode=True, for_git_cli=False):
|
||||
raise Exception(
|
||||
"Fine-grained token supplied does not look like a GitHub PAT"
|
||||
)
|
||||
elif args.token_classic:
|
||||
if args.token_classic.startswith(FILE_URI_PREFIX):
|
||||
elif args.token_classic or args.token_from_gh:
|
||||
if args.token_from_gh:
|
||||
if args.as_app:
|
||||
raise Exception(
|
||||
"--token-from-gh cannot be used with --as-app; provide the app token with --token instead"
|
||||
)
|
||||
args.token_classic = read_token_from_gh_cli(args)
|
||||
elif args.token_classic.startswith(FILE_URI_PREFIX):
|
||||
args.token_classic = read_file_contents(args.token_classic)
|
||||
|
||||
if not args.as_app:
|
||||
@@ -580,6 +592,38 @@ def read_file_contents(file_uri):
|
||||
return open(file_uri[len(FILE_URI_PREFIX) :], "rt").readline().strip()
|
||||
|
||||
|
||||
def read_token_from_gh_cli(args):
|
||||
cached_token = getattr(args, "_token_from_gh_value", None)
|
||||
if cached_token:
|
||||
return cached_token
|
||||
|
||||
command = ["gh", "auth", "token"]
|
||||
if args.github_host:
|
||||
command.extend(["--hostname", get_github_host(args)])
|
||||
|
||||
try:
|
||||
token = subprocess.check_output(command, stderr=subprocess.PIPE).decode(
|
||||
"utf-8"
|
||||
).strip()
|
||||
except FileNotFoundError:
|
||||
raise Exception(
|
||||
"Unable to read token from GitHub CLI: 'gh' executable not found"
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
stderr = e.stderr.decode("utf-8", errors="replace").strip()
|
||||
if stderr:
|
||||
raise Exception(
|
||||
"Unable to read token from GitHub CLI: {0}".format(stderr)
|
||||
)
|
||||
raise Exception("Unable to read token from GitHub CLI")
|
||||
|
||||
if not token:
|
||||
raise Exception("Unable to read token from GitHub CLI: token was empty")
|
||||
|
||||
args._token_from_gh_value = token
|
||||
return token
|
||||
|
||||
|
||||
def get_github_repo_url(args, repository):
|
||||
if repository.get("is_gist"):
|
||||
if args.prefer_ssh:
|
||||
|
||||
65
tests/test_auth.py
Normal file
65
tests/test_auth.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""Tests for authentication helpers."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from github_backup import github_backup
|
||||
|
||||
|
||||
def test_token_from_gh_flag_parses():
|
||||
args = github_backup.parse_args(["--token-from-gh", "testuser"])
|
||||
assert args.token_from_gh is True
|
||||
|
||||
|
||||
def test_get_auth_reads_token_from_gh_cli(create_args):
|
||||
args = create_args(token_from_gh=True)
|
||||
|
||||
with patch(
|
||||
"github_backup.github_backup.subprocess.check_output",
|
||||
return_value=b"gho_test_token\n",
|
||||
) as mock_check_output:
|
||||
auth = github_backup.get_auth(args, encode=False)
|
||||
|
||||
assert auth == "gho_test_token:x-oauth-basic"
|
||||
mock_check_output.assert_called_once_with(
|
||||
["gh", "auth", "token"], stderr=github_backup.subprocess.PIPE
|
||||
)
|
||||
|
||||
|
||||
def test_get_auth_reads_token_from_gh_cli_for_enterprise_host(create_args):
|
||||
args = create_args(token_from_gh=True, github_host="ghe.example.com")
|
||||
|
||||
with patch(
|
||||
"github_backup.github_backup.subprocess.check_output",
|
||||
return_value=b"gho_enterprise_token\n",
|
||||
) as mock_check_output:
|
||||
auth = github_backup.get_auth(args, encode=False)
|
||||
|
||||
assert auth == "gho_enterprise_token:x-oauth-basic"
|
||||
mock_check_output.assert_called_once_with(
|
||||
["gh", "auth", "token", "--hostname", "ghe.example.com"],
|
||||
stderr=github_backup.subprocess.PIPE,
|
||||
)
|
||||
|
||||
|
||||
def test_token_from_gh_is_cached(create_args):
|
||||
args = create_args(token_from_gh=True)
|
||||
|
||||
with patch(
|
||||
"github_backup.github_backup.subprocess.check_output",
|
||||
return_value=b"gho_cached_token\n",
|
||||
) as mock_check_output:
|
||||
assert github_backup.get_auth(args, encode=False) == "gho_cached_token:x-oauth-basic"
|
||||
assert github_backup.get_auth(args, encode=False) == "gho_cached_token:x-oauth-basic"
|
||||
|
||||
mock_check_output.assert_called_once()
|
||||
|
||||
|
||||
def test_token_from_gh_rejects_as_app(create_args):
|
||||
args = create_args(token_from_gh=True, as_app=True)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
github_backup.get_auth(args, encode=False)
|
||||
|
||||
assert "--token-from-gh cannot be used with --as-app" in str(exc_info.value)
|
||||
Reference in New Issue
Block a user