diff --git a/docs/recipes/git-clone-ssh.rst b/docs/recipes/git-clone-ssh.rst index 0a57eafc..d161cf05 100644 --- a/docs/recipes/git-clone-ssh.rst +++ b/docs/recipes/git-clone-ssh.rst @@ -13,9 +13,9 @@ Example for cloning a git repository over ssh. class MyRemoteCallbacks(pygit2.RemoteCallbacks): def credentials(self, url, username_from_url, allowed_types): - if allowed_types & pygit2.credentials.GIT_CREDENTIAL_USERNAME: + if allowed_types & pygit2.CredentialType.USERNAME: return pygit2.Username("git") - elif allowed_types & pygit2.credentials.GIT_CREDENTIAL_SSH_KEY: + elif allowed_types & pygit2.CredentialType.SSH_KEY: return pygit2.Keypair("git", "id_rsa.pub", "id_rsa", "") else: return None diff --git a/pygit2/__init__.py b/pygit2/__init__.py index 66123064..b980a5b5 100644 --- a/pygit2/__init__.py +++ b/pygit2/__init__.py @@ -48,6 +48,7 @@ CheckoutNotifyFlags, CheckoutStrategy, ConfigLevel, + CredentialType, DescribeStrategy, DeltaFlags, DeltaStatus, @@ -162,6 +163,15 @@ GIT_STASH_APPLY_PROGRESS_CHECKOUT_MODIFIED = StashApplyProgress.CHECKOUT_MODIFIED GIT_STASH_APPLY_PROGRESS_DONE = StashApplyProgress.DONE +# GIT_CREDENTIAL_* values for legacy code +GIT_CREDENTIAL_USERPASS_PLAINTEXT = CredentialType.USERPASS_PLAINTEXT +GIT_CREDENTIAL_SSH_KEY = CredentialType.SSH_KEY +GIT_CREDENTIAL_SSH_CUSTOM = CredentialType.SSH_CUSTOM +GIT_CREDENTIAL_DEFAULT = CredentialType.DEFAULT +GIT_CREDENTIAL_SSH_INTERACTIVE = CredentialType.SSH_INTERACTIVE +GIT_CREDENTIAL_USERNAME = CredentialType.USERNAME +GIT_CREDENTIAL_SSH_MEMORY = CredentialType.SSH_MEMORY + # libgit version tuple LIBGIT2_VER = (LIBGIT2_VER_MAJOR, LIBGIT2_VER_MINOR, LIBGIT2_VER_REVISION) diff --git a/pygit2/callbacks.py b/pygit2/callbacks.py index 885944ec..66cdfa9f 100644 --- a/pygit2/callbacks.py +++ b/pygit2/callbacks.py @@ -65,11 +65,11 @@ # Standard Library from contextlib import contextmanager from functools import wraps -from typing import Optional +from typing import Optional, Union # pygit2 from ._pygit2 import Oid, DiffFile -from .enums import CheckoutNotifyFlags, CheckoutStrategy, StashApplyProgress +from .enums import CheckoutNotifyFlags, CheckoutStrategy, CredentialType, StashApplyProgress from .errors import check_error, Passthrough from .ffi import ffi, C from .utils import maybe_string, to_bytes, ptr_to_bytes, StrArray @@ -131,7 +131,7 @@ def sideband_progress(self, string): Progress output from the remote. """ - def credentials(self, url, username_from_url, allowed_types): + def credentials(self, url: str, username_from_url: Union[str, None], allowed_types: CredentialType): """ Credentials callback. If the remote server requires authentication, this function will be called and its return value used for @@ -148,8 +148,9 @@ def credentials(self, url, username_from_url, allowed_types): username_from_url : str or None Username extracted from the url, if any. - allowed_types : int - Credential types supported by the remote. + allowed_types : CredentialType + A combination of CredentialType bitflags representing the + credential types supported by the remote. """ raise Passthrough @@ -477,6 +478,9 @@ def _credentials_cb(cred_out, url, username, allowed, data): if not credentials: return 0 + # convert int flags to enum before forwarding to user code + allowed = CredentialType(allowed) + ccred = get_credentials(credentials, url, username, allowed) cred_out[0] = ccred[0] return 0 @@ -575,12 +579,12 @@ def get_credentials(fn, url, username, allowed): raise TypeError("invalid credential type") ccred = ffi.new('git_credential **') - if cred_type == C.GIT_CREDENTIAL_USERPASS_PLAINTEXT: + if cred_type == CredentialType.USERPASS_PLAINTEXT: name, passwd = credential_tuple err = C.git_credential_userpass_plaintext_new(ccred, to_bytes(name), to_bytes(passwd)) - elif cred_type == C.GIT_CREDENTIAL_SSH_KEY: + elif cred_type == CredentialType.SSH_KEY: name, pubkey, privkey, passphrase = credential_tuple name = to_bytes(name) if pubkey is None and privkey is None: @@ -590,11 +594,11 @@ def get_credentials(fn, url, username, allowed): to_bytes(privkey), to_bytes(passphrase)) - elif cred_type == C.GIT_CREDENTIAL_USERNAME: + elif cred_type == CredentialType.USERNAME: name, = credential_tuple err = C.git_credential_username_new(ccred, to_bytes(name)) - elif cred_type == C.GIT_CREDENTIAL_SSH_MEMORY: + elif cred_type == CredentialType.SSH_MEMORY: name, pubkey, privkey, passphrase = credential_tuple if pubkey is None and privkey is None: raise TypeError("SSH keys from memory are empty") diff --git a/pygit2/credentials.py b/pygit2/credentials.py index d5b81d51..9b25737c 100644 --- a/pygit2/credentials.py +++ b/pygit2/credentials.py @@ -25,14 +25,7 @@ from .ffi import C - -GIT_CREDENTIAL_USERPASS_PLAINTEXT = C.GIT_CREDENTIAL_USERPASS_PLAINTEXT -GIT_CREDENTIAL_SSH_KEY = C.GIT_CREDENTIAL_SSH_KEY -GIT_CREDENTIAL_SSH_CUSTOM = C.GIT_CREDENTIAL_SSH_CUSTOM -GIT_CREDENTIAL_DEFAULT = C.GIT_CREDENTIAL_DEFAULT -GIT_CREDENTIAL_SSH_INTERACTIVE = C.GIT_CREDENTIAL_SSH_INTERACTIVE -GIT_CREDENTIAL_USERNAME = C.GIT_CREDENTIAL_USERNAME -GIT_CREDENTIAL_SSH_MEMORY = C.GIT_CREDENTIAL_SSH_MEMORY +from .enums import CredentialType class Username: @@ -46,8 +39,8 @@ def __init__(self, username): self._username = username @property - def credential_type(self): - return GIT_CREDENTIAL_USERNAME + def credential_type(self) -> CredentialType: + return CredentialType.USERNAME @property def credential_tuple(self): @@ -69,8 +62,8 @@ def __init__(self, username, password): self._password = password @property - def credential_type(self): - return GIT_CREDENTIAL_USERPASS_PLAINTEXT + def credential_type(self) -> CredentialType: + return CredentialType.USERPASS_PLAINTEXT @property def credential_tuple(self): @@ -110,8 +103,8 @@ def __init__(self, username, pubkey, privkey, passphrase): self._passphrase = passphrase @property - def credential_type(self): - return GIT_CREDENTIAL_SSH_KEY + def credential_type(self) -> CredentialType: + return CredentialType.SSH_KEY @property def credential_tuple(self): @@ -128,5 +121,5 @@ def __init__(self, username): class KeypairFromMemory(Keypair): @property - def credential_type(self): - return GIT_CREDENTIAL_SSH_MEMORY + def credential_type(self) -> CredentialType: + return CredentialType.SSH_MEMORY diff --git a/pygit2/enums.py b/pygit2/enums.py index 00595977..2885c93f 100644 --- a/pygit2/enums.py +++ b/pygit2/enums.py @@ -234,6 +234,43 @@ class ConfigLevel(IntEnum): specific config file available that actually is loaded)""" +class CredentialType(IntFlag): + """ + Supported credential types. This represents the various types of + authentication methods supported by the library. + """ + + USERPASS_PLAINTEXT = C.GIT_CREDENTIAL_USERPASS_PLAINTEXT + "A vanilla user/password request" + + SSH_KEY = C.GIT_CREDENTIAL_SSH_KEY + "An SSH key-based authentication request" + + SSH_CUSTOM = C.GIT_CREDENTIAL_SSH_CUSTOM + "An SSH key-based authentication request, with a custom signature" + + DEFAULT = C.GIT_CREDENTIAL_DEFAULT + "An NTLM/Negotiate-based authentication request." + + SSH_INTERACTIVE = C.GIT_CREDENTIAL_SSH_INTERACTIVE + "An SSH interactive authentication request." + + USERNAME = C.GIT_CREDENTIAL_USERNAME + """ + Username-only authentication request. + Used as a pre-authentication step if the underlying transport (eg. SSH, + with no username in its URL) does not know which username to use. + """ + + SSH_MEMORY = C.GIT_CREDENTIAL_SSH_MEMORY + """ + An SSH key-based authentication request. + Allows credentials to be read from memory instead of files. + Note that because of differences in crypto backend support, it might + not be functional. + """ + + class DeltaFlags(IntFlag): """ Flags for the delta object and the file objects on each side. diff --git a/test/test_credentials.py b/test/test_credentials.py index 3bccc725..18088d09 100644 --- a/test/test_credentials.py +++ b/test/test_credentials.py @@ -30,7 +30,7 @@ import pytest import pygit2 -from pygit2 import Username, UserPass, Keypair, KeypairFromAgent, KeypairFromMemory +from pygit2 import CredentialType, Username, UserPass, Keypair, KeypairFromAgent, KeypairFromMemory from . import utils @@ -127,7 +127,7 @@ def test_keypair_from_memory(tmp_path, pygit2_empty_key): def test_callback(testrepo): class MyCallbacks(pygit2.RemoteCallbacks): def credentials(testrepo, url, username, allowed): - assert allowed & pygit2.GIT_CREDENTIAL_USERPASS_PLAINTEXT + assert allowed & CredentialType.USERPASS_PLAINTEXT raise Exception("I don't know the password") url = "https://github.com/github/github" @@ -138,7 +138,7 @@ def credentials(testrepo, url, username, allowed): def test_bad_cred_type(testrepo): class MyCallbacks(pygit2.RemoteCallbacks): def credentials(testrepo, url, username, allowed): - assert allowed & pygit2.GIT_CREDENTIAL_USERPASS_PLAINTEXT + assert allowed & CredentialType.USERPASS_PLAINTEXT return Keypair("git", "foo.pub", "foo", "sekkrit") url = "https://github.com/github/github"