Merge remote-tracking branch 'carlos/credentials'

This commit is contained in:
J. David Ibáñez
2014-03-27 20:54:02 +01:00
10 changed files with 344 additions and 19 deletions

View File

@@ -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

View File

@@ -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):
"""
@@ -51,30 +51,31 @@ 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*.
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.
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.
: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()

77
pygit2/credentials.py Normal file
View File

@@ -0,0 +1,77 @@
# -*- 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 GIT_CREDTYPE_USERPASS_PLAINTEXT, GIT_CREDTYPE_SSH_KEY
class UserPass:
"""Username/Password credentials
This is an object suitable for passing to a remote's credentials
callback and for returning from said callback.
"""
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:
"""SSH key pair credentials
This is an object suitable for passing to a remote's credentials
callback and for returning from said callback.
"""
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

View File

@@ -116,6 +116,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"
@@ -144,11 +152,12 @@ clone_repository(PyObject *self, PyObject *args) {
const char *path;
unsigned int bare, ignore_cert_errors;
const char *remote_name, *checkout_branch;
PyObject *credentials = NULL;
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;
@@ -156,6 +165,11 @@ clone_repository(PyObject *self, PyObject *args) {
opts.remote_name = remote_name;
opts.checkout_branch = checkout_branch;
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)
return Error_set(err);
@@ -452,6 +466,9 @@ moduleinit(PyObject* m)
/* 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)

View File

@@ -156,6 +156,14 @@ progress_cb(const char *str, int len, void *data)
return 0;
}
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;
return callable_to_credentials(out, url, username_from_url, allowed_types, remote->credentials);
}
static int
transfer_progress_cb(const git_transfer_progress *stats, void *data)
{
@@ -632,6 +640,18 @@ PyGetSetDef Remote_getseters[] = {
PyMemberDef Remote_members[] = {
MEMBER(Remote, progress, T_OBJECT_EX, "Progress output 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},
@@ -692,10 +712,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;

View File

@@ -202,6 +202,7 @@ typedef struct {
git_remote *remote;
/* Callbacks for network events */
PyObject *progress;
PyObject *credentials;
PyObject *transfer_progress;
PyObject *update_tips;
} Remote;

View File

@@ -153,3 +153,92 @@ on_error:
return -1;
}
static int
py_cred_to_git_cred(git_cred **out, PyObject *py_cred, unsigned int allowed)
{
PyObject *py_type, *py_tuple;
long type;
int err = -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;
}
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 & type)) {
PyErr_SetString(PyExc_TypeError, "invalid credential type");
goto cleanup;
}
switch (type) {
case GIT_CREDTYPE_USERPASS_PLAINTEXT:
{
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:
{
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");
break;
}
cleanup:
Py_XDECREF(py_type);
Py_XDECREF(py_tuple);
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 = NULL, *arglist = NULL;
if (credentials == NULL || credentials == Py_None)
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;
}

View File

@@ -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)

93
test/test_credentials.py Normal file
View File

@@ -0,0 +1,93 @@
# -*- 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 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 = UserPass(username, password)
self.assertEqual((username, password), cred.credential_tuple)
def test_ssh_key(self):
username = "git"
pubkey = "id_rsa.pub"
privkey = "id_rsa"
passphrase = "bad wolf"
cred = Keypair(username, pubkey, privkey, passphrase)
self.assertEqual((username, pubkey, privkey, passphrase), cred.credential_tuple)
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 Keypair("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()

View File

@@ -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:
#