Merge pull request #216 from Ondkloss/feature/fine_grained

Add support for fine-grained tokens (continued)
This commit is contained in:
Jose Diaz-Gonzalez
2023-10-07 00:04:17 -04:00
committed by GitHub
2 changed files with 71 additions and 29 deletions

View File

@@ -29,16 +29,17 @@ Usage
CLI Usage is as follows:: CLI Usage is as follows::
github-backup [-h] [-u USERNAME] [-p PASSWORD] [-t TOKEN] [--as-app] github-backup [-h] [-u USERNAME] [-p PASSWORD] [-t TOKEN_CLASSIC]
[-o OUTPUT_DIRECTORY] [-l LOG_LEVEL] [-i] [--starred] [-f TOKEN_FINE] [--as-app] [-o OUTPUT_DIRECTORY]
[--all-starred] [--watched] [--followers] [--following] [-l LOG_LEVEL] [-i] [--starred] [--all-starred]
[--all] [--issues] [--issue-comments] [--issue-events] [--watched] [--followers] [--following] [--all] [--issues]
[--pulls] [--pull-comments] [--pull-commits] [--issue-comments] [--issue-events] [--pulls]
[--pull-details] [--labels] [--hooks] [--milestones] [--pull-comments] [--pull-commits] [--pull-details]
[--repositories] [--bare] [--lfs] [--wikis] [--gists] [--labels] [--hooks] [--milestones] [--repositories]
[--starred-gists] [--skip-archived] [--skip-existing] [--bare] [--lfs] [--wikis] [--gists] [--starred-gists]
[-L [LANGUAGES ...]] [-N NAME_REGEX] [-H GITHUB_HOST] [--skip-archived] [--skip-existing] [-L [LANGUAGES ...]]
[-O] [-R REPOSITORY] [-P] [-F] [--prefer-ssh] [-v] [-N NAME_REGEX] [-H GITHUB_HOST] [-O] [-R REPOSITORY]
[-P] [-F] [--prefer-ssh] [-v]
[--keychain-name OSX_KEYCHAIN_ITEM_NAME] [--keychain-name OSX_KEYCHAIN_ITEM_NAME]
[--keychain-account OSX_KEYCHAIN_ITEM_ACCOUNT] [--keychain-account OSX_KEYCHAIN_ITEM_ACCOUNT]
[--releases] [--assets] [--exclude [REPOSITORY [REPOSITORY ...]] [--releases] [--assets] [--exclude [REPOSITORY [REPOSITORY ...]]
@@ -57,7 +58,10 @@ CLI Usage is as follows::
-p PASSWORD, --password PASSWORD -p PASSWORD, --password PASSWORD
password for basic auth. If a username is given but password for basic auth. If a username is given but
not a password, the password will be prompted for. not a password, the password will be prompted for.
-t TOKEN, --token TOKEN -f TOKEN_FINE, --token-fine TOKEN_FINE
fine-grained personal access token or path to token
(file://...)
-t TOKEN_CLASSIC, --token TOKEN_CLASSIC
personal access, OAuth, or JSON Web token, or path to personal access, OAuth, or JSON Web token, or path to
token (file://...) token (file://...)
--as-app authenticate as github app instead of as a user. --as-app authenticate as github app instead of as a user.
@@ -163,13 +167,13 @@ Backup all repositories, including private ones::
export ACCESS_TOKEN=SOME-GITHUB-TOKEN export ACCESS_TOKEN=SOME-GITHUB-TOKEN
github-backup WhiteHouse --token $ACCESS_TOKEN --organization --output-directory /tmp/white-house --repositories --private github-backup WhiteHouse --token $ACCESS_TOKEN --organization --output-directory /tmp/white-house --repositories --private
Backup a single organization repository with everything else (wiki, pull requests, comments, issues etc):: Use a fine-grained access token to backup a single organization repository with everything else (wiki, pull requests, comments, issues etc)::
export ACCESS_TOKEN=SOME-GITHUB-TOKEN export ACCESS_TOKEN=SOME-GITHUB-TOKEN
ORGANIZATION=docker ORGANIZATION=docker
REPO=cli REPO=cli
# e.g. git@github.com:docker/cli.git # e.g. git@github.com:docker/cli.git
github-backup $ORGANIZATION -P -t $ACCESS_TOKEN -o . --all -O -R $REPO github-backup $ORGANIZATION -P -f $ACCESS_TOKEN -o . --all -O -R $REPO
Testing Testing
======= =======

View File

@@ -36,6 +36,7 @@ except ImportError:
VERSION = "unknown" VERSION = "unknown"
FNULL = open(os.devnull, "w") FNULL = open(os.devnull, "w")
FILE_URI_PREFIX = "file://"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -126,9 +127,15 @@ def parse_args(args=None):
parser.add_argument( parser.add_argument(
"-t", "-t",
"--token", "--token",
dest="token", dest="token_classic",
help="personal access, OAuth, or JSON Web token, or path to token (file://...)", help="personal access, OAuth, or JSON Web token, or path to token (file://...)",
) # noqa ) # noqa
parser.add_argument(
"-f",
"--token-fine",
dest="token_fine",
help="fine-grained personal access token (github_pat_....), or path to token (file://...)",
) # noqa
parser.add_argument( parser.add_argument(
"--as-app", "--as-app",
action="store_true", action="store_true",
@@ -431,18 +438,27 @@ def get_auth(args, encode=True, for_git_cli=False):
raise Exception( raise Exception(
"You must specify both name and account fields for osx keychain password items" "You must specify both name and account fields for osx keychain password items"
) )
elif args.token: elif args.token_fine:
_path_specifier = "file://" if args.token_fine.startswith(FILE_URI_PREFIX):
if args.token.startswith(_path_specifier): args.token_fine = read_file_contents(args.token_fine)
path_specifier_len = len(_path_specifier)
args.token = open(args.token[path_specifier_len:], "rt").readline().strip() if args.token_fine.startswith("github_pat_"):
auth = args.token_fine
else:
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):
args.token_classic = read_file_contents(args.token_classic)
if not args.as_app: if not args.as_app:
auth = args.token + ":" + "x-oauth-basic" auth = args.token_classic + ":" + "x-oauth-basic"
else: else:
if not for_git_cli: if not for_git_cli:
auth = args.token auth = args.token_classic
else: else:
auth = "x-access-token:" + args.token auth = "x-access-token:" + args.token_classic
elif args.username: elif args.username:
if not args.password: if not args.password:
args.password = getpass.getpass() args.password = getpass.getpass()
@@ -457,7 +473,7 @@ def get_auth(args, encode=True, for_git_cli=False):
if not auth: if not auth:
return None return None
if not encode: if not encode or args.token_fine is not None:
return auth return auth
return base64.b64encode(auth.encode("ascii")) return base64.b64encode(auth.encode("ascii"))
@@ -481,6 +497,10 @@ def get_github_host(args):
return host return host
def read_file_contents(file_uri):
return open(file_uri[len(FILE_URI_PREFIX) :], "rt").readline().strip()
def get_github_repo_url(args, repository): def get_github_repo_url(args, repository):
if repository.get("is_gist"): if repository.get("is_gist"):
if args.prefer_ssh: if args.prefer_ssh:
@@ -503,7 +523,7 @@ def get_github_repo_url(args, repository):
auth = get_auth(args, encode=False, for_git_cli=True) auth = get_auth(args, encode=False, for_git_cli=True)
if auth: if auth:
repo_url = "https://{0}@{1}/{2}/{3}.git".format( repo_url = "https://{0}@{1}/{2}/{3}.git".format(
auth, auth if args.token_fine is None else "oauth2:" + auth,
get_github_host(args), get_github_host(args),
repository["owner"]["login"], repository["owner"]["login"],
repository["name"], repository["name"],
@@ -523,7 +543,13 @@ def retrieve_data_gen(args, template, query_args=None, single_request=False):
while True: while True:
page = page + 1 page = page + 1
request = _construct_request( request = _construct_request(
per_page, page, query_args, template, auth, as_app=args.as_app per_page,
page,
query_args,
template,
auth,
as_app=args.as_app,
fine=True if args.token_fine is not None else False,
) # noqa ) # noqa
r, errors = _get_response(request, auth, template) r, errors = _get_response(request, auth, template)
@@ -559,7 +585,13 @@ def retrieve_data_gen(args, template, query_args=None, single_request=False):
retries += 1 retries += 1
time.sleep(5) time.sleep(5)
request = _construct_request( request = _construct_request(
per_page, page, query_args, template, auth, as_app=args.as_app per_page,
page,
query_args,
template,
auth,
as_app=args.as_app,
fine=True if args.token_fine is not None else False,
) # noqa ) # noqa
r, errors = _get_response(request, auth, template) r, errors = _get_response(request, auth, template)
@@ -643,17 +675,23 @@ def _get_response(request, auth, template):
return r, errors return r, errors
def _construct_request(per_page, page, query_args, template, auth, as_app=None): def _construct_request(
per_page, page, query_args, template, auth, as_app=None, fine=False
):
querystring = urlencode( querystring = urlencode(
dict( dict(
list({"per_page": per_page, "page": page}.items()) + list(query_args.items()) list({"per_page": per_page, "page": page}.items())
+ list(query_args.items())
) )
) )
request = Request(template + "?" + querystring) request = Request(template + "?" + querystring)
if auth is not None: if auth is not None:
if not as_app: if not as_app:
request.add_header("Authorization", "Basic ".encode("ascii") + auth) if fine:
request.add_header("Authorization", "token " + auth)
else:
request.add_header("Authorization", "Basic ".encode("ascii") + auth)
else: else:
auth = auth.encode("ascii") auth = auth.encode("ascii")
request.add_header("Authorization", "token ".encode("ascii") + auth) request.add_header("Authorization", "token ".encode("ascii") + auth)