Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce credential support #354

Merged
merged 6 commits into from
Mar 27, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/remotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
31 changes: 16 additions & 15 deletions pygit2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from .repository import Repository
from .version import __version__
from .settings import Settings

from .credentials import *

def init_repository(path, bare=False):
"""
Expand All @@ -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*.
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.

**bare** indicates whether a bare git repository should be created.
:param str url: URL of the repository to clone

**remote_name** is the name given to the "origin" remote.
The default is "origin".
:param str path: Local path to clone into

**checkout_branch** gives the name of the branch to checkout.
None means use the remote's *HEAD*.
:param bool bare: Whether the local repository should be bare

Returns a Repository class pointing to the newly cloned repository.
: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.

If you wish to use the repo, you need to do a checkout for one of
the available branches, like this:
:param callable credentials: authentication to use if the remote
requires it

>>> repo = repo.clone_repository("url", "path")
>>> repo.checkout(branch) # i.e.: refs/heads/master
: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 changes: 77 additions & 0 deletions pygit2/credentials.py
Original file line number Diff line number Diff line change
@@ -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
21 changes: 19 additions & 2 deletions src/pygit2.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -144,18 +152,24 @@ 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;
opts.ignore_cert_errors = ignore_cert_errors;
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);
Expand Down Expand Up @@ -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)
Expand Down
22 changes: 22 additions & 0 deletions src/remote.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -631,6 +639,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},
Expand Down Expand Up @@ -691,10 +711,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;
Expand Down
1 change: 1 addition & 0 deletions src/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ typedef struct {
git_remote *remote;
/* Callbacks for network events */
PyObject *progress;
PyObject *credentials;
PyObject *transfer_progress;
PyObject *update_tips;
} Remote;
Expand Down
89 changes: 89 additions & 0 deletions src/utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,92 @@ get_strarraygit_from_pylist(git_strarray *array, PyObject *pylist)

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;
}
2 changes: 2 additions & 0 deletions src/utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading