From 9570ca098cd0e92d1eb6aabc00fb8cac9fddd442 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Mon, 14 Sep 2009 13:03:57 +0200 Subject: Provide python bindings for sysdb Implement a set of python bindings for the sysdb with feature set similar to what is available in the tools. The primary consumers would be applications like system-config-users. Resolves: Ticket #102 --- contrib/sssd.spec.in | 7 +- server/Makefile.am | 34 ++ server/conf_macros.m4 | 17 + server/configure.ac | 9 + server/external/python.m4 | 49 +++ server/python/pysss.c | 1024 +++++++++++++++++++++++++++++++++++++++++++ server/tests/python-test.py | 391 +++++++++++++++++ server/tools/tools_util.h | 2 + server/util/util.h | 8 + 9 files changed, 1540 insertions(+), 1 deletion(-) create mode 100644 server/external/python.m4 create mode 100644 server/python/pysss.c create mode 100644 server/tests/python-test.py diff --git a/contrib/sssd.spec.in b/contrib/sssd.spec.in index e5134c06..408d4493 100644 --- a/contrib/sssd.spec.in +++ b/contrib/sssd.spec.in @@ -1,3 +1,5 @@ +%{!?python_sitearch: %global python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")} + Name: @PACKAGE_NAME@ Version: @PACKAGE_VERSION@ Release: 0@PRERELEASE_VERSION@%{?dist} @@ -48,6 +50,7 @@ BuildRequires: libxml2 BuildRequires: docbook-style-xsl BuildRequires: krb5-devel BuildRequires: c-ares-devel +BuildRequires: python-devel %description Provides a set of daemons to manage access to remote directories and @@ -86,7 +89,8 @@ rm -f \ $RPM_BUILD_ROOT/%{_libdir}/sssd/libsss_ldap.la \ $RPM_BUILD_ROOT/%{_libdir}/sssd/libsss_proxy.la \ $RPM_BUILD_ROOT/%{_libdir}/sssd/libsss_krb5.la \ - $RPM_BUILD_ROOT/%{_libdir}/krb5/plugins/libkrb5/sssd_krb5_locator_plugin.la + $RPM_BUILD_ROOT/%{_libdir}/krb5/plugins/libkrb5/sssd_krb5_locator_plugin.la \ + $RPM_BUILD_ROOT/%{python_sitearch}/pysss.la %clean rm -rf $RPM_BUILD_ROOT @@ -118,6 +122,7 @@ rm -rf $RPM_BUILD_ROOT %{_mandir}/man8/* %{_datadir}/locale/*/LC_MESSAGES/sss_client.mo %{_datadir}/locale/*/LC_MESSAGES/sss_daemon.mo +%{python_sitearch}/pysss.so %post /sbin/ldconfig diff --git a/server/Makefile.am b/server/Makefile.am index 6036110b..8ea773b0 100644 --- a/server/Makefile.am +++ b/server/Makefile.am @@ -88,6 +88,11 @@ libsss_crypt_la_SOURCES = \ libsss_crypt_la_CPPFLAGS = \ $(NSS_CFLAGS) +if BUILD_PYTHON_BINDINGS +pyexec_LTLIBRARIES = \ + pysss.la +endif + ############################### # Global compilation settings # ############################### @@ -201,6 +206,18 @@ SSSD_LIBS = \ $(NSS_LIBS) \ libsss_crypt.la +PYTHON_BINDINGS_LIBS = \ + $(TALLOC_LIBS) \ + $(TDB_LIBS) \ + $(TEVENT_LIBS) \ + $(POPT_LIBS) \ + $(LDB_LIBS) \ + $(DBUS_LIBS) \ + $(REPLACE_LIBS) \ + $(PCRE_LIBS) \ + $(NSS_LIBS) \ + libsss_crypt.la + dist_noinst_HEADERS = \ monitor/monitor.h \ util/btreemap.h \ @@ -443,6 +460,7 @@ memberof_la_SOURCES = \ ldb_modules/memberof.c memberof_la_CFLAGS = \ $(AM_CFLAGS) +memberof_la_LIBADD = $(LDB_LIBS) memberof_la_LDFLAGS = \ -avoid-version \ -module @@ -456,6 +474,22 @@ sssd_krb5_locator_plugin_la_LDFLAGS = \ -avoid-version \ -module +if BUILD_PYTHON_BINDINGS +pysss_la_SOURCES = \ + $(SSSD_UTIL_OBJ) \ + $(SSSD_TOOLS_OBJ) \ + python/pysss.c +pysss_la_CFLAGS = \ + $(AM_CFLAGS) \ + $(PYTHON_CFLAGS) +pysss_la_LIBADD = \ + $(PYTHON_BINDINGS_LIBS) \ + $(PYTHON_LIBS) +pysss_la_LDFLAGS = \ + -avoid-version \ + -module +endif + ############ # MANPAGES # ############ diff --git a/server/conf_macros.m4 b/server/conf_macros.m4 index aa2d578f..9ab2003e 100644 --- a/server/conf_macros.m4 +++ b/server/conf_macros.m4 @@ -137,3 +137,20 @@ AC_DEFUN([WITH_KRB5_PLUGIN_PATH], fi AC_SUBST(krb5pluginpath) ]) + +AC_DEFUN([WITH_PYTHON_BINDINGS], + [ AC_ARG_WITH([python-bindings], + [AC_HELP_STRING([--with-python-bindings], + [Whether to build python bindings [yes]] + ) + ], + [], + with_python_bindings=yes + ) + if test x"$with_python_bindings" == xyes; then + HAVE_PYTHON_BINDINGS=1 + AC_SUBST(HAVE_PYTHON_BINDINGS) + fi + AM_CONDITIONAL([BUILD_PYTHON_BINDINGS], [test x"$with_python_bindings" = xyes]) + ]) + diff --git a/server/configure.ac b/server/configure.ac index 3320507a..475bdca6 100644 --- a/server/configure.ac +++ b/server/configure.ac @@ -48,6 +48,7 @@ WITH_SHADOW_UTILS_PATH WITH_MANPAGES WITH_XML_CATALOG WITH_KRB5_PLUGIN_PATH +WITH_PYTHON_BINDINGS m4_include([external/pkg.m4]) m4_include([external/libpopt.m4]) @@ -62,6 +63,7 @@ m4_include([external/krb5.m4]) m4_include([external/libcares.m4]) m4_include([external/docbook.m4]) m4_include([external/sizes.m4]) +m4_include([external/python.m4]) m4_include([util/signal.m4]) PKG_CHECK_MODULES([DBUS],[dbus-1]) @@ -83,6 +85,13 @@ if test x$HAVE_MANPAGES != x; then [Docbook XSL templates]) fi +if test x$HAVE_PYTHON_BINDINGS != x; then + AM_PATH_PYTHON([2.4]) + AM_CHECK_PYTHON_HEADERS([], + AC_MSG_ERROR([Could not find python headers])) + AM_PYTHON_CONFIG +fi + AC_CHECK_HEADERS([sys/inotify.h]) PKG_CHECK_MODULES([CHECK], [check], [have_check=1], [have_check=]) diff --git a/server/external/python.m4 b/server/external/python.m4 new file mode 100644 index 00000000..37eeac24 --- /dev/null +++ b/server/external/python.m4 @@ -0,0 +1,49 @@ +dnl Check for python-config and substitute needed CFLAGS and LDFLAGS +dnl Usage: +dnl AM_PYTHON_CONFIG +AC_DEFUN([AM_PYTHON_CONFIG], +[ AC_SUBST(PYTHON_CFLAGS) + AC_SUBST(PYTHON_LIBS) + + AC_PATH_PROG(PYTHON_CONFIG, python-config) + AC_MSG_CHECKING(for working python-config) + if test -x "$PYTHON_CONFIG"; then + PYTHON_CFLAGS="`$PYTHON_CONFIG --includes`" + PYTHON_CFLAGS=$PYTHON_CFLAGS" -fno-strict-aliasing" + PYTHON_LIBS="`$PYTHON_CONFIG --libs`" + AC_MSG_RESULT([yes]) + else + AC_MSG_ERROR([no. Please install python devel package]) + fi +]) + +dnl Taken from GNOME sources +dnl a macro to check for ability to create python extensions +dnl AM_CHECK_PYTHON_HEADERS([ACTION-IF-POSSIBLE], [ACTION-IF-NOT-POSSIBLE]) +dnl function also defines PYTHON_INCLUDES +AC_DEFUN([AM_CHECK_PYTHON_HEADERS], +[AC_REQUIRE([AM_PATH_PYTHON]) + AC_MSG_CHECKING(for headers required to compile python extensions) + + dnl deduce PYTHON_INCLUDES + py_prefix=`$PYTHON -c "import sys; print sys.prefix"` + py_exec_prefix=`$PYTHON -c "import sys; print sys.exec_prefix"` + PYTHON_INCLUDES="-I${py_prefix}/include/python${PYTHON_VERSION}" + if test "$py_prefix" != "$py_exec_prefix"; then + PYTHON_INCLUDES="$PYTHON_INCLUDES -I${py_exec_prefix}/include/python${PYTHON_VERSION}" + fi + + AC_SUBST(PYTHON_INCLUDES) + + dnl check if the headers exist: + save_CPPFLAGS="$CPPFLAGS" + CPPFLAGS="$CPPFLAGS $PYTHON_INCLUDES" + AC_TRY_CPP([#include ],dnl + [AC_MSG_RESULT([found]) + $1],dnl + [AC_MSG_RESULT([not found]) + $2]) + CPPFLAGS="$save_CPPFLAGS" +]) + + diff --git a/server/python/pysss.c b/server/python/pysss.c new file mode 100644 index 00000000..aab17186 --- /dev/null +++ b/server/python/pysss.c @@ -0,0 +1,1024 @@ +/* + Authors: + Jakub Hrozek + + Copyright (C) 2009 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program 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. If not, see . +*/ + +#include +#include +#include +#include +#include + +#include "util/util.h" +#include "db/sysdb.h" +#include "tools/tools_util.h" +#include "tools/sss_sync_ops.h" + +#define TRANSACTION_WAIT(trs, retval) do { \ + while (!trs->transaction_done) { \ + tevent_loop_once(trs->self->ev); \ + } \ + retval = trs->error; \ + if (retval) { \ + PyErr_SetSssError(retval); \ + goto fail; \ + } \ +} while(0) + +/* + * function taken from samba sources tree as of Aug 20 2009, + * file source4/lib/ldb/pyldb.c + */ +static char **PyList_AsStringList(TALLOC_CTX *mem_ctx, PyObject *list, + const char *paramname) +{ + char **ret; + int i; + + ret = talloc_array(NULL, char *, PyList_Size(list)+1); + for (i = 0; i < PyList_Size(list); i++) { + PyObject *item = PyList_GetItem(list, i); + if (!PyString_Check(item)) { + PyErr_Format(PyExc_TypeError, "%s should be strings", paramname); + return NULL; + } + ret[i] = talloc_strndup(ret, PyString_AsString(item), + PyString_Size(item)); + } + + ret[i] = NULL; + return ret; +} + +/* + * The sss.local object + */ +typedef struct { + PyObject_HEAD + + TALLOC_CTX *mem_ctx; + struct tevent_context *ev; + struct sysdb_ctx *sysdb; + struct confdb_ctx *confdb; + + struct sss_domain_info *local; + + int lock; + int unlock; +} PySssLocalObject; + +/* + * The transaction object + */ +struct py_sss_transaction { + PySssLocalObject *self; + struct ops_ctx *ops; + + struct sysdb_handle *handle; + bool transaction_done; + int error; +}; + +/* + * Error reporting + */ +static void PyErr_SetSssErrorWithMessage(int ret, const char *message) +{ + PyObject *exc = Py_BuildValue(discard_const_p(char, "(is)"), + ret, message); + + PyErr_SetObject(PyExc_IOError, exc); + Py_XDECREF(exc); +} + +static void PyErr_SetSssError(int ret) +{ + PyErr_SetSssErrorWithMessage(ret, strerror(ret)); +} + +/* + * Common init of all methods + */ +struct ops_ctx *init_ctx(PySssLocalObject *self) +{ + struct ops_ctx *ops = NULL; + + ops = talloc_zero(self->mem_ctx, struct ops_ctx); + if (ops == NULL) { + PyErr_NoMemory(); + return NULL; + } + + ops->domain = self->local; + return ops; +} + +/* + * Common transaction finish + */ +static void req_done(struct tevent_req *req) +{ + struct py_sss_transaction *trs = tevent_req_callback_data(req, + struct py_sss_transaction); + + trs->error = sysdb_transaction_commit_recv(req); + trs->transaction_done = true; +} + +/* + * Add a user + */ +static void py_sss_useradd_transaction(struct tevent_req *req); + +PyDoc_STRVAR(py_sss_useradd__doc__, + "Add a user named ``username``.\n\n" + ":param username: name of the user\n\n" + ":param kwargs: Keyword arguments ro customize the operation\n\n" + "* useradd can be customized further with keyword arguments:\n" + " * ``uid``: The UID of the user\n" + " * ``gid``: The GID of the user\n" + " * ``gecos``: The comment string\n" + " * ``homedir``: Home directory\n" + " * ``shell``: Login shell\n" + " * ``groups``: List of groups the user is member of\n"); + + +static PyObject *py_sss_useradd(PySssLocalObject *self, + PyObject *args, + PyObject *kwds) +{ + struct ops_ctx *ops = NULL; + struct py_sss_transaction *trs = NULL; + struct tevent_req *req; + unsigned long uid = 0; + unsigned long gid = 0; + const char *gecos = NULL; + const char *home = NULL; + const char *shell = NULL; + char *username = NULL; + int ret; + const char * const kwlist[] = { "username", "uid", "gid", "gecos", + "homedir", "shell", "groups", NULL }; + PyObject *py_groups = Py_None; + + /* parse arguments */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|kksssO!", + discard_const_p(char *, kwlist), + &username, + &uid, + &gid, + &gecos, + &home, + &shell, + &PyList_Type, + &py_groups)) { + goto fail; + } + + ops = init_ctx(self); + if (!ops) { + return NULL; + } + + if (py_groups != Py_None) { + ops->addgroups = PyList_AsStringList(self->mem_ctx, py_groups, "groups"); + if (!ops->addgroups) { + return NULL; + } + } + + ops->name = username; + ops->uid = uid; + + /* fill in defaults */ + ret = useradd_defaults(self->mem_ctx, + self->confdb, + ops, gecos, + home, shell); + if (ret != EOK) { + PyErr_SetSssError(ret); + goto fail; + } + + /* add the user within a sysdb transaction */ + trs = talloc_zero(self->mem_ctx, struct py_sss_transaction); + if (!trs) { + PyErr_NoMemory(); + return NULL; + } + trs->self = self; + trs->ops = ops; + + req = sysdb_transaction_send(self->mem_ctx, self->ev, self->sysdb); + if (!req) { + DEBUG(1, ("Could not start transaction")); + PyErr_NoMemory(); + goto fail; + } + tevent_req_set_callback(req, py_sss_useradd_transaction, trs); + + TRANSACTION_WAIT(trs, ret); + + talloc_zfree(ops); + talloc_zfree(trs); + Py_RETURN_NONE; + +fail: + talloc_zfree(ops); + talloc_zfree(trs); + return NULL; +} + +static void py_sss_useradd_transaction(struct tevent_req *req) +{ + int ret; + struct py_sss_transaction *trs = tevent_req_callback_data(req, + struct py_sss_transaction); + struct tevent_req *subreq; + + ret = sysdb_transaction_recv(req, trs, &trs->handle); + if (ret) { + tevent_req_error(req, ret); + return; + } + talloc_zfree(req); + + /* useradd */ + ret = useradd(trs->self->mem_ctx, trs->self->ev, + trs->self->sysdb, trs->handle, trs->ops); + if (ret != EOK) { + goto fail; + } + + subreq = sysdb_transaction_commit_send(trs->self->mem_ctx, trs->self->ev, trs->handle); + if (!subreq) { + ret = ENOMEM; + goto fail; + } + tevent_req_set_callback(subreq, req_done, trs); + return; + +fail: + /* free transaction and signal error */ + talloc_zfree(trs->handle); + trs->transaction_done = true; + trs->error = ret; +} + +/* + * Delete a user + */ +static void py_sss_userdel_transaction(struct tevent_req *); + +PyDoc_STRVAR(py_sss_userdel__doc__, + "Remove the user named ``username``.\n\n" + ":param username: Name of user being removed\n"); + +static PyObject *py_sss_userdel(PySssLocalObject *self, + PyObject *args, + PyObject *kwds) +{ + struct ops_ctx *ops = NULL; + struct tevent_req *req; + struct py_sss_transaction *trs = NULL; + char *username = NULL; + int ret; + + if(!PyArg_ParseTuple(args, "s", &username)) { + goto fail; + } + + ops = init_ctx(self); + if (!ops) { + return NULL; + } + + ops->name = username; + + /* delete the user within a sysdb transaction */ + trs = talloc_zero(self->mem_ctx, struct py_sss_transaction); + if (!trs) { + PyErr_NoMemory(); + return NULL; + } + trs->self = self; + trs->ops = ops; + + req = sysdb_transaction_send(self->mem_ctx, self->ev, self->sysdb); + if (!req) { + DEBUG(1, ("Could not start transaction")); + PyErr_NoMemory(); + goto fail; + } + tevent_req_set_callback(req, py_sss_userdel_transaction, trs); + + TRANSACTION_WAIT(trs, ret); + + talloc_zfree(ops); + talloc_zfree(trs); + Py_RETURN_NONE; + +fail: + talloc_zfree(ops); + talloc_zfree(trs); + return NULL; +} + +static void py_sss_userdel_transaction(struct tevent_req *req) +{ + int ret; + struct py_sss_transaction *trs = tevent_req_callback_data(req, + struct py_sss_transaction); + struct tevent_req *subreq; + + ret = sysdb_transaction_recv(req, trs, &trs->handle); + if (ret) { + tevent_req_error(req, ret); + return; + } + talloc_zfree(req); + + /* userdel */ + ret = userdel(trs->self->mem_ctx, trs->self->ev, + trs->self->sysdb, trs->handle, trs->ops); + if (ret != EOK) { + goto fail; + } + + subreq = sysdb_transaction_commit_send(trs->self->mem_ctx, trs->self->ev, trs->handle); + if (!subreq) { + ret = ENOMEM; + goto fail; + } + tevent_req_set_callback(subreq, req_done, trs); + return; + +fail: + /* free transaction and signal error */ + talloc_zfree(trs->handle); + trs->transaction_done = true; + trs->error = ret; +} + +/* + * Modify a user + */ +static void py_sss_usermod_transaction(struct tevent_req *); + +PyDoc_STRVAR(py_sss_usermod__doc__, + "Modify a user.\n\n" + ":param username: Name of user being modified\n\n" + ":param kwargs: Keyword arguments ro customize the operation\n\n" + "* usermod can be customized further with keyword arguments:\n" + " * ``uid``: The UID of the user\n" + " * ``gid``: The GID of the user\n" + " * ``gecos``: The comment string\n" + " * ``homedir``: Home directory\n" + " * ``shell``: Login shell\n" + " * ``addgroups``: List of groups to add the user to\n" + " * ``rmgroups``: List of groups to remove the user from\n" + " * ``lock``: Lock or unlock the account\n"); + +static PyObject *py_sss_usermod(PySssLocalObject *self, + PyObject *args, + PyObject *kwds) +{ + struct ops_ctx *ops = NULL; + struct tevent_req *req; + struct py_sss_transaction *trs = NULL; + int ret; + PyObject *py_addgroups = Py_None; + PyObject *py_rmgroups = Py_None; + unsigned long uid = 0; + unsigned long gid = 0; + char *gecos = NULL; + char *home = NULL; + char *shell = NULL; + char *username = NULL; + unsigned long lock = 0; + const char * const kwlist[] = { "username", "uid", "gid", "lock", + "gecos", "homedir", "shell", + "addgroups", "rmgroups", NULL }; + + /* parse arguments */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|kkksssO!O!", + discard_const_p(char *, kwlist), + &username, + &uid, + &gid, + &lock, + &gecos, + &home, + &shell, + &PyList_Type, + &py_addgroups, + &PyList_Type, + &py_rmgroups)) { + goto fail; + } + + ops = init_ctx(self); + if (!ops) { + return NULL; + } + + if (lock && lock != DO_LOCK && lock != DO_UNLOCK) { + PyErr_SetString(PyExc_ValueError, + "Unkown value for lock parameter"); + goto fail; + } + + if (py_addgroups != Py_None) { + ops->addgroups = PyList_AsStringList(self->mem_ctx, + py_addgroups, + "addgroups"); + if (!ops->addgroups) { + return NULL; + } + } + + if (py_rmgroups != Py_None) { + ops->rmgroups = PyList_AsStringList(self->mem_ctx, + py_rmgroups, + "rmgroups"); + if (!ops->rmgroups) { + return NULL; + } + } + + ops->name = username; + ops->uid = uid; + ops->gid = gid; + ops->gecos = gecos; + ops->home = home; + ops->shell = shell; + ops->lock = lock; + + /* modify the user within a sysdb transaction */ + trs = talloc_zero(self->mem_ctx, struct py_sss_transaction); + if (!trs) { + PyErr_NoMemory(); + return NULL; + } + trs->self = self; + trs->ops = ops; + + req = sysdb_transaction_send(self->mem_ctx, self->ev, self->sysdb); + if (!req) { + DEBUG(1, ("Could not start transaction")); + PyErr_NoMemory(); + goto fail; + } + tevent_req_set_callback(req, py_sss_usermod_transaction, trs); + + TRANSACTION_WAIT(trs, ret); + + talloc_zfree(ops); + talloc_zfree(trs); + Py_RETURN_NONE; + +fail: + talloc_zfree(ops); + talloc_zfree(trs); + return NULL; +} + +static void py_sss_usermod_transaction(struct tevent_req *req) +{ + int ret; + struct py_sss_transaction *trs = tevent_req_callback_data(req, + struct py_sss_transaction); + struct tevent_req *subreq; + + ret = sysdb_transaction_recv(req, trs, &trs->handle); + if (ret) { + tevent_req_error(req, ret); + return; + } + talloc_zfree(req); + /* usermod */ + ret = usermod(trs->self->mem_ctx, trs->self->ev, + trs->self->sysdb, trs->handle, trs->ops); + if (ret != EOK) { + goto fail; + } + + subreq = sysdb_transaction_commit_send(trs->self->mem_ctx, trs->self->ev, trs->handle); + if (!subreq) { + ret = ENOMEM; + goto fail; + } + tevent_req_set_callback(subreq, req_done, trs); + return; + +fail: + /* free transaction and signal error */ + talloc_zfree(trs->handle); + trs->transaction_done = true; + trs->error = ret; +} + +/* + * Add a group + */ +static void py_sss_groupadd_transaction(struct tevent_req *); + +PyDoc_STRVAR(py_sss_groupadd__doc__, + "Add a group.\n\n" + ":param groupname: Name of group being added\n\n" + ":param kwargs: Keyword arguments ro customize the operation\n\n" + "* groupmod can be customized further with keyword arguments:\n" + " * ``gid``: The GID of the group\n"); + +static PyObject *py_sss_groupadd(PySssLocalObject *self, + PyObject *args, + PyObject *kwds) +{ + struct ops_ctx *ops = NULL; + struct tevent_req *req; + struct py_sss_transaction *trs = NULL; + char *groupname; + unsigned long gid = 0; + int ret; + const char * const kwlist[] = { "groupname", "gid", NULL }; + + /* parse arguments */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|k", + discard_const_p(char *, kwlist), + &groupname, + &gid)) { + goto fail; + } + + ops = init_ctx(self); + if (!ops) { + return NULL; + } + + ops->name = groupname; + ops->gid = gid; + + /* add the group within a sysdb transaction */ + trs = talloc_zero(self->mem_ctx, struct py_sss_transaction); + if (!trs) { + PyErr_NoMemory(); + return NULL; + } + trs->self = self; + trs->ops = ops; + + req = sysdb_transaction_send(self->mem_ctx, self->ev, self->sysdb); + if (!req) { + DEBUG(1, ("Could not start transaction")); + PyErr_NoMemory(); + goto fail; + } + tevent_req_set_callback(req, py_sss_groupadd_transaction, trs); + + TRANSACTION_WAIT(trs, ret); + + talloc_zfree(ops); + talloc_zfree(trs); + Py_RETURN_NONE; + +fail: + talloc_zfree(ops); + talloc_zfree(trs); + return NULL; +} + +static void py_sss_groupadd_transaction(struct tevent_req *req) +{ + int ret; + struct py_sss_transaction *trs = tevent_req_callback_data(req, + struct py_sss_transaction); + struct tevent_req *subreq; + + ret = sysdb_transaction_recv(req, trs, &trs->handle); + if (ret) { + tevent_req_error(req, ret); + return; + } + talloc_zfree(req); + + /* groupadd */ + ret = groupadd(trs->self->mem_ctx, trs->self->ev, + trs->self->sysdb, trs->handle, trs->ops); + if (ret != EOK) { + goto fail; + } + + subreq = sysdb_transaction_commit_send(trs->self->mem_ctx, trs->self->ev, trs->handle); + if (!subreq) { + ret = ENOMEM; + goto fail; + } + tevent_req_set_callback(subreq, req_done, trs); + return; + +fail: + /* free transaction and signal error */ + talloc_zfree(trs->handle); + trs->transaction_done = true; + trs->error = ret; +} + +/* + * Delete a group + */ +static void py_sss_groupdel_transaction(struct tevent_req *req); + +PyDoc_STRVAR(py_sss_groupdel__doc__, + "Remove a group.\n\n" + ":param groupname: Name of group being removed\n"); + +static PyObject *py_sss_groupdel(PySssLocalObject *self, + PyObject *args, + PyObject *kwds) +{ + struct ops_ctx *ops = NULL; + struct tevent_req *req; + struct py_sss_transaction *trs = NULL; + char *groupname = NULL; + int ret; + + if(!PyArg_ParseTuple(args, "s", &groupname)) { + goto fail; + } + + ops = init_ctx(self); + if (!ops) { + return NULL; + } + + ops->name = groupname; + + /* delete the group within a sysdb transaction */ + trs = talloc_zero(self->mem_ctx, struct py_sss_transaction); + if (!trs) { + PyErr_NoMemory(); + return NULL; + } + trs->self = self; + trs->ops = ops; + + req = sysdb_transaction_send(self->mem_ctx, self->ev, self->sysdb); + if (!req) { + DEBUG(1, ("Could not start transaction")); + PyErr_NoMemory(); + goto fail; + } + tevent_req_set_callback(req, py_sss_groupdel_transaction, trs); + + TRANSACTION_WAIT(trs, ret); + + talloc_zfree(ops); + talloc_zfree(trs); + Py_RETURN_NONE; + +fail: + talloc_zfree(ops); + talloc_zfree(trs); + return NULL; +} + +static void py_sss_groupdel_transaction(struct tevent_req *req) +{ + int ret; + struct py_sss_transaction *trs = tevent_req_callback_data(req, + struct py_sss_transaction); + struct tevent_req *subreq; + + ret = sysdb_transaction_recv(req, trs, &trs->handle); + if (ret) { + tevent_req_error(req, ret); + return; + } + talloc_zfree(req); + + /* groupdel */ + ret = groupdel(trs->self->mem_ctx, trs->self->ev, + trs->self->sysdb, trs->handle, trs->ops); + if (ret != EOK) { + goto fail; + } + + subreq = sysdb_transaction_commit_send(trs->self->mem_ctx, trs->self->ev, trs->handle); + if (!subreq) { + ret = ENOMEM; + goto fail; + } + tevent_req_set_callback(subreq, req_done, trs); + return; + +fail: + /* free transaction and signal error */ + talloc_zfree(trs->handle); + trs->transaction_done = true; + trs->error = ret; +} + +/* + * Modify a group + */ +static void py_sss_groupmod_transaction(struct tevent_req *); + +PyDoc_STRVAR(py_sss_groupmod__doc__, +"Modify a group.\n\n" +":param groupname: Name of group being modified\n\n" +":param kwargs: Keyword arguments ro customize the operation\n\n" +"* groupmod can be customized further with keyword arguments:\n" +" * ``gid``: The GID of the group\n\n" +" * ``addgroups``: Groups to add the group to\n\n" +" * ``rmgroups``: Groups to remove the group from\n\n"); + +static PyObject *py_sss_groupmod(PySssLocalObject *self, + PyObject *args, + PyObject *kwds) +{ + struct ops_ctx *ops = NULL; + struct tevent_req *req; + struct py_sss_transaction *trs = NULL; + int ret; + PyObject *py_addgroups = Py_None; + PyObject *py_rmgroups = Py_None; + unsigned long gid = 0; + char *groupname = NULL; + const char * const kwlist[] = { "groupname", "gid", "addgroups", + "rmgroups", NULL }; + + /* parse arguments */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|kO!O!", + discard_const_p(char *, kwlist), + &groupname, + &gid, + &PyList_Type, + &py_addgroups, + &PyList_Type, + &py_rmgroups)) { + goto fail; + } + + ops = init_ctx(self); + if (!ops) { + return NULL; + } + + if (py_addgroups != Py_None) { + ops->addgroups = PyList_AsStringList(self->mem_ctx, + py_addgroups, + "addgroups"); + if (!ops->addgroups) { + return NULL; + } + } + + if (py_rmgroups != Py_None) { + ops->rmgroups = PyList_AsStringList(self->mem_ctx, + py_rmgroups, + "rmgroups"); + if (!ops->rmgroups) { + return NULL; + } + } + + ops->name = groupname; + ops->gid = gid; + + /* modify the group within a sysdb transaction */ + trs = talloc_zero(self->mem_ctx, struct py_sss_transaction); + if (!trs) { + PyErr_NoMemory(); + return NULL; + } + trs->self = self; + trs->ops = ops; + + req = sysdb_transaction_send(self->mem_ctx, self->ev, self->sysdb); + if (!req) { + DEBUG(1, ("Could not start transaction")); + PyErr_NoMemory(); + goto fail; + } + tevent_req_set_callback(req, py_sss_groupmod_transaction, trs); + + TRANSACTION_WAIT(trs, ret); + + + talloc_zfree(ops); + talloc_zfree(trs); + Py_RETURN_NONE; + +fail: + talloc_zfree(ops); + talloc_zfree(trs); + return NULL; +} + +static void py_sss_groupmod_transaction(struct tevent_req *req) +{ + int ret; + struct py_sss_transaction *trs = tevent_req_callback_data(req, + struct py_sss_transaction); + struct tevent_req *subreq; + + ret = sysdb_transaction_recv(req, trs, &trs->handle); + if (ret) { + tevent_req_error(req, ret); + return; + } + talloc_zfree(req); + + /* groupmod */ + ret = groupmod(trs->self->mem_ctx, trs->self->ev, + trs->self->sysdb, trs->handle, trs->ops); + if (ret != EOK) { + goto fail; + } + + subreq = sysdb_transaction_commit_send(trs->self->mem_ctx, trs->self->ev, trs->handle); + if (!subreq) { + ret = ENOMEM; + goto fail; + } + tevent_req_set_callback(subreq, req_done, trs); + return; + +fail: + /* free transaction and signal error */ + talloc_zfree(trs->handle); + trs->transaction_done = true; + trs->error = ret; +} + +/*** python plumbing begins here ***/ + +/* + * The sss.local destructor + */ +static void PySssLocalObject_dealloc(PySssLocalObject *self) +{ + talloc_free(self->mem_ctx); + self->ob_type->tp_free((PyObject*) self); +} + +/* + * The sss.local constructor + */ +static PyObject *PySssLocalObject_new(PyTypeObject *type, + PyObject *args, + PyObject *kwds) +{ + TALLOC_CTX *mem_ctx; + PySssLocalObject *self; + char *confdb_path; + int ret; + + mem_ctx = talloc_new(NULL); + if (mem_ctx == NULL) { + PyErr_NoMemory(); + return NULL; + } + + self = (PySssLocalObject *) type->tp_alloc(type, 0); + if (self == NULL) { + talloc_free(mem_ctx); + PyErr_NoMemory(); + return NULL; + } + self->mem_ctx = mem_ctx; + + self->ev = tevent_context_init(mem_ctx); + if (self->ev == NULL) { + talloc_free(mem_ctx); + PyErr_SetSssErrorWithMessage(EIO, "Cannot create event context"); + return NULL; + } + + confdb_path = talloc_asprintf(self->mem_ctx, "%s/%s", DB_PATH, CONFDB_FILE); + if (confdb_path == NULL) { + talloc_free(mem_ctx); + PyErr_NoMemory(); + return NULL; + } + + /* Connect to the conf db */ + ret = confdb_init(self->mem_ctx, &self->confdb, confdb_path); + if (ret != EOK) { + talloc_free(mem_ctx); + PyErr_SetSssErrorWithMessage(ret, + "Could not initialize connection to the confdb\n"); + return NULL; + } + + ret = confdb_get_domain(self->confdb, "local", &self->local); + if (ret != EOK) { + talloc_free(mem_ctx); + PyErr_SetSssErrorWithMessage(ret, "Cannot get local domain"); + return NULL; + } + + /* open 'local' sysdb at default path */ + ret = sysdb_domain_init(self->mem_ctx, self->ev, self->local, DB_PATH, &self->sysdb); + if (ret != EOK) { + talloc_free(mem_ctx); + PyErr_SetSssErrorWithMessage(ret, + "Could not initialize connection to the sysdb\n"); + return NULL; + } + + self->lock = DO_LOCK; + self->unlock = DO_UNLOCK; + + return (PyObject *) self; +} + +/* + * sss.local object methods + */ +static PyMethodDef sss_local_methods[] = { + { "useradd", (PyCFunction) py_sss_useradd, + METH_KEYWORDS, py_sss_useradd__doc__ + }, + { "userdel", (PyCFunction) py_sss_userdel, + METH_VARARGS, py_sss_userdel__doc__ + }, + { "usermod", (PyCFunction) py_sss_usermod, + METH_KEYWORDS, py_sss_usermod__doc__ + }, + { "groupadd", (PyCFunction) py_sss_groupadd, + METH_KEYWORDS, py_sss_groupadd__doc__ + }, + { "groupdel", (PyCFunction) py_sss_groupdel, + METH_KEYWORDS, py_sss_groupdel__doc__ + }, + { "groupmod", (PyCFunction) py_sss_groupmod, + METH_KEYWORDS, py_sss_groupmod__doc__ + }, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +static PyMemberDef sss_members[] = { + { discard_const_p(char, "lock"), T_INT, + offsetof(PySssLocalObject, lock), RO }, + { discard_const_p(char, "unlock"), T_INT, + offsetof(PySssLocalObject, unlock), RO }, + {NULL} /* Sentinel */ +}; + +/* + * sss.local object properties + */ +static PyTypeObject pysss_local_type = { + PyObject_HEAD_INIT(NULL) + .tp_name = "sss.local", + .tp_basicsize = sizeof(PySssLocalObject), + .tp_new = PySssLocalObject_new, + .tp_dealloc = (destructor) PySssLocalObject_dealloc, + .tp_methods = sss_local_methods, + .tp_members = sss_members, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = "SSS DB manipulation", +}; + +/* + * Module methods + */ +static PyMethodDef module_methods[] = { + {NULL} /* Sentinel */ +}; + +/* + * Module initialization + */ +PyMODINIT_FUNC +initpysss(void) +{ + PyObject *m; + + if (PyType_Ready(&pysss_local_type) < 0) + return; + + m = Py_InitModule("pysss", module_methods); + if (m == NULL) + return; + + Py_INCREF(&pysss_local_type); + PyModule_AddObject(m, "local", (PyObject *)&pysss_local_type); +} + diff --git a/server/tests/python-test.py b/server/tests/python-test.py new file mode 100644 index 00000000..fddf9c31 --- /dev/null +++ b/server/tests/python-test.py @@ -0,0 +1,391 @@ +#!/usr/bin/python +#coding=utf-8 + +# Authors: +# Jakub Hrozek +# +# Copyright (C) 2009 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program 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; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import unittest +import commands +import errno + +# module under test +import pysss + +class LocalTest(unittest.TestCase): + local_path = "/var/lib/sss/db/sssd.ldb" + + def setUp(self): + self.local = pysss.local() + + def _run_and_check(self, runme): + (status, output) = commands.getstatusoutput(runme) + self.failUnlessEqual(status, 0, output) + + def _get_object_info(self, name, subtree, domain): + search_dn = "dn=name=%s,cn=%s,cn=%s,cn=sysdb" % (name, subtree, domain) + (status, output) = commands.getstatusoutput("ldbsearch -H %s %s" % (self.local_path,search_dn)) + + if status: return {} + + kw = {} + for key, value in [ l.split(':') for l in output.split('\n') if ":" in l ]: + kw[key] = value.strip() + + del kw['asq'] + return kw + + def get_user_info(self, name, domain="LOCAL"): + return self._get_object_info(name, "users", domain) + + def get_group_info(self, name, domain="LOCAL"): + return self._get_object_info(name, "groups", domain) + + def _validate_object(self, kw, name, **kwargs): + if kw == {}: self.fail("Could not get %s info" % name) + for key in kwargs.keys(): + self.assert_(str(kwargs[key]) == str(kw[key]), "%s %s != %s %s" % (key, kwargs[key], key, kw[key])) + + def validate_user(self, username, **kwargs): + return self._validate_object(self.get_user_info(username), "user", **kwargs) + + def validate_group(self, groupname, **kwargs): + return self._validate_object(self.get_group_info(groupname), "group", **kwargs) + + def _validate_no_object(self, kw, name): + if kw != {}: + self.fail("Got %s info" % name) + + def validate_no_user(self, username): + return self._validate_no_object(self.get_user_info(username), "user") + + def validate_no_group(self, groupname): + return self._validate_no_object(self.get_group_info(groupname), "group") + + def _get_object_membership(self, name, subtree, domain): + search_dn = "dn=name=%s,cn=%s,cn=%s,cn=sysdb" % (name, subtree, domain) + (status, output) = commands.getstatusoutput("ldbsearch -H %s %s" % (self.local_path,search_dn)) + + if status: + return [] + + members = [ value.strip() for key, value in [ l.split(':') for l in output.split('\n') if ":" in l ] if key == "memberof" ] + return members + + def _assertMembership(self, name, group_list, subtree, domain): + members = self._get_object_membership(name, subtree, domain) + for group in group_list: + group_dn = "name=%s,cn=groups,cn=%s,cn=sysdb" % (group, domain) + if group_dn in members: + members.remove(group_dn) + else: + self.fail("Cannot find required group %s" % group_dn) + + if len(members) > 0: + self.fail("More groups than selected") + + def assertUserMembership(self, name, group_list, domain="LOCAL"): + return self._assertMembership(name, group_list, "users", domain) + + def assertGroupMembership(self, name, group_list, domain="LOCAL"): + return self._assertMembership(name, group_list, "groups", domain) + + def get_user_membership(self, name, domain="LOCAL"): + return self._get_object_membership(name, "users", domain) + + def get_group_membership(self, name, domain="LOCAL"): + return self._get_object_membership(name, "groups", domain) + + def add_group(self, groupname): + self._run_and_check("sss_groupadd %s" % (groupname)) + + def remove_group(self, groupname): + self._run_and_check("sss_groupdel %s" % (groupname)) + + def add_user(self, username): + self._run_and_check("sss_useradd %s" % (username)) + + def remove_user(self, username): + self._run_and_check("sss_userdel %s" % (username)) + +class SanityTest(unittest.TestCase): + def testInstantiate(self): + "Test that the local backed binding can be instantiated" + local = pysss.local() + self.assert_(local.__class__, "") + +class UseraddTest(LocalTest): + def tearDown(self): + if self.username: + self.remove_user(self.username) + + def testUseradd(self): + "Test adding a local user" + self.username = "testUseradd" + self.local.useradd(self.username) + self.validate_user(self.username) + + def testUseraddWithParams(self): + "Test adding a local user with modified parameters" + self.username = "testUseraddWithParams" + self.local.useradd(self.username, + gecos="foo bar", + homedir="/people/foobar", + shell="/bin/zsh") + self.validate_user(self.username, + gecos="foo bar", + homeDirectory="/people/foobar", + loginShell="/bin/zsh") + + def testUseraddToGroups(self): + "Test adding a local user with group membership" + self.username = "testUseraddToGroups" + self.add_group("gr1") + self.add_group("gr2") + try: + self.local.useradd(self.username, + groups=["gr1","gr2"]) + self.assertUserMembership(self.username, + ["gr1","gr2"]) + finally: + self.remove_group("gr1") + self.remove_group("gr2") + + def testUseraddWithUID(self): + "Test adding a local user with a custom UID" + self.username = "testUseraddWithUID" + self.local.useradd(self.username, + uid=1024) + self.validate_user(self.username, + uidNumber=1024) + +class UseraddTestNegative(LocalTest): + def testUseraddNoParams(self): + "Test that local.useradd() requires the username parameter" + self.assertRaises(TypeError, self.local.useradd) + + def testUseraddUserAlreadyExists(self): + "Test adding a local with a duplicite name" + self.username = "testUseraddUserAlreadyExists" + self.local.useradd(self.username) + try: + self.local.useradd(self.username) + except IOError, e: + self.assertEquals(e.errno, errno.EEXIST) + else: + self.fail("Was expecting exception") + finally: + self.remove_user(self.username) + + def testUseraddUIDAlreadyExists(self): + "Test adding a local with a duplicite user ID" + self.username = "testUseraddUIDAlreadyExists1" + self.local.useradd(self.username, uid=1025) + try: + self.local.useradd("testUseraddUIDAlreadyExists2", uid=1025) + except IOError, e: + self.assertEquals(e.errno, errno.EEXIST) + else: + self.fail("Was expecting exception") + finally: + self.remove_user(self.username) + +class UserdelTest(LocalTest): + def testUserdel(self): + self.add_user("testUserdel") + self.validate_user("testUserdel") + self.local.userdel("testUserdel") + self.validate_no_user("testUserdel") + + def testUserdelNegative(self): + self.validate_no_user("testUserdelNegative") + try: + self.local.userdel("testUserdelNegative") + except IOError, e: + self.assertEquals(e.errno, errno.ENOENT) + else: + fail("Was expecting exception") + +class UsermodTest(LocalTest): + def setUp(self): + self.local = pysss.local() + self.username = "UsermodTest" + self.add_user(self.username) + + def tearDown(self): + self.remove_user(self.username) + + def testUsermod(self): + "Test modifying user attributes" + self.local.usermod(self.username, + gecos="foo bar", + homedir="/people/foobar", + shell="/bin/zsh") + self.validate_user(self.username, + gecos="foo bar", + homeDirectory="/people/foobar", + loginShell="/bin/zsh") + + def testUsermodUID(self): + "Test modifying UID" + self.local.usermod(self.username, + uid=1024) + self.validate_user(self.username, + uidNumber=1024) + + def testUsermodGroupMembership(self): + "Test adding to and removing from groups" + self.add_group("gr1") + self.add_group("gr2") + + try: + self.local.usermod(self.username, + addgroups=["gr1","gr2"]) + self.assertUserMembership(self.username, + ["gr1","gr2"]) + self.local.usermod(self.username, + rmgroups=["gr2"]) + self.assertUserMembership(self.username, + ["gr1"]) + self.local.usermod(self.username, + rmgroups=["gr1"]) + self.assertUserMembership(self.username, + []) + finally: + self.remove_group("gr1") + self.remove_group("gr2") + + def testUsermodLockUnlock(self): + "Test locking and unlocking user" + self.local.usermod(self.username, + lock=self.local.lock) + self.validate_user(self.username, + disabled="true") + self.local.usermod(self.username, + lock=self.local.unlock) + self.validate_user(self.username, + disabled="false") + +class GroupaddTest(LocalTest): + def tearDown(self): + if self.groupname: + self.remove_group(self.groupname) + + def testGroupadd(self): + "Test adding a local group" + self.groupname = "testGroupadd" + self.local.groupadd(self.groupname) + self.validate_group(self.groupname) + + def testGroupaddWithGID(self): + "Test adding a local group with a custom GID" + self.groupname = "testUseraddWithGID" + self.local.groupadd(self.groupname, + gid=1024) + self.validate_group(self.groupname, + gidNumber=1024) + +class GroupaddTestNegative(LocalTest): + def testGroupaddNoParams(self): + "Test that local.groupadd() requires the groupname parameter" + self.assertRaises(TypeError, self.local.groupadd) + + def testGroupaddUserAlreadyExists(self): + "Test adding a local with a duplicite name" + self.groupname = "testGroupaddUserAlreadyExists" + self.local.groupadd(self.groupname) + try: + self.local.groupadd(self.groupname) + except IOError, e: + self.assertEquals(e.errno, errno.EEXIST) + else: + self.fail("Was expecting exception") + finally: + self.remove_group(self.groupname) + + def testGroupaddGIDAlreadyExists(self): + "Test adding a local with a duplicite group ID" + self.groupname = "testGroupaddGIDAlreadyExists1" + self.local.groupadd(self.groupname, gid=1025) + try: + self.local.groupadd("testGroupaddGIDAlreadyExists2", gid=1025) + except IOError, e: + self.assertEquals(e.errno, errno.EEXIST) + else: + self.fail("Was expecting exception") + finally: + self.remove_group(self.groupname) + +class GroupdelTest(LocalTest): + def testGroupdel(self): + self.add_group("testGroupdel") + self.validate_group("testGroupdel") + self.local.groupdel("testGroupdel") + self.validate_no_group("testGroupdel") + + def testGroupdelNegative(self): + self.validate_no_group("testGroupdelNegative") + try: + self.local.groupdel("testGroupdelNegative") + except IOError, e: + self.assertEquals(e.errno, errno.ENOENT) + else: + fail("Was expecting exception") + + +class GroupmodTest(LocalTest): + def setUp(self): + self.local = pysss.local() + self.groupname = "GroupmodTest" + self.add_group(self.groupname) + + def tearDown(self): + self.remove_group(self.groupname) + + def testGroupmodGID(self): + "Test modifying UID" + self.local.groupmod(self.groupname, + gid=1024) + self.validate_group(self.groupname, + gidNumber=1024) + + def testGroupmodGroupMembership(self): + "Test adding to groups" + self.add_group("gr1") + self.add_group("gr2") + try: + self.local.groupmod(self.groupname, + addgroups=["gr1","gr2"]) + self.assertGroupMembership(self.groupname, + ["gr1","gr2"]) + self.local.groupmod(self.groupname, + rmgroups=["gr2"]) + self.assertGroupMembership(self.groupname, + ["gr1"]) + self.local.groupmod(self.groupname, + rmgroups=["gr1"]) + self.assertGroupMembership(self.groupname, + []) + finally: + self.remove_group("gr1") + self.remove_group("gr2") + +# -------------- run the test suite -------------- # +if __name__ == "__main__": + unittest.main() + diff --git a/server/tools/tools_util.h b/server/tools/tools_util.h index b6509c22..c63b9033 100644 --- a/server/tools/tools_util.h +++ b/server/tools/tools_util.h @@ -23,6 +23,8 @@ #ifndef __TOOLS_UTIL_H__ #define __TOOLS_UTIL_H__ +#include + #include "util/sssd-i18n.h" #include "tools/sss_sync_ops.h" diff --git a/server/util/util.h b/server/util/util.h index dda7527e..ea7f44e8 100644 --- a/server/util/util.h +++ b/server/util/util.h @@ -104,6 +104,14 @@ void debug_fn(const char *format, ...); #define talloc_zfree(ptr) do { talloc_free(ptr); ptr = NULL; } while(0) #endif +#ifndef discard_const_p +#if defined(__intptr_t_defined) || defined(HAVE_INTPTR_T) +# define discard_const_p(type, ptr) ((type *)((intptr_t)(ptr))) +#else +# define discard_const_p(type, ptr) ((type *)(ptr)) +#endif +#endif + /* TODO: remove later * These functions are available in the latest tevent and are the ones that * should be used as tevent_req is rightfully opaque there */ -- cgit