From b49da5396228526a1a9ca7400437c28027662200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Tue, 25 Mar 2014 18:31:43 +0100 Subject: [PATCH 1/6] Introduce credentials --- pygit2/__init__.py | 2 +- pygit2/credentials.py | 52 +++++++++ src/credentials.c | 225 +++++++++++++++++++++++++++++++++++++++ src/pygit2.c | 9 ++ src/remote.c | 77 ++++++++++++++ src/types.h | 20 ++++ test/test_credentials.py | 98 +++++++++++++++++ 7 files changed, 482 insertions(+), 1 deletion(-) create mode 100644 pygit2/credentials.py create mode 100644 src/credentials.c create mode 100644 test/test_credentials.py diff --git a/pygit2/__init__.py b/pygit2/__init__.py index 8d40fc4..60e8942 100644 --- a/pygit2/__init__.py +++ b/pygit2/__init__.py @@ -36,7 +36,7 @@ from _pygit2 import * from .repository import Repository from .version import __version__ from .settings import Settings - +from .credentials import * def init_repository(path, bare=False): """ diff --git a/pygit2/credentials.py b/pygit2/credentials.py new file mode 100644 index 0000000..a3a3c25 --- /dev/null +++ b/pygit2/credentials.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2010-2014 The pygit2 contributors +# +# This file is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2, +# as published by the Free Software Foundation. +# +# In addition to the permissions in the GNU General Public License, +# the authors give you unlimited permission to link the compiled +# version of this file into combinations with other programs, +# and to distribute those combinations without any restriction +# coming from the use of this file. (The General Public License +# restrictions do apply in other respects; for example, they cover +# modification of the file, and distribution when not linked into +# a combined executable.) +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +# Import from pygit2 +from _pygit2 import CredUsernamePassword, CredSshKey +from _pygit2 import GIT_CREDTYPE_USERPASS_PLAINTEXT + +class UserPass(CredUsernamePassword): + """Username/Password credentials + + This is an object suitable for passing to a remote's credentials + callback. + + """ + + def __call__(self, _url, _username, _allowed): + return self + +class Keypair(CredSshKey): + """SSH key pair credentials + + This is an object suitable for passing to a remote's credentials + callback. + + """ + + def __call__(self, _url, _username, _allowed): + return self diff --git a/src/credentials.c b/src/credentials.c new file mode 100644 index 0000000..5711f04 --- /dev/null +++ b/src/credentials.c @@ -0,0 +1,225 @@ +/* + * Copyright 2010-2014 The pygit2 contributors + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License, version 2, + * as published by the Free Software Foundation. + * + * In addition to the permissions in the GNU General Public License, + * the authors give you unlimited permission to link the compiled + * version of this file into combinations with other programs, + * and to distribute those combinations without any restriction + * coming from the use of this file. (The General Public License + * restrictions do apply in other respects; for example, they cover + * modification of the file, and distribution when not linked into + * a combined executable.) + * + * This file is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#define PY_SSIZE_T_CLEAN +#include +#include +#include "error.h" +#include "types.h" +#include "utils.h" +#include "oid.h" +#include "refspec.h" +#include "remote.h" + +int +CredUsernamePassword_init(CredUsernamePassword *self, PyObject *args, PyObject *kwds) +{ + char *username, *password; + + if (kwds && PyDict_Size(kwds) > 0) { + PyErr_SetString(PyExc_TypeError, "CredUsernamePassword takes no keyword arguments"); + return -1; + } + + if (!PyArg_ParseTuple(args, "ss", &username, &password)) + return -1; + + self->parent.credtype = GIT_CREDTYPE_USERPASS_PLAINTEXT; + + self->username = strdup(username); + if (!self->username) { + PyErr_NoMemory(); + return -1; + } + + self->password = strdup(password); + if (!self->password) { + free(self->username); + PyErr_NoMemory(); + return -1; + } + + return 0; +} + +void +CredUsernamePassword_dealloc(CredUsernamePassword *self) +{ + free(self->username); + free(self->password); + + PyObject_Del(self); +} + +PyMemberDef CredUsernamePassword_members[] = { + MEMBER(CredUsernamePassword, username, T_STRING, "username"), + MEMBER(CredUsernamePassword, password, T_STRING, "password"), + {NULL}, +}; + +PyDoc_STRVAR(CredUsernamePassword__doc__, + "Credential type for username/password combination"); + +PyTypeObject CredUsernamePasswordType = { + PyVarObject_HEAD_INIT(NULL, 0) + "_pygit2.CredUsernamePassword", /* tp_name */ + sizeof(CredUsernamePassword), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)CredUsernamePassword_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ + CredUsernamePassword__doc__, /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + CredUsernamePassword_members, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc)CredUsernamePassword_init, /* tp_init */ + 0, /* tp_alloc */ + 0, /* tp_new */ +}; + +int +CredSshKey_init(CredSshKey *self, PyObject *args, PyObject *kwds) +{ + char *username, *pubkey, *privkey, *passphrase; + + if (kwds && PyDict_Size(kwds) > 0) { + PyErr_SetString(PyExc_TypeError, "CredSshKey takes no keyword arguments"); + return -1; + } + + if (!PyArg_ParseTuple(args, "ssss", &username, &pubkey, + &privkey, &passphrase)) + return -1; + + self->parent.credtype = GIT_CREDTYPE_SSH_KEY; + self->username = self->pubkey = self->privkey = self->passphrase = NULL; + + self->username = strdup(username); + self->pubkey = strdup(pubkey); + self->privkey = strdup(privkey); + self->passphrase = strdup(passphrase); + + if (!(self->username && self->pubkey && self->privkey && self->passphrase)) + goto on_oom; + + return 0; + + on_oom: + free(self->username); + free(self->pubkey); + free(self->privkey); + free(self->passphrase); + PyErr_NoMemory(); + return -1; +} + +void +CredSshKey_dealloc(CredSshKey *self) +{ + free(self->username); + free(self->pubkey); + free(self->privkey); + free(self->passphrase); + + PyObject_Del(self); +} + +PyMemberDef CredSshKey_members[] = { + MEMBER(CredSshKey, username, T_STRING, "username"), + MEMBER(CredSshKey, pubkey, T_STRING, "pubkey"), + MEMBER(CredSshKey, privkey, T_STRING, "privkey"), + MEMBER(CredSshKey, passphrase, T_STRING, "passphrase"), + {NULL}, +}; + +PyDoc_STRVAR(CredSshKey__doc__, + "Credential type for an SSH keypair"); + +PyTypeObject CredSshKeyType = { + PyVarObject_HEAD_INIT(NULL, 0) + "_pygit2.CredSshKey", /* tp_name */ + sizeof(CredSshKey), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)CredSshKey_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ + CredSshKey__doc__, /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + CredSshKey_members, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc)CredSshKey_init, /* tp_init */ + 0, /* tp_alloc */ + 0, /* tp_new */ +}; diff --git a/src/pygit2.c b/src/pygit2.c index 9b6319e..ea64354 100644 --- a/src/pygit2.c +++ b/src/pygit2.c @@ -74,6 +74,8 @@ extern PyTypeObject RefLogEntryType; extern PyTypeObject BranchType; extern PyTypeObject SignatureType; extern PyTypeObject RemoteType; +extern PyTypeObject CredUsernamePasswordType; +extern PyTypeObject CredSshKeyType; extern PyTypeObject RefspecType; extern PyTypeObject TransferProgressType; extern PyTypeObject NoteType; @@ -444,14 +446,21 @@ moduleinit(PyObject* m) /* Remotes */ INIT_TYPE(RemoteType, NULL, NULL) + INIT_TYPE(CredUsernamePasswordType, NULL, PyType_GenericNew) + INIT_TYPE(CredSshKeyType, NULL, PyType_GenericNew) INIT_TYPE(RefspecType, NULL, NULL) INIT_TYPE(TransferProgressType, NULL, NULL) ADD_TYPE(m, Remote) + ADD_TYPE(m, CredUsernamePassword) + ADD_TYPE(m, CredSshKey) ADD_TYPE(m, Refspec) ADD_TYPE(m, TransferProgress) /* Direction for the refspec */ ADD_CONSTANT_INT(m, GIT_DIRECTION_FETCH) ADD_CONSTANT_INT(m, GIT_DIRECTION_PUSH) + /* Credential types */ + ADD_CONSTANT_INT(m, GIT_CREDTYPE_USERPASS_PLAINTEXT) + ADD_CONSTANT_INT(m, GIT_CREDTYPE_SSH_KEY) /* Blame */ INIT_TYPE(BlameType, NULL, NULL) diff --git a/src/remote.c b/src/remote.c index c738784..9c7cce2 100644 --- a/src/remote.c +++ b/src/remote.c @@ -39,6 +39,8 @@ extern PyObject *GitError; extern PyTypeObject RepositoryType; extern PyTypeObject TransferProgressType; +extern PyTypeObject CredUsernamePasswordType; +extern PyTypeObject CredSshKeyType; PyObject * wrap_transfer_progress(const git_transfer_progress *stats) @@ -156,6 +158,78 @@ progress_cb(const char *str, int len, void *data) return 0; } +static int +py_cred_to_git_cred(git_cred **out, PyObject *py_cred, unsigned int allowed) +{ + Cred *base_cred; + int err; + + if (!PyObject_TypeCheck(py_cred, &CredUsernamePasswordType) && + !PyObject_TypeCheck(py_cred, &CredSshKeyType)) { + PyErr_SetString(PyExc_TypeError, "unkown credential type"); + return -1; + } + + base_cred = (Cred *) py_cred; + + /* Sanity check, make sure we're given credentials we can use */ + if (!(allowed & base_cred->credtype)) { + PyErr_SetString(PyExc_TypeError, "invalid credential type"); + return -1; + } + + switch (base_cred->credtype) { + case GIT_CREDTYPE_USERPASS_PLAINTEXT: + { + CredUsernamePassword *cred = (CredUsernamePassword *) base_cred; + err = git_cred_userpass_plaintext_new(out, cred->username, cred->password); + break; + } + case GIT_CREDTYPE_SSH_KEY: + { + CredSshKey *cred = (CredSshKey *) base_cred; + err = git_cred_ssh_key_new(out, cred->username, cred->pubkey, cred->privkey, cred->passphrase); + break; + } + default: + PyErr_SetString(PyExc_TypeError, "unsupported credential type"); + err = -1; + break; + } + + return err; +} + +static int +credentials_cb(git_cred **out, const char *url, const char *username_from_url, unsigned int allowed_types, void *data) +{ + Remote *remote = (Remote *) data; + PyObject *arglist, *py_cred; + int err; + + if (remote->credentials == NULL) + return 0; + + if (!PyCallable_Check(remote->credentials)) { + PyErr_SetString(PyExc_TypeError, "credentials callback is not callable"); + return -1; + } + + arglist = Py_BuildValue("(szI)", url, username_from_url, allowed_types); + py_cred = PyObject_CallObject(remote->credentials, arglist); + Py_DECREF(arglist); + + if (!py_cred) + return -1; + + err = py_cred_to_git_cred(out, py_cred, allowed_types); + + + Py_DECREF(py_cred); + + return err; +} + static int transfer_progress_cb(const git_transfer_progress *stats, void *data) { @@ -631,6 +705,7 @@ PyGetSetDef Remote_getseters[] = { PyMemberDef Remote_members[] = { MEMBER(Remote, progress, T_OBJECT_EX, "Progress output callback"), + MEMBER(Remote, credentials, T_OBJECT_EX, "Credentials callback"), MEMBER(Remote, transfer_progress, T_OBJECT_EX, "Transfer progress callback"), MEMBER(Remote, update_tips, T_OBJECT_EX, "update tips callback"), {NULL}, @@ -691,10 +766,12 @@ wrap_remote(git_remote *c_remote, Repository *repo) py_remote->repo = repo; py_remote->remote = c_remote; py_remote->progress = NULL; + py_remote->credentials = NULL; py_remote->transfer_progress = NULL; py_remote->update_tips = NULL; callbacks.progress = progress_cb; + callbacks.credentials = credentials_cb; callbacks.transfer_progress = transfer_progress_cb; callbacks.update_tips = update_tips_cb; callbacks.payload = py_remote; diff --git a/src/types.h b/src/types.h index e6a3189..1a73fc5 100644 --- a/src/types.h +++ b/src/types.h @@ -202,10 +202,30 @@ typedef struct { git_remote *remote; /* Callbacks for network events */ PyObject *progress; + PyObject *credentials; PyObject *transfer_progress; PyObject *update_tips; } Remote; +typedef struct { + PyObject_HEAD + git_credtype_t credtype; +} Cred; + +typedef struct { + Cred parent; + char *username; + char *password; +} CredUsernamePassword; + +typedef struct { + Cred parent; + char *username; + char *pubkey; + char *privkey; + char *passphrase; +} CredSshKey; + /* git_refspec */ typedef struct { PyObject_HEAD diff --git a/test/test_credentials.py b/test/test_credentials.py new file mode 100644 index 0000000..c1b95e5 --- /dev/null +++ b/test/test_credentials.py @@ -0,0 +1,98 @@ +# -*- coding: UTF-8 -*- +# +# Copyright 2010-2014 The pygit2 contributors +# +# This file is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2, +# as published by the Free Software Foundation. +# +# In addition to the permissions in the GNU General Public License, +# the authors give you unlimited permission to link the compiled +# version of this file into combinations with other programs, +# and to distribute those combinations without any restriction +# coming from the use of this file. (The General Public License +# restrictions do apply in other respects; for example, they cover +# modification of the file, and distribution when not linked into +# a combined executable.) +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +"""Tests for credentials""" + + +import unittest +import pygit2 +from pygit2 import CredUsernamePassword, CredSshKey +from pygit2 import GIT_CREDTYPE_USERPASS_PLAINTEXT +from pygit2 import UserPass, Keypair +from . import utils + +REMOTE_NAME = 'origin' +REMOTE_URL = 'git://github.com/libgit2/pygit2.git' +REMOTE_FETCHSPEC_SRC = 'refs/heads/*' +REMOTE_FETCHSPEC_DST = 'refs/remotes/origin/*' +REMOTE_REPO_OBJECTS = 30 +REMOTE_REPO_BYTES = 2758 + +ORIGIN_REFSPEC = '+refs/heads/*:refs/remotes/origin/*' + +class CredentialCreateTest(utils.NoRepoTestCase): + def test_userpass(self): + username = "git" + password = "sekkrit" + + cred = CredUsernamePassword(username, password) + self.assertEqual(username, cred.username) + self.assertEqual(password, cred.password) + + def test_ssh_key(self): + username = "git" + pubkey = "id_rsa.pub" + privkey = "id_rsa" + passphrase = "bad wolf" + + cred = CredSshKey(username, pubkey, privkey, passphrase) + self.assertEqual(username, cred.username) + self.assertEqual(pubkey, cred.pubkey) + self.assertEqual(privkey, cred.privkey) + self.assertEqual(passphrase, cred.passphrase) + +class CredentialCallback(utils.RepoTestCase): + def test_callback(self): + def credentials_cb(url, username, allowed): + self.assertTrue(allowed & GIT_CREDTYPE_USERPASS_PLAINTEXT) + raise Exception("I don't know the password") + + remote = self.repo.create_remote("github", "https://github.com/github/github") + remote.credentials = credentials_cb + + self.assertRaises(Exception, remote.fetch) + + def test_bad_cred_type(self): + def credentials_cb(url, username, allowed): + self.assertTrue(allowed & GIT_CREDTYPE_USERPASS_PLAINTEXT) + return CredSshKey("git", "foo.pub", "foo", "sekkrit") + + remote = self.repo.create_remote("github", "https://github.com/github/github") + remote.credentials = credentials_cb + + self.assertRaises(TypeError, remote.fetch) + +class CallableCredentialTest(utils.RepoTestCase): + + def test_user_pass(self): + remote = self.repo.create_remote("bb", "https://bitbucket.org/libgit2/testgitrepository.git") + remote.credentials = UserPass("libgit2", "libgit2") + + remote.fetch() + +if __name__ == '__main__': + unittest.main() From fc0cdaebd6a4ecc92cc3ca59db755a394bde145d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Wed, 26 Mar 2014 14:03:29 +0100 Subject: [PATCH 2/6] Remote: add documentation for credentials --- docs/remotes.rst | 16 ++++++++++++++++ pygit2/credentials.py | 4 ++-- src/remote.c | 13 ++++++++++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/docs/remotes.rst b/docs/remotes.rst index f77bd73..58ab8ef 100644 --- a/docs/remotes.rst +++ b/docs/remotes.rst @@ -52,3 +52,19 @@ The Refspec type .. automethod:: pygit2.Refspec.dst_matches .. automethod:: pygit2.Refspec.transform .. automethod:: pygit2.Refspec.rtransform + +Credentials +================ + +.. automethod:: pygit2.Remote.credentials + +There are two types of credentials: username/password and SSH key +pairs. Both :py:class:`pygit2.UserPass` and :py:class:`pygit2.Keypair` +are callable objects, with the appropriate signature for the +credentials callback. They will ignore all the arguments and return +themselves. This is useful for scripts where the credentials are known +ahead of time. More complete interfaces would want to look up in their +keychain or ask the user for the data to use in the credentials. + +.. autoclass:: pygit2.UserPass +.. autoclass:: pygit2.Keypair diff --git a/pygit2/credentials.py b/pygit2/credentials.py index a3a3c25..dad966b 100644 --- a/pygit2/credentials.py +++ b/pygit2/credentials.py @@ -33,7 +33,7 @@ class UserPass(CredUsernamePassword): """Username/Password credentials This is an object suitable for passing to a remote's credentials - callback. + callback and for returning from said callback. """ @@ -44,7 +44,7 @@ class Keypair(CredSshKey): """SSH key pair credentials This is an object suitable for passing to a remote's credentials - callback. + callback and for returning from said callback. """ diff --git a/src/remote.c b/src/remote.c index 9c7cce2..65a195e 100644 --- a/src/remote.c +++ b/src/remote.c @@ -705,7 +705,18 @@ PyGetSetDef Remote_getseters[] = { PyMemberDef Remote_members[] = { MEMBER(Remote, progress, T_OBJECT_EX, "Progress output callback"), - MEMBER(Remote, credentials, T_OBJECT_EX, "Credentials callback"), + MEMBER(Remote, credentials, T_OBJECT_EX, + "credentials(url, username_from_url, allowed_types) -> credential\n" + "\n" + "Credentials callback\n" + "\n" + "If the remote server requires authentication, this function will\n" + "be called and its return value used for authentication.\n" + "\n" + ":param str url: The url of the remote\n" + ":param username_from_url: Username extracted from the url, if any\n" + ":type username_from_url: str or None\n" + ":param int allowed_types: credential types supported by the remote "), MEMBER(Remote, transfer_progress, T_OBJECT_EX, "Transfer progress callback"), MEMBER(Remote, update_tips, T_OBJECT_EX, "update tips callback"), {NULL}, From 75f9f883352b1ad8cff069a538607adf79bd7fe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Wed, 26 Mar 2014 14:19:38 +0100 Subject: [PATCH 3/6] Update clone_repository's docs It claims you need to checkout a branch after clone, which is not the case currently (the clone function will do it for you). While here, format the docstring for sphinx to make it pretty. --- pygit2/__init__.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/pygit2/__init__.py b/pygit2/__init__.py index 60e8942..f4f471e 100644 --- a/pygit2/__init__.py +++ b/pygit2/__init__.py @@ -52,24 +52,20 @@ def init_repository(path, bare=False): def clone_repository( url, path, bare=False, ignore_cert_errors=False, remote_name="origin", checkout_branch=None): - """ - Clones a new Git repository from *url* in the given *path*. - - **bare** indicates whether a bare git repository should be created. - - **remote_name** is the name given to the "origin" remote. - The default is "origin". - - **checkout_branch** gives the name of the branch to checkout. - None means use the remote's *HEAD*. + """Clones a new Git repository from *url* in the given *path*. Returns a Repository class pointing to the newly cloned repository. - If you wish to use the repo, you need to do a checkout for one of - the available branches, like this: + :param str url: URL of the repository to clone - >>> repo = repo.clone_repository("url", "path") - >>> repo.checkout(branch) # i.e.: refs/heads/master + :param str path: Local path to clone into + + :param bool bare: Whether the local repository should be bare + + :param str remote_name: Name to give the remote at *url*. + + :param str checkout_branch: Branch to checkout after the + clone. The default is to use the remote's default branch. """ From b3ce1b5da6200a027a38d70a845cb79ab1ff9787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Wed, 26 Mar 2014 14:50:25 +0100 Subject: [PATCH 4/6] Add credentials support to clone_repository() --- pygit2/__init__.py | 9 ++++-- src/pygit2.c | 16 +++++++++-- src/remote.c | 66 +----------------------------------------- src/utils.c | 71 ++++++++++++++++++++++++++++++++++++++++++++++ src/utils.h | 2 ++ 5 files changed, 95 insertions(+), 69 deletions(-) diff --git a/pygit2/__init__.py b/pygit2/__init__.py index f4f471e..fc2e693 100644 --- a/pygit2/__init__.py +++ b/pygit2/__init__.py @@ -51,7 +51,7 @@ def init_repository(path, bare=False): def clone_repository( url, path, bare=False, ignore_cert_errors=False, - remote_name="origin", checkout_branch=None): + remote_name="origin", checkout_branch=None, credentials=None): """Clones a new Git repository from *url* in the given *path*. Returns a Repository class pointing to the newly cloned repository. @@ -67,10 +67,15 @@ def clone_repository( :param str checkout_branch: Branch to checkout after the clone. The default is to use the remote's default branch. + :param callable credentials: authentication to use if the remote + requires it + + :rtype: Repository + """ _pygit2.clone_repository( - url, path, bare, ignore_cert_errors, remote_name, checkout_branch) + url, path, bare, ignore_cert_errors, remote_name, checkout_branch, credentials) return Repository(path) settings = Settings() diff --git a/src/pygit2.c b/src/pygit2.c index ea64354..0126dc8 100644 --- a/src/pygit2.c +++ b/src/pygit2.c @@ -118,6 +118,14 @@ init_repository(PyObject *self, PyObject *args) { Py_RETURN_NONE; }; +static int +credentials_cb(git_cred **out, const char *url, const char *username_from_url, unsigned int allowed_types, void *data) +{ + PyObject *credentials = (PyObject *) data; + + return callable_to_credentials(out, url, username_from_url, allowed_types, credentials); +} + PyDoc_STRVAR(clone_repository__doc__, "clone_repository(url, path, bare, remote_name, checkout_branch)\n" "\n" @@ -146,11 +154,12 @@ clone_repository(PyObject *self, PyObject *args) { const char *path; unsigned int bare, ignore_cert_errors; const char *remote_name, *checkout_branch; + PyObject *credentials; int err; git_clone_options opts = GIT_CLONE_OPTIONS_INIT; - if (!PyArg_ParseTuple(args, "zzIIzz", - &url, &path, &bare, &ignore_cert_errors, &remote_name, &checkout_branch)) + if (!PyArg_ParseTuple(args, "zzIIzzO", + &url, &path, &bare, &ignore_cert_errors, &remote_name, &checkout_branch, &credentials)) return NULL; opts.bare = bare; @@ -158,6 +167,9 @@ clone_repository(PyObject *self, PyObject *args) { opts.remote_name = remote_name; opts.checkout_branch = checkout_branch; + opts.remote_callbacks.credentials = credentials_cb; + opts.remote_callbacks.payload = credentials; + err = git_clone(&repo, url, path, &opts); if (err < 0) return Error_set(err); diff --git a/src/remote.c b/src/remote.c index 65a195e..b654a8c 100644 --- a/src/remote.c +++ b/src/remote.c @@ -158,76 +158,12 @@ progress_cb(const char *str, int len, void *data) return 0; } -static int -py_cred_to_git_cred(git_cred **out, PyObject *py_cred, unsigned int allowed) -{ - Cred *base_cred; - int err; - - if (!PyObject_TypeCheck(py_cred, &CredUsernamePasswordType) && - !PyObject_TypeCheck(py_cred, &CredSshKeyType)) { - PyErr_SetString(PyExc_TypeError, "unkown credential type"); - return -1; - } - - base_cred = (Cred *) py_cred; - - /* Sanity check, make sure we're given credentials we can use */ - if (!(allowed & base_cred->credtype)) { - PyErr_SetString(PyExc_TypeError, "invalid credential type"); - return -1; - } - - switch (base_cred->credtype) { - case GIT_CREDTYPE_USERPASS_PLAINTEXT: - { - CredUsernamePassword *cred = (CredUsernamePassword *) base_cred; - err = git_cred_userpass_plaintext_new(out, cred->username, cred->password); - break; - } - case GIT_CREDTYPE_SSH_KEY: - { - CredSshKey *cred = (CredSshKey *) base_cred; - err = git_cred_ssh_key_new(out, cred->username, cred->pubkey, cred->privkey, cred->passphrase); - break; - } - default: - PyErr_SetString(PyExc_TypeError, "unsupported credential type"); - err = -1; - break; - } - - return err; -} - static int credentials_cb(git_cred **out, const char *url, const char *username_from_url, unsigned int allowed_types, void *data) { Remote *remote = (Remote *) data; - PyObject *arglist, *py_cred; - int err; - if (remote->credentials == NULL) - return 0; - - if (!PyCallable_Check(remote->credentials)) { - PyErr_SetString(PyExc_TypeError, "credentials callback is not callable"); - return -1; - } - - arglist = Py_BuildValue("(szI)", url, username_from_url, allowed_types); - py_cred = PyObject_CallObject(remote->credentials, arglist); - Py_DECREF(arglist); - - if (!py_cred) - return -1; - - err = py_cred_to_git_cred(out, py_cred, allowed_types); - - - Py_DECREF(py_cred); - - return err; + return callable_to_credentials(out, url, username_from_url, allowed_types, remote->credentials); } static int diff --git a/src/utils.c b/src/utils.c index 44acf5c..66e546d 100644 --- a/src/utils.c +++ b/src/utils.c @@ -31,6 +31,8 @@ #include "utils.h" extern PyTypeObject ReferenceType; +extern PyTypeObject CredUsernamePasswordType; +extern PyTypeObject CredSshKeyType; /** * py_str_to_c_str() returns a newly allocated C string holding the string @@ -153,3 +155,72 @@ on_error: return -1; } + +static int +py_cred_to_git_cred(git_cred **out, PyObject *py_cred, unsigned int allowed) +{ + Cred *base_cred; + int err; + + if (!PyObject_TypeCheck(py_cred, &CredUsernamePasswordType) && + !PyObject_TypeCheck(py_cred, &CredSshKeyType)) { + PyErr_SetString(PyExc_TypeError, "unkown credential type"); + return -1; + } + + base_cred = (Cred *) py_cred; + + /* Sanity check, make sure we're given credentials we can use */ + if (!(allowed & base_cred->credtype)) { + PyErr_SetString(PyExc_TypeError, "invalid credential type"); + return -1; + } + + switch (base_cred->credtype) { + case GIT_CREDTYPE_USERPASS_PLAINTEXT: + { + CredUsernamePassword *cred = (CredUsernamePassword *) base_cred; + err = git_cred_userpass_plaintext_new(out, cred->username, cred->password); + break; + } + case GIT_CREDTYPE_SSH_KEY: + { + CredSshKey *cred = (CredSshKey *) base_cred; + err = git_cred_ssh_key_new(out, cred->username, cred->pubkey, cred->privkey, cred->passphrase); + break; + } + default: + PyErr_SetString(PyExc_TypeError, "unsupported credential type"); + err = -1; + break; + } + + return err; +} + +int +callable_to_credentials(git_cred **out, const char *url, const char *username_from_url, unsigned int allowed_types, PyObject *credentials) +{ + int err; + PyObject *py_cred, *arglist; + + if (credentials == NULL) + return 0; + + if (!PyCallable_Check(credentials)) { + PyErr_SetString(PyExc_TypeError, "credentials callback is not callable"); + return -1; + } + + arglist = Py_BuildValue("(szI)", url, username_from_url, allowed_types); + py_cred = PyObject_CallObject(credentials, arglist); + Py_DECREF(arglist); + + if (!py_cred) + return -1; + + err = py_cred_to_git_cred(out, py_cred, allowed_types); + Py_DECREF(py_cred); + + return err; +} diff --git a/src/utils.h b/src/utils.h index 81744f6..7f95d73 100644 --- a/src/utils.h +++ b/src/utils.h @@ -117,6 +117,8 @@ const char *py_str_borrow_c_str(PyObject **tvaue, PyObject *value, const char *e PyObject * get_pylist_from_git_strarray(git_strarray *strarray); int get_strarraygit_from_pylist(git_strarray *array, PyObject *pylist); +int callable_to_credentials(git_cred **out, const char *url, const char *username_from_url, unsigned int allowed_types, PyObject *credentials); + #define py_path_to_c_str(py_path) \ py_str_to_c_str(py_path, Py_FileSystemDefaultEncoding) From 77acb11cd0bb5d9c2be8fc046e71296b501de874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Wed, 26 Mar 2014 19:14:57 +0100 Subject: [PATCH 5/6] credentials: memory safety The docs say to use tp_free() to free the memory, and even though we use PyObject_Del() everywhere else, using this in the credentials does cause issues. --- src/credentials.c | 4 ++-- src/pygit2.c | 8 +++++--- src/utils.c | 4 ++-- test/test_repository.py | 7 +++++++ 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/credentials.c b/src/credentials.c index 5711f04..c3127ae 100644 --- a/src/credentials.c +++ b/src/credentials.c @@ -72,7 +72,7 @@ CredUsernamePassword_dealloc(CredUsernamePassword *self) free(self->username); free(self->password); - PyObject_Del(self); + Py_TYPE(self)->tp_free(self); } PyMemberDef CredUsernamePassword_members[] = { @@ -169,7 +169,7 @@ CredSshKey_dealloc(CredSshKey *self) free(self->privkey); free(self->passphrase); - PyObject_Del(self); + Py_TYPE(self)->tp_free(self); } PyMemberDef CredSshKey_members[] = { diff --git a/src/pygit2.c b/src/pygit2.c index 0126dc8..199bbac 100644 --- a/src/pygit2.c +++ b/src/pygit2.c @@ -154,7 +154,7 @@ clone_repository(PyObject *self, PyObject *args) { const char *path; unsigned int bare, ignore_cert_errors; const char *remote_name, *checkout_branch; - PyObject *credentials; + PyObject *credentials = NULL; int err; git_clone_options opts = GIT_CLONE_OPTIONS_INIT; @@ -167,8 +167,10 @@ clone_repository(PyObject *self, PyObject *args) { opts.remote_name = remote_name; opts.checkout_branch = checkout_branch; - opts.remote_callbacks.credentials = credentials_cb; - opts.remote_callbacks.payload = credentials; + if (credentials != Py_None) { + opts.remote_callbacks.credentials = credentials_cb; + opts.remote_callbacks.payload = credentials; + } err = git_clone(&repo, url, path, &opts); if (err < 0) diff --git a/src/utils.c b/src/utils.c index 66e546d..59e7ad5 100644 --- a/src/utils.c +++ b/src/utils.c @@ -202,9 +202,9 @@ int callable_to_credentials(git_cred **out, const char *url, const char *username_from_url, unsigned int allowed_types, PyObject *credentials) { int err; - PyObject *py_cred, *arglist; + PyObject *py_cred = NULL, *arglist = NULL; - if (credentials == NULL) + if (credentials == NULL || credentials == Py_None) return 0; if (!PyCallable_Check(credentials)) { diff --git a/test/test_repository.py b/test/test_repository.py index 8e8101c..71214cc 100644 --- a/test/test_repository.py +++ b/test/test_repository.py @@ -461,6 +461,13 @@ class CloneRepositoryTest(utils.NoRepoTestCase): self.assertFalse(repo.is_empty) self.assertEqual(repo.remotes[0].name, "custom_remote") + def test_clone_with_credentials(self): + credentials = pygit2.UserPass("libgit2", "libgit2") + repo = clone_repository( + "https://bitbucket.org/libgit2/testgitrepository.git", + self._temp_dir, credentials=credentials) + + self.assertFalse(repo.is_empty) # FIXME The tests below are commented because they are broken: # From 82d88191bb450a153a217ed8227dd1865c48016d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Wed, 26 Mar 2014 20:23:04 +0100 Subject: [PATCH 6/6] credentials: use more ducks Instead of making everyone inherit from our credential types, use an interface with two attributes, which makes the C code much shorter and simpler. --- pygit2/credentials.py | 33 +++++- src/credentials.c | 225 --------------------------------------- src/pygit2.c | 6 -- src/remote.c | 2 - src/types.h | 19 ---- src/utils.c | 52 ++++++--- test/test_credentials.py | 15 +-- 7 files changed, 69 insertions(+), 283 deletions(-) delete mode 100644 src/credentials.c diff --git a/pygit2/credentials.py b/pygit2/credentials.py index dad966b..1789022 100644 --- a/pygit2/credentials.py +++ b/pygit2/credentials.py @@ -26,10 +26,9 @@ # Boston, MA 02110-1301, USA. # Import from pygit2 -from _pygit2 import CredUsernamePassword, CredSshKey -from _pygit2 import GIT_CREDTYPE_USERPASS_PLAINTEXT +from _pygit2 import GIT_CREDTYPE_USERPASS_PLAINTEXT, GIT_CREDTYPE_SSH_KEY -class UserPass(CredUsernamePassword): +class UserPass: """Username/Password credentials This is an object suitable for passing to a remote's credentials @@ -37,10 +36,22 @@ class UserPass(CredUsernamePassword): """ + def __init__(self, username, password): + self._username = username + self._password = password + + @property + def credential_type(self): + return GIT_CREDTYPE_USERPASS_PLAINTEXT + + @property + def credential_tuple(self): + return (self._username, self._password) + def __call__(self, _url, _username, _allowed): return self -class Keypair(CredSshKey): +class Keypair: """SSH key pair credentials This is an object suitable for passing to a remote's credentials @@ -48,5 +59,19 @@ class Keypair(CredSshKey): """ + def __init__(self, username, pubkey, privkey, passphrase): + self._username = username + self._pubkey = pubkey + self._privkey = privkey + self._passphrase = passphrase + + @property + def credential_type(self): + return GIT_CREDTYPE_SSH_KEY + + @property + def credential_tuple(self): + return (self._username, self._pubkey, self._privkey, self._passphrase) + def __call__(self, _url, _username, _allowed): return self diff --git a/src/credentials.c b/src/credentials.c deleted file mode 100644 index c3127ae..0000000 --- a/src/credentials.c +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright 2010-2014 The pygit2 contributors - * - * This file is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License, version 2, - * as published by the Free Software Foundation. - * - * In addition to the permissions in the GNU General Public License, - * the authors give you unlimited permission to link the compiled - * version of this file into combinations with other programs, - * and to distribute those combinations without any restriction - * coming from the use of this file. (The General Public License - * restrictions do apply in other respects; for example, they cover - * modification of the file, and distribution when not linked into - * a combined executable.) - * - * This file is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; see the file COPYING. If not, write to - * the Free Software Foundation, 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#define PY_SSIZE_T_CLEAN -#include -#include -#include "error.h" -#include "types.h" -#include "utils.h" -#include "oid.h" -#include "refspec.h" -#include "remote.h" - -int -CredUsernamePassword_init(CredUsernamePassword *self, PyObject *args, PyObject *kwds) -{ - char *username, *password; - - if (kwds && PyDict_Size(kwds) > 0) { - PyErr_SetString(PyExc_TypeError, "CredUsernamePassword takes no keyword arguments"); - return -1; - } - - if (!PyArg_ParseTuple(args, "ss", &username, &password)) - return -1; - - self->parent.credtype = GIT_CREDTYPE_USERPASS_PLAINTEXT; - - self->username = strdup(username); - if (!self->username) { - PyErr_NoMemory(); - return -1; - } - - self->password = strdup(password); - if (!self->password) { - free(self->username); - PyErr_NoMemory(); - return -1; - } - - return 0; -} - -void -CredUsernamePassword_dealloc(CredUsernamePassword *self) -{ - free(self->username); - free(self->password); - - Py_TYPE(self)->tp_free(self); -} - -PyMemberDef CredUsernamePassword_members[] = { - MEMBER(CredUsernamePassword, username, T_STRING, "username"), - MEMBER(CredUsernamePassword, password, T_STRING, "password"), - {NULL}, -}; - -PyDoc_STRVAR(CredUsernamePassword__doc__, - "Credential type for username/password combination"); - -PyTypeObject CredUsernamePasswordType = { - PyVarObject_HEAD_INIT(NULL, 0) - "_pygit2.CredUsernamePassword", /* tp_name */ - sizeof(CredUsernamePassword), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor)CredUsernamePassword_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ - CredUsernamePassword__doc__, /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - 0, /* tp_methods */ - CredUsernamePassword_members, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc)CredUsernamePassword_init, /* tp_init */ - 0, /* tp_alloc */ - 0, /* tp_new */ -}; - -int -CredSshKey_init(CredSshKey *self, PyObject *args, PyObject *kwds) -{ - char *username, *pubkey, *privkey, *passphrase; - - if (kwds && PyDict_Size(kwds) > 0) { - PyErr_SetString(PyExc_TypeError, "CredSshKey takes no keyword arguments"); - return -1; - } - - if (!PyArg_ParseTuple(args, "ssss", &username, &pubkey, - &privkey, &passphrase)) - return -1; - - self->parent.credtype = GIT_CREDTYPE_SSH_KEY; - self->username = self->pubkey = self->privkey = self->passphrase = NULL; - - self->username = strdup(username); - self->pubkey = strdup(pubkey); - self->privkey = strdup(privkey); - self->passphrase = strdup(passphrase); - - if (!(self->username && self->pubkey && self->privkey && self->passphrase)) - goto on_oom; - - return 0; - - on_oom: - free(self->username); - free(self->pubkey); - free(self->privkey); - free(self->passphrase); - PyErr_NoMemory(); - return -1; -} - -void -CredSshKey_dealloc(CredSshKey *self) -{ - free(self->username); - free(self->pubkey); - free(self->privkey); - free(self->passphrase); - - Py_TYPE(self)->tp_free(self); -} - -PyMemberDef CredSshKey_members[] = { - MEMBER(CredSshKey, username, T_STRING, "username"), - MEMBER(CredSshKey, pubkey, T_STRING, "pubkey"), - MEMBER(CredSshKey, privkey, T_STRING, "privkey"), - MEMBER(CredSshKey, passphrase, T_STRING, "passphrase"), - {NULL}, -}; - -PyDoc_STRVAR(CredSshKey__doc__, - "Credential type for an SSH keypair"); - -PyTypeObject CredSshKeyType = { - PyVarObject_HEAD_INIT(NULL, 0) - "_pygit2.CredSshKey", /* tp_name */ - sizeof(CredSshKey), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor)CredSshKey_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ - CredSshKey__doc__, /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - 0, /* tp_methods */ - CredSshKey_members, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc)CredSshKey_init, /* tp_init */ - 0, /* tp_alloc */ - 0, /* tp_new */ -}; diff --git a/src/pygit2.c b/src/pygit2.c index 199bbac..bda9545 100644 --- a/src/pygit2.c +++ b/src/pygit2.c @@ -74,8 +74,6 @@ extern PyTypeObject RefLogEntryType; extern PyTypeObject BranchType; extern PyTypeObject SignatureType; extern PyTypeObject RemoteType; -extern PyTypeObject CredUsernamePasswordType; -extern PyTypeObject CredSshKeyType; extern PyTypeObject RefspecType; extern PyTypeObject TransferProgressType; extern PyTypeObject NoteType; @@ -460,13 +458,9 @@ moduleinit(PyObject* m) /* Remotes */ INIT_TYPE(RemoteType, NULL, NULL) - INIT_TYPE(CredUsernamePasswordType, NULL, PyType_GenericNew) - INIT_TYPE(CredSshKeyType, NULL, PyType_GenericNew) INIT_TYPE(RefspecType, NULL, NULL) INIT_TYPE(TransferProgressType, NULL, NULL) ADD_TYPE(m, Remote) - ADD_TYPE(m, CredUsernamePassword) - ADD_TYPE(m, CredSshKey) ADD_TYPE(m, Refspec) ADD_TYPE(m, TransferProgress) /* Direction for the refspec */ diff --git a/src/remote.c b/src/remote.c index b654a8c..ffbe17d 100644 --- a/src/remote.c +++ b/src/remote.c @@ -39,8 +39,6 @@ extern PyObject *GitError; extern PyTypeObject RepositoryType; extern PyTypeObject TransferProgressType; -extern PyTypeObject CredUsernamePasswordType; -extern PyTypeObject CredSshKeyType; PyObject * wrap_transfer_progress(const git_transfer_progress *stats) diff --git a/src/types.h b/src/types.h index 1a73fc5..a36fad6 100644 --- a/src/types.h +++ b/src/types.h @@ -207,25 +207,6 @@ typedef struct { PyObject *update_tips; } Remote; -typedef struct { - PyObject_HEAD - git_credtype_t credtype; -} Cred; - -typedef struct { - Cred parent; - char *username; - char *password; -} CredUsernamePassword; - -typedef struct { - Cred parent; - char *username; - char *pubkey; - char *privkey; - char *passphrase; -} CredSshKey; - /* git_refspec */ typedef struct { PyObject_HEAD diff --git a/src/utils.c b/src/utils.c index 59e7ad5..24b6bbd 100644 --- a/src/utils.c +++ b/src/utils.c @@ -31,8 +31,6 @@ #include "utils.h" extern PyTypeObject ReferenceType; -extern PyTypeObject CredUsernamePasswordType; -extern PyTypeObject CredSshKeyType; /** * py_str_to_c_str() returns a newly allocated C string holding the string @@ -159,42 +157,62 @@ on_error: static int py_cred_to_git_cred(git_cred **out, PyObject *py_cred, unsigned int allowed) { - Cred *base_cred; - int err; + PyObject *py_type, *py_tuple; + long type; + int err = -1; - if (!PyObject_TypeCheck(py_cred, &CredUsernamePasswordType) && - !PyObject_TypeCheck(py_cred, &CredSshKeyType)) { - PyErr_SetString(PyExc_TypeError, "unkown credential type"); - return -1; + py_type = PyObject_GetAttrString(py_cred, "credential_type"); + py_tuple = PyObject_GetAttrString(py_cred, "credential_tuple"); + + if (!py_type || !py_tuple) { + printf("py_type %p, py_tuple %p\n", py_type, py_tuple); + PyErr_SetString(PyExc_TypeError, "credential doesn't implement the interface"); + goto cleanup; } - base_cred = (Cred *) py_cred; + if (!PyLong_Check(py_type)) { + PyErr_SetString(PyExc_TypeError, "credential type is not a long"); + goto cleanup; + } + + type = PyLong_AsLong(py_type); /* Sanity check, make sure we're given credentials we can use */ - if (!(allowed & base_cred->credtype)) { + if (!(allowed & type)) { PyErr_SetString(PyExc_TypeError, "invalid credential type"); - return -1; + goto cleanup; } - switch (base_cred->credtype) { + switch (type) { case GIT_CREDTYPE_USERPASS_PLAINTEXT: { - CredUsernamePassword *cred = (CredUsernamePassword *) base_cred; - err = git_cred_userpass_plaintext_new(out, cred->username, cred->password); + const char *username, *password; + + if (!PyArg_ParseTuple(py_tuple, "ss", &username, &password)) + goto cleanup; + + err = git_cred_userpass_plaintext_new(out, username, password); break; } case GIT_CREDTYPE_SSH_KEY: { - CredSshKey *cred = (CredSshKey *) base_cred; - err = git_cred_ssh_key_new(out, cred->username, cred->pubkey, cred->privkey, cred->passphrase); + const char *username, *pubkey, *privkey, *passphrase; + + if (!PyArg_ParseTuple(py_tuple, "ssss", &username, &pubkey, &privkey, &passphrase)) + goto cleanup; + + err = git_cred_ssh_key_new(out, username, pubkey, privkey, passphrase); break; } default: PyErr_SetString(PyExc_TypeError, "unsupported credential type"); - err = -1; break; } +cleanup: + Py_XDECREF(py_type); + Py_XDECREF(py_tuple); + return err; } diff --git a/test/test_credentials.py b/test/test_credentials.py index c1b95e5..3bdeb6f 100644 --- a/test/test_credentials.py +++ b/test/test_credentials.py @@ -30,7 +30,6 @@ import unittest import pygit2 -from pygit2 import CredUsernamePassword, CredSshKey from pygit2 import GIT_CREDTYPE_USERPASS_PLAINTEXT from pygit2 import UserPass, Keypair from . import utils @@ -49,9 +48,8 @@ class CredentialCreateTest(utils.NoRepoTestCase): username = "git" password = "sekkrit" - cred = CredUsernamePassword(username, password) - self.assertEqual(username, cred.username) - self.assertEqual(password, cred.password) + cred = UserPass(username, password) + self.assertEqual((username, password), cred.credential_tuple) def test_ssh_key(self): username = "git" @@ -59,11 +57,8 @@ class CredentialCreateTest(utils.NoRepoTestCase): privkey = "id_rsa" passphrase = "bad wolf" - cred = CredSshKey(username, pubkey, privkey, passphrase) - self.assertEqual(username, cred.username) - self.assertEqual(pubkey, cred.pubkey) - self.assertEqual(privkey, cred.privkey) - self.assertEqual(passphrase, cred.passphrase) + cred = Keypair(username, pubkey, privkey, passphrase) + self.assertEqual((username, pubkey, privkey, passphrase), cred.credential_tuple) class CredentialCallback(utils.RepoTestCase): def test_callback(self): @@ -79,7 +74,7 @@ class CredentialCallback(utils.RepoTestCase): def test_bad_cred_type(self): def credentials_cb(url, username, allowed): self.assertTrue(allowed & GIT_CREDTYPE_USERPASS_PLAINTEXT) - return CredSshKey("git", "foo.pub", "foo", "sekkrit") + return Keypair("git", "foo.pub", "foo", "sekkrit") remote = self.repo.create_remote("github", "https://github.com/github/github") remote.credentials = credentials_cb