aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakub Hrozek <jakub.hrozek@posteo.se>2015-11-04 16:59:41 +0100
committerAndreas Schneider <asn@samba.org>2015-12-10 13:31:20 +0100
commitcdbc3389480f610341d244f648cc5a3a2f23c67c (patch)
treed5fc9b15777863f4bbf423a6f8abc962dfd30ca5
parent40763faa1c51aab94a16c280dc3702353a3f889b (diff)
downloadpam_wrapper-cdbc3389480f610341d244f648cc5a3a2f23c67c.tar.gz
pam_wrapper-cdbc3389480f610341d244f648cc5a3a2f23c67c.tar.xz
pam_wrapper-cdbc3389480f610341d244f648cc5a3a2f23c67c.zip
libpamtest: Add Python bindings
So far only for Python3 Pair-Programmed-With: Andreas Schneider <asn@samba.org>
-rw-r--r--.travis.yml2
-rw-r--r--CMakeLists.txt1
-rw-r--r--include/libpamtest.h2
-rw-r--r--src/CMakeLists.txt27
-rw-r--r--src/python/CMakeLists.txt14
-rw-r--r--src/python/pypamtest.c1044
-rw-r--r--tests/CMakeLists.txt18
-rwxr-xr-xtests/pypamtest_test.py159
-rw-r--r--tests/services/matrix.in9
-rw-r--r--tests/services/matrix_opt.in2
-rw-r--r--tests/services/matrix_py.in2
11 files changed, 1245 insertions, 35 deletions
diff --git a/.travis.yml b/.travis.yml
index 46342ed..4b3a65e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,7 +3,7 @@ compiler:
- gcc
before_install:
- sudo apt-get update -qq
- - sudo apt-get install build-essential gcc make cmake libpam0g-dev git
+ - sudo apt-get install build-essential gcc make cmake libpam0g-dev git python-dev
- pip install --user cpp-coveralls
script:
- git clone https://git.cryptomilk.org/projects/cmocka.git/
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4623409..7033301 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -42,6 +42,7 @@ macro_ensure_out_of_source_build("${PROJECT_NAME} requires an out of source buil
# Find out if we have threading available
set(CMAKE_THREAD_PREFER_PTHREADS ON)
find_package(Threads)
+find_package(PythonLibs)
# config.h checks
include(ConfigureChecks.cmake)
diff --git a/include/libpamtest.h b/include/libpamtest.h
index 85007f5..0307a26 100644
--- a/include/libpamtest.h
+++ b/include/libpamtest.h
@@ -244,7 +244,7 @@ const struct pam_testcase *pamtest_failed_case(struct pam_testcase *test_cases);
*
* @param[in] perr libpamtest error code
*
- * @return String representation of the perr argument
+ * @return String representation of the perr argument. Never returns NULL.
*/
const char *pamtest_strerror(enum pamtest_err perr);
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 7afb598..fae6dfa 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -45,29 +45,10 @@ install(
ARCHIVE DESTINATION ${LIB_INSTALL_DIR}
)
-set(PAM_MODULES pam_matrix pam_get_items pam_set_items)
-
-set(PAM_LIBRARIES pam)
-if (HAVE_PAM_MISC)
- list(APPEND PAM_LIBRARIES pam_misc)
-endif (HAVE_PAM_MISC)
-
-set(PWRAP_PRIVATE_LIBRARIES
- ${LIB_INSTALL_DIR}/pam_wrapper)
-
-foreach(_PAM_MODULE ${PAM_MODULES})
- add_library(${_PAM_MODULE} MODULE modules/${_PAM_MODULE}.c)
- set_property(TARGET ${_PAM_MODULE} PROPERTY PREFIX "")
-
- target_link_libraries(${_PAM_MODULE}
- ${PAM_LIBRARIES})
-
- install(
- TARGETS
- ${_PAM_MODULE}
- LIBRARY DESTINATION ${PWRAP_PRIVATE_LIBRARIES}
- ARCHIVE DESTINATION ${PWRAP_PRIVATE_LIBRARIES})
-endforeach()
+add_subdirectory(modules)
+if (PYTHONLIBS_FOUND)
+ add_subdirectory(python)
+endif()
# This needs to be at the end
if (POLICY CMP0026)
diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt
new file mode 100644
index 0000000..75bd16f
--- /dev/null
+++ b/src/python/CMakeLists.txt
@@ -0,0 +1,14 @@
+project(pypamtest)
+
+include_directories(${CMAKE_BINARY_DIR})
+include_directories(${pam_wrapper-headers_DIR})
+include_directories(${PYTHON_INCLUDE_DIR})
+
+add_library(pypamtest MODULE pypamtest.c)
+target_link_libraries(pypamtest pamtest pam ${PYTHON_LIBRARY})
+
+set_target_properties(
+ pypamtest
+ PROPERTIES
+ PREFIX "")
+
diff --git a/src/python/pypamtest.c b/src/python/pypamtest.c
new file mode 100644
index 0000000..72b2de3
--- /dev/null
+++ b/src/python/pypamtest.c
@@ -0,0 +1,1044 @@
+/*
+ * Copyright (c) 2015 Andreas Schneider <asn@samba.org>
+ * Copyright (c) 2015 Jakub Hrozek <jakub.hrozek@posteo.se>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <Python.h>
+#include <structmember.h>
+
+#include "libpamtest.h"
+
+#define PYTHON_MODULE_NAME "pypamtest"
+
+#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
+
+#define __unused __attribute__((__unused__))
+
+#if PY_MAJOR_VERSION >= 3
+#define IS_PYTHON3 1
+#define RETURN_ON_ERROR return NULL
+#else
+#define RETURN_ON_ERROR return
+#endif /* PY_MAJOR_VERSION */
+
+/* We only return up to 16 messages from the PAM conversation */
+#define PAM_CONV_MSG_MAX 16
+
+/**********************************************************
+ *** module-specific exceptions
+ **********************************************************/
+static PyObject *PyExc_PamTestError;
+
+/**********************************************************
+ *** helper functions
+ **********************************************************/
+
+static const char *repr_fmt = "{ pam_operation [%d] "
+ "expected_rv [%d] "
+ "flags [%d] }";
+
+static char *py_strdup(const char *string)
+{
+ char *copy;
+
+ copy = PyMem_New(char, strlen(string) + 1);
+ if (copy == NULL) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+
+ return strcpy(copy, string);
+}
+
+static PyObject *get_utf8_string(PyObject *obj,
+ const char *attrname)
+{
+ const char *a = attrname ? attrname : "attribute";
+ PyObject *obj_utf8 = NULL;
+
+ if (PyBytes_Check(obj)) {
+ obj_utf8 = obj;
+ Py_INCREF(obj_utf8); /* Make sure we can DECREF later */
+ } else if (PyUnicode_Check(obj)) {
+ if ((obj_utf8 = PyUnicode_AsUTF8String(obj)) == NULL) {
+ return NULL;
+ }
+ } else {
+ PyErr_Format(PyExc_TypeError, "%s must be a string", a);
+ return NULL;
+ }
+
+ return obj_utf8;
+}
+
+static void free_cstring_list(const char **list)
+{
+ int i;
+
+ if (list == NULL) {
+ return;
+ }
+
+ for (i=0; list[i]; i++) {
+ PyMem_Free(discard_const_p(char, list[i]));
+ }
+ PyMem_Free(list);
+}
+
+static void free_string_list(char **list)
+{
+ int i;
+
+ if (list == NULL) {
+ return;
+ }
+
+ for (i=0; list[i]; i++) {
+ PyMem_Free(list[i]);
+ }
+ PyMem_Free(list);
+}
+
+static char **new_conv_list(const int list_size)
+{
+ char **list;
+
+ list = PyMem_New(char *, list_size + 1);
+ if (list == NULL) {
+ return NULL;
+ }
+ list[list_size] = NULL;
+
+ for (int i =0; i < list_size; i++) {
+ list[i] = PyMem_New(char, PAM_MAX_MSG_SIZE);
+ if (list[i] == NULL) {
+ PyMem_Free(list);
+ return NULL;
+ }
+ memset(list[i], 0, PAM_MAX_MSG_SIZE);
+ }
+
+ return list;
+}
+
+static const char **sequence_as_string_list(PyObject *seq,
+ const char *paramname)
+{
+ const char *p = paramname ? paramname : "attribute values";
+ const char **ret;
+ PyObject *utf_item;
+ int i;
+ Py_ssize_t len;
+ PyObject *item;
+
+ if (!PySequence_Check(seq)) {
+ PyErr_Format(PyExc_TypeError,
+ "The object must be a sequence\n");
+ return NULL;
+ }
+
+ len = PySequence_Size(seq);
+ if (len == -1) {
+ return NULL;
+ }
+
+ ret = PyMem_New(const char *, (len + 1));
+ if (!ret) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+
+ for (i = 0; i < len; i++) {
+ item = PySequence_GetItem(seq, i);
+ if (item == NULL) {
+ break;
+ }
+
+ utf_item = get_utf8_string(item, p);
+ if (utf_item == NULL) {
+ Py_DECREF(item);
+ return NULL;
+ }
+
+ ret[i] = py_strdup(PyBytes_AsString(utf_item));
+ Py_DECREF(utf_item);
+ if (!ret[i]) {
+ Py_DECREF(item);
+ return NULL;
+ }
+ Py_DECREF(item);
+ }
+
+ ret[i] = NULL;
+ return ret;
+}
+
+static PyObject *string_list_as_tuple(char **str_list)
+{
+ int rc;
+ size_t len, i;
+ PyObject *tup;
+ PyObject *py_str;
+
+ for (len=0; len < PAM_CONV_MSG_MAX; len++) {
+ if (str_list[len][0] == '\0') {
+ /* unused string, stop counting */
+ break;
+ }
+ }
+
+ tup = PyTuple_New(len);
+ if (tup == NULL) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+
+ for (i = 0; i < len; i++) {
+ py_str = PyUnicode_FromString(str_list[i]);
+ if (py_str == NULL) {
+ Py_DECREF(tup);
+ PyErr_NoMemory();
+ return NULL;
+ }
+
+ /* PyTuple_SetItem() steals the reference to
+ * py_str, so it's enough to decref the tuple
+ * pointer afterwards */
+ rc = PyTuple_SetItem(tup, i, py_str);
+ if (rc != 0) {
+ /* cleanup */
+ Py_DECREF(py_str);
+ Py_DECREF(tup);
+ PyErr_NoMemory();
+ return NULL;
+ }
+ }
+
+ return tup;
+}
+
+static void
+set_pypamtest_exception(PyObject *exc,
+ enum pamtest_err perr,
+ struct pam_testcase *tests,
+ size_t num_tests)
+{
+ PyObject *obj = NULL;
+ /* repr_fmt is fixed and contains just %d expansions, so this is safe */
+ char test_repr[256] = { '\0' };
+ const char *strerr;
+ const struct pam_testcase *failed;
+
+ strerr = pamtest_strerror(perr);
+
+ if (perr == PAMTEST_ERR_CASE) {
+ failed = _pamtest_failed_case(tests, num_tests);
+ if (failed) {
+ snprintf(test_repr, sizeof(test_repr), repr_fmt,
+ failed->pam_operation,
+ failed->expected_rv,
+ failed->flags);
+ }
+ }
+
+ if (test_repr[0] != '\0') {
+ PyErr_Format(exc,
+ "Error [%d]: Test case %s retured [%d]",
+ perr, test_repr, failed->op_rv);
+ } else {
+ obj = Py_BuildValue(discard_const_p(char, "(i,s)"),
+ perr,
+ strerr ? strerr : "Unknown error");
+ PyErr_SetObject(exc, obj);
+ }
+
+ Py_XDECREF(test_repr);
+ Py_XDECREF(obj);
+}
+
+PyMODINIT_FUNC PyInit_pypamtest(void);
+
+typedef struct {
+ PyObject_HEAD
+
+ enum pamtest_ops pam_operation;
+ int expected_rv;
+ int flags;
+} TestCaseObject;
+
+/* Returned when doc(test_case) is invoked */
+PyDoc_STRVAR(TestCaseObject__doc__,
+"pamtest test case\n\n"
+"Represents one operation in PAM transaction. An example is authentication, "
+"opening a session or password change. Each operation has an expected error "
+"code. The run_pamtest() function accepts a list of these test case objects\n"
+"Params:\n\n"
+"pam_operation: - the PAM operation to run. Use constants from pypamtest "
+"such as pypamtest.PAMTEST_AUTHENTICATE. This argument is required.\n"
+"expected_rv: - The PAM return value we expect the operation to return. "
+"Defaults to 0 (PAM_SUCCESS)\n"
+"flags: - Additional flags to pass to the PAM operation. Defaults to 0.\n"
+);
+
+static PyObject *
+TestCase_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+ TestCaseObject *self;
+
+ (void) args; /* unused */
+ (void) kwds; /* unused */
+
+ self = (TestCaseObject *)type->tp_alloc(type, 0);
+ if (self == NULL) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+
+ return (PyObject *) self;
+}
+
+/* The traverse and clear methods must be defined even though they do nothing
+ * otherwise Garbage Collector is not happy
+ */
+static int TestCase_clear(TestCaseObject *self)
+{
+ (void) self; /* unused */
+
+ return 0;
+}
+
+static void TestCase_dealloc(TestCaseObject *self)
+{
+ Py_TYPE(self)->tp_free((PyObject *)self);
+}
+
+static int TestCase_traverse(TestCaseObject *self,
+ visitproc visit,
+ void *arg)
+{
+ (void) self; /* unused */
+ (void) visit; /* unused */
+ (void) arg; /* unused */
+
+ return 0;
+}
+
+static int TestCase_init(TestCaseObject *self,
+ PyObject *args,
+ PyObject *kwargs)
+{
+ const char * const kwlist[] = { "pam_operation",
+ "expected_rv",
+ "flags",
+ NULL };
+ int pam_operation = -1;
+ int expected_rv = PAM_SUCCESS;
+ int flags = 0;
+ int ok;
+
+ ok = PyArg_ParseTupleAndKeywords(args,
+ kwargs,
+ "i|ii",
+ discard_const_p(char *, kwlist),
+ &pam_operation,
+ &expected_rv,
+ &flags);
+ if (!ok) {
+ return -1;
+ }
+
+ switch (pam_operation) {
+ case PAMTEST_AUTHENTICATE:
+ case PAMTEST_SETCRED:
+ case PAMTEST_ACCOUNT:
+ case PAMTEST_OPEN_SESSION:
+ case PAMTEST_CLOSE_SESSION:
+ case PAMTEST_CHAUTHTOK:
+ case PAMTEST_GETENVLIST:
+ case PAMTEST_KEEPHANDLE:
+ break;
+ default:
+ PyErr_Format(PyExc_ValueError,
+ "Unsupported PAM operation %d",
+ pam_operation);
+ return -1;
+ }
+
+ self->flags = flags;
+ self->expected_rv = expected_rv;
+ self->pam_operation = pam_operation;
+
+ return 0;
+}
+
+/*
+ * This function returns string representation of the object, but one that
+ * can be parsed by a machine.
+ *
+ * str() is also string represtentation, but just human-readable.
+ */
+static PyObject *TestCase_repr(TestCaseObject *self)
+{
+ return PyUnicode_FromFormat(repr_fmt,
+ self->pam_operation,
+ self->expected_rv,
+ self->flags);
+}
+
+static PyMemberDef pypamtest_test_case_members[] = {
+ {
+ discard_const_p(char, "pam_operation"),
+ T_INT,
+ offsetof(TestCaseObject, pam_operation),
+ READONLY,
+ discard_const_p(char, "The PAM operation to run"),
+ },
+
+ {
+ discard_const_p(char, "expected_rv"),
+ T_INT,
+ offsetof(TestCaseObject, expected_rv),
+ READONLY,
+ discard_const_p(char, "The expected PAM return code"),
+ },
+
+ {
+ discard_const_p(char, "flags"),
+ T_INT,
+ offsetof(TestCaseObject, flags),
+ READONLY,
+ discard_const_p(char, "Additional flags for the PAM operation"),
+ },
+
+ { NULL, 0, 0, 0, NULL } /* Sentinel */
+};
+
+static PyTypeObject pypamtest_test_case = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ .tp_name = "pypamtest.TestCase",
+ .tp_basicsize = sizeof(TestCaseObject),
+ .tp_new = TestCase_new,
+ .tp_dealloc = (destructor) TestCase_dealloc,
+ .tp_traverse = (traverseproc) TestCase_traverse,
+ .tp_clear = (inquiry) TestCase_clear,
+ .tp_init = (initproc) TestCase_init,
+ .tp_repr = (reprfunc) TestCase_repr,
+ .tp_members = pypamtest_test_case_members,
+ .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
+ .tp_doc = TestCaseObject__doc__
+};
+
+PyDoc_STRVAR(TestResultObject__doc__,
+"pamtest test result\n\n"
+"The test result object is returned from run_pamtest on success. It contains"
+"two lists of strings (up to 16 strings each) which contain the info and error"
+"messages the PAM conversation printed\n\n"
+"Attributes:\n"
+"errors: PAM_ERROR_MSG-level messages printed during the PAM conversation\n"
+"info: PAM_TEXT_INFO-level messages printed during the PAM conversation\n"
+);
+
+typedef struct {
+ PyObject_HEAD
+
+ PyObject *info_msg_list;
+ PyObject *error_msg_list;
+} TestResultObject;
+
+static PyObject *
+TestResult_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+ TestResultObject *self;
+
+ (void) args; /* unused */
+ (void) kwds; /* unused */
+
+ self = (TestResultObject *)type->tp_alloc(type, 0);
+ if (self == NULL) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+
+ return (PyObject *) self;
+}
+
+static int TestResult_clear(TestResultObject *self)
+{
+ (void) self; /* unused */
+
+ return 0;
+}
+
+static void TestResult_dealloc(TestResultObject *self)
+{
+ Py_TYPE(self)->tp_free((PyObject *)self);
+}
+
+static int TestResult_traverse(TestResultObject *self,
+ visitproc visit,
+ void *arg)
+{
+ (void) self; /* unused */
+ (void) visit; /* unused */
+ (void) arg; /* unused */
+
+ return 0;
+}
+
+static int TestResult_init(TestResultObject *self,
+ PyObject *args,
+ PyObject *kwargs)
+{
+ const char * const kwlist[] = { "info_msg_list",
+ "error_msg_list",
+ NULL };
+ int ok;
+ PyObject *py_info_list = NULL;
+ PyObject *py_err_list = NULL;
+
+ ok = PyArg_ParseTupleAndKeywords(args,
+ kwargs,
+ "|OO",
+ discard_const_p(char *, kwlist),
+ &py_info_list,
+ &py_err_list);
+ if (!ok) {
+ return -1;
+ }
+
+ if (py_info_list) {
+ ok = PySequence_Check(py_info_list);
+ if (!ok) {
+ PyErr_Format(PyExc_TypeError,
+ "List of info messages must be a sequence\n");
+ return -1;
+ }
+
+ self->info_msg_list = py_info_list;
+ Py_XINCREF(py_info_list);
+ } else {
+ self->info_msg_list = PyList_New(0);
+ if (self->info_msg_list == NULL) {
+ PyErr_NoMemory();
+ return -1;
+ }
+ }
+
+ if (py_err_list) {
+ ok = PySequence_Check(py_err_list);
+ if (!ok) {
+ PyErr_Format(PyExc_TypeError,
+ "List of error messages must be a sequence\n");
+ return -1;
+ }
+
+ self->error_msg_list = py_err_list;
+ Py_XINCREF(py_err_list);
+ } else {
+ self->error_msg_list = PyList_New(0);
+ if (self->error_msg_list == NULL) {
+ PyErr_NoMemory();
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+static PyObject *test_result_list_concat(PyObject *list,
+ const char delim_pre,
+ const char delim_post)
+{
+ PyObject *res;
+ PyObject *item;
+ Py_ssize_t size;
+ Py_ssize_t i;
+
+ res = PyUnicode_FromString("");
+ if (res == NULL) {
+ return NULL;
+ }
+
+ size = PySequence_Size(list);
+
+ for (i=0; i < size; i++) {
+ item = PySequence_GetItem(list, i);
+ if (item == NULL) {
+ PyMem_Free(res);
+ return NULL;
+ }
+
+ res = PyUnicode_FromFormat("%U%c%U%c",
+ res, delim_pre, item, delim_post);
+ Py_XDECREF(item);
+ if (item == NULL) {
+ PyMem_Free(res);
+ return NULL;
+ }
+ }
+
+ return res;
+}
+
+static PyObject *TestResult_repr(TestResultObject *self)
+{
+ PyObject *u_info = NULL;
+ PyObject *u_error = NULL;
+ PyObject *res = NULL;
+
+ u_info = test_result_list_concat(self->info_msg_list, '{', '}');
+ u_error = test_result_list_concat(self->info_msg_list, '{', '}');
+ if (u_info == NULL || u_error == NULL) {
+ Py_XDECREF(u_error);
+ Py_XDECREF(u_info);
+ return NULL;
+ }
+
+ res = PyUnicode_FromFormat("{ errors: { %U } infos: { %U } }",
+ u_info, u_error);
+ Py_DECREF(u_error);
+ Py_DECREF(u_info);
+ return res;
+}
+
+static PyMemberDef pypamtest_test_result_members[] = {
+ {
+ discard_const_p(char, "errors"),
+ T_OBJECT_EX,
+ offsetof(TestResultObject, error_msg_list),
+ READONLY,
+ discard_const_p(char,
+ "List of error messages from PAM conversation"),
+ },
+
+ {
+ discard_const_p(char, "info"),
+ T_OBJECT_EX,
+ offsetof(TestResultObject, info_msg_list),
+ READONLY,
+ discard_const_p(char,
+ "List of info messages from PAM conversation"),
+ },
+
+ { NULL, 0, 0, 0, NULL } /* Sentinel */
+};
+
+static PyTypeObject pypamtest_test_result = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ .tp_name = "pypamtest.TestResult",
+ .tp_basicsize = sizeof(TestResultObject),
+ .tp_new = TestResult_new,
+ .tp_dealloc = (destructor) TestResult_dealloc,
+ .tp_traverse = (traverseproc) TestResult_traverse,
+ .tp_clear = (inquiry) TestResult_clear,
+ .tp_init = (initproc) TestResult_init,
+ .tp_repr = (reprfunc) TestResult_repr,
+ .tp_members = pypamtest_test_result_members,
+ .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
+ .tp_doc = TestResultObject__doc__
+};
+
+/**********************************************************
+ *** Methods of the module
+ **********************************************************/
+
+static TestResultObject *construct_test_conv_result(char **msg_info, char **msg_err)
+{
+ PyObject *py_msg_info = NULL;
+ PyObject *py_msg_err = NULL;
+ TestResultObject *result = NULL;
+ PyObject *result_args = NULL;
+ int rc;
+
+ py_msg_info = string_list_as_tuple(msg_info);
+ py_msg_err = string_list_as_tuple(msg_err);
+ if (py_msg_info == NULL || py_msg_err == NULL) {
+ /* The exception is raised in string_list_as_tuple() */
+ Py_XDECREF(py_msg_err);
+ Py_XDECREF(py_msg_info);
+ return NULL;
+ }
+
+ result = (TestResultObject *) TestResult_new(&pypamtest_test_result,
+ NULL,
+ NULL);
+ if (result == NULL) {
+ /* The exception is raised in TestResult_new */
+ Py_XDECREF(py_msg_err);
+ Py_XDECREF(py_msg_info);
+ return NULL;
+ }
+
+ result_args = PyTuple_New(2);
+ if (result_args == NULL) {
+ /* The exception is raised in TestResult_new */
+ Py_XDECREF(result);
+ Py_XDECREF(py_msg_err);
+ Py_XDECREF(py_msg_info);
+ return NULL;
+ }
+
+ /* Brand new tuples with fixed size don't need error checking */
+ PyTuple_SET_ITEM(result_args, 0, py_msg_info);
+ PyTuple_SET_ITEM(result_args, 1, py_msg_err);
+
+ rc = TestResult_init(result, result_args, NULL);
+ Py_XDECREF(result_args);
+ if (rc != 0) {
+ Py_XDECREF(result);
+ return NULL;
+ }
+
+ return result;
+}
+
+static int py_testcase_get(PyObject *py_test,
+ const char *member_name,
+ long *_value)
+{
+ PyObject* item = NULL;
+
+ /*
+ * PyPyObject_GetAttrString() increases the refcount on the
+ * returned value.
+ */
+ item = PyObject_GetAttrString(py_test, member_name);
+ if (item == NULL) {
+ return EINVAL;
+ }
+
+ *_value = PyLong_AsLong(item);
+ Py_DECREF(item);
+
+ return 0;
+}
+
+static int py_testcase_to_cstruct(PyObject *py_test, struct pam_testcase *test)
+{
+ int rc;
+ long value;
+
+ rc = py_testcase_get(py_test, "pam_operation", &value);
+ if (rc != 0) {
+ return rc;
+ }
+ test->pam_operation = value;
+
+ rc = py_testcase_get(py_test, "expected_rv", &value);
+ if (rc != 0) {
+ return rc;
+ }
+ test->expected_rv = value;
+
+ rc = py_testcase_get(py_test, "flags", &value);
+ if (rc != 0) {
+ return rc;
+ }
+ test->flags = value;
+
+ return 0;
+}
+
+static void free_conv_data(struct pamtest_conv_data *conv_data)
+{
+ if (conv_data == NULL) {
+ return;
+ }
+
+ free_string_list(conv_data->out_err);
+ free_string_list(conv_data->out_info);
+ free_cstring_list(conv_data->in_echo_on);
+ free_cstring_list(conv_data->in_echo_off);
+}
+
+/* conv_data must be a pointer to allocated conv_data structure.
+ *
+ * Use free_conv_data() to free the contents.
+ */
+static int fill_conv_data(PyObject *py_echo_off,
+ PyObject *py_echo_on,
+ struct pamtest_conv_data *conv_data)
+{
+ conv_data->in_echo_on = NULL;
+ conv_data->in_echo_off = NULL;
+ conv_data->out_err = NULL;
+ conv_data->out_info = NULL;
+
+ if (py_echo_off != NULL) {
+ conv_data->in_echo_off = sequence_as_string_list(py_echo_off,
+ "echo_off");
+ if (conv_data->in_echo_off == NULL) {
+ free_conv_data(conv_data);
+ return ENOMEM;
+ }
+ }
+
+ if (py_echo_on != NULL) {
+ conv_data->in_echo_on = sequence_as_string_list(py_echo_on,
+ "echo_on");
+ if (conv_data->in_echo_on == NULL) {
+ free_conv_data(conv_data);
+ return ENOMEM;
+ }
+ }
+
+ conv_data->out_info = new_conv_list(PAM_CONV_MSG_MAX);
+ conv_data->out_err = new_conv_list(PAM_CONV_MSG_MAX);
+ if (conv_data->out_info == NULL || conv_data->out_err == NULL) {
+ free_conv_data(conv_data);
+ return ENOMEM;
+ }
+
+ return 0;
+}
+
+/* test_list is allocated using PyMem_New and must be freed accordingly.
+ * Returns errno that should be handled into exception in the caller
+ */
+static int py_tc_list_to_cstruct_list(PyObject *py_test_list,
+ Py_ssize_t num_tests,
+ struct pam_testcase **_test_list)
+{
+ Py_ssize_t i;
+ PyObject *py_test;
+ int rc;
+ struct pam_testcase *test_list;
+
+ test_list = PyMem_New(struct pam_testcase,
+ num_tests * sizeof(struct pam_testcase));
+ if (test_list == NULL) {
+ return ENOMEM;
+ }
+
+ for (i = 0; i < num_tests; i++) {
+ /*
+ * PySequence_GetItem() increases the refcount on the
+ * returned value
+ */
+ py_test = PySequence_GetItem(py_test_list, i);
+ if (py_test == NULL) {
+ PyMem_Free(test_list);
+ return EIO;
+ }
+
+ rc = py_testcase_to_cstruct(py_test, &test_list[i]);
+ Py_DECREF(py_test);
+ if (rc != 0) {
+ PyMem_Free(test_list);
+ return EIO;
+ }
+ }
+
+ *_test_list = test_list;
+ return 0;
+}
+
+static PyObject *pypamtest_run_pamtest(PyObject *module, PyObject *args)
+{
+ int ok;
+ int rc;
+ char *username = NULL;
+ char *service = NULL;
+ PyObject *py_test_list;
+ PyObject *py_echo_off = NULL;
+ PyObject *py_echo_on = NULL;
+ Py_ssize_t num_tests;
+ struct pam_testcase *test_list;
+ enum pamtest_err perr;
+ struct pamtest_conv_data conv_data;
+ TestResultObject *result = NULL;
+
+ (void) module; /* unused */
+
+ ok = PyArg_ParseTuple(args,
+ discard_const_p(char, "ssO|OO"),
+ &username,
+ &service,
+ &py_test_list,
+ &py_echo_off,
+ &py_echo_on);
+ if (!ok) {
+ return NULL;
+ }
+
+ ok = PySequence_Check(py_test_list);
+ if (!ok) {
+ PyErr_Format(PyExc_TypeError, "tests must be a sequence");
+ return NULL;
+ }
+
+ num_tests = PySequence_Size(py_test_list);
+ if (num_tests == -1) {
+ PyErr_Format(PyExc_IOError, "Cannot get sequence length");
+ return NULL;
+ }
+
+ rc = py_tc_list_to_cstruct_list(py_test_list, num_tests, &test_list);
+ if (rc != 0) {
+ if (rc == ENOMEM) {
+ PyErr_NoMemory();
+ return NULL;
+ } else {
+ PyErr_Format(PyExc_IOError,
+ "Cannot convert test to C structure");
+ return NULL;
+ }
+ }
+
+ rc = fill_conv_data(py_echo_off, py_echo_on, &conv_data);
+ if (rc != 0) {
+ PyMem_Free(test_list);
+ PyErr_NoMemory();
+ return NULL;
+ }
+
+ perr = _pamtest(service, username, &conv_data, test_list, num_tests);
+ if (perr != PAMTEST_ERR_OK) {
+ free_conv_data(&conv_data);
+ set_pypamtest_exception(PyExc_PamTestError,
+ perr,
+ test_list,
+ num_tests);
+ PyMem_Free(test_list);
+ return NULL;
+ }
+ PyMem_Free(test_list);
+
+ result = construct_test_conv_result(conv_data.out_info,
+ conv_data.out_err);
+ free_conv_data(&conv_data);
+ if (result == NULL) {
+ PyMem_Free(test_list);
+ return NULL;
+ }
+
+ return (PyObject *)result;
+}
+
+static PyMethodDef pypamtest_module_methods[] = {
+ {
+ discard_const_p(char, "run_pamtest"),
+ (PyCFunction)pypamtest_run_pamtest,
+ METH_VARARGS,
+ discard_const_p(char, "TODO"),
+ },
+
+ { NULL, NULL, 0, NULL } /* Sentinel */
+};
+
+/*
+ * This is the module structure describing the module and
+ * to define methods
+ */
+static struct PyModuleDef pypamtestdef = {
+ .m_base = PyModuleDef_HEAD_INIT,
+ .m_name = PYTHON_MODULE_NAME,
+ .m_size = -1,
+ .m_methods = pypamtest_module_methods,
+};
+
+/**********************************************************
+ *** Initialize the module
+ **********************************************************/
+
+PyDoc_STRVAR(PamTestError__doc__,
+"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus"
+"Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec"
+"consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero"
+"egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem"
+"lacinia consectetur. Donec ut libero sed arcu vehicula ultricies"
+);
+
+PyMODINIT_FUNC PyInit_pypamtest(void)
+{
+ PyObject *m;
+ int ret;
+
+ m = PyModule_Create(&pypamtestdef);
+ if (m == NULL) {
+ RETURN_ON_ERROR;
+ }
+
+ PyExc_PamTestError = PyErr_NewExceptionWithDoc("pypamtest.PamTestError",
+ PamTestError__doc__,
+ PyExc_EnvironmentError,
+ NULL);
+ if (PyExc_PamTestError == NULL) {
+ RETURN_ON_ERROR;
+ }
+
+ Py_INCREF(PyExc_PamTestError);
+ ret = PyModule_AddObject(m, discard_const_p(char, "PamTestError"),
+ PyExc_PamTestError);
+ if (ret == -1) {
+ RETURN_ON_ERROR;
+ }
+
+ ret = PyModule_AddIntMacro(m, PAMTEST_AUTHENTICATE);
+ if (ret == -1) {
+ RETURN_ON_ERROR;
+ }
+ ret = PyModule_AddIntMacro(m, PAMTEST_SETCRED);
+ if (ret == -1) {
+ RETURN_ON_ERROR;
+ }
+ ret = PyModule_AddIntMacro(m, PAMTEST_ACCOUNT);
+ if (ret == -1) {
+ RETURN_ON_ERROR;
+ }
+ ret = PyModule_AddIntMacro(m, PAMTEST_OPEN_SESSION);
+ if (ret == -1) {
+ RETURN_ON_ERROR;
+ }
+ ret = PyModule_AddIntMacro(m, PAMTEST_CLOSE_SESSION);
+ if (ret == -1) {
+ RETURN_ON_ERROR;
+ }
+ ret = PyModule_AddIntMacro(m, PAMTEST_CHAUTHTOK);
+ if (ret == -1) {
+ RETURN_ON_ERROR;
+ }
+
+ ret = PyModule_AddIntMacro(m, PAMTEST_GETENVLIST);
+ if (ret == -1) {
+ RETURN_ON_ERROR;
+ }
+ ret = PyModule_AddIntMacro(m, PAMTEST_KEEPHANDLE);
+ if (ret == -1) {
+ RETURN_ON_ERROR;
+ }
+
+ if (PyType_Ready(&pypamtest_test_case) < 0) {
+ RETURN_ON_ERROR;
+ }
+ Py_INCREF(&pypamtest_test_case);
+ PyModule_AddObject(m, "TestCase", (PyObject *) &pypamtest_test_case);
+
+ if (PyType_Ready(&pypamtest_test_result) < 0) {
+ RETURN_ON_ERROR;
+ }
+ Py_INCREF(&pypamtest_test_result);
+ PyModule_AddObject(m, "TestResult",
+ (PyObject *) &pypamtest_test_result);
+
+ return m;
+}
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index daa78a0..0055280 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -7,14 +7,19 @@ include_directories(
${CMAKE_SOURCE_DIR}/include
)
+set(PAM_MATRIX_PATH "${CMAKE_BINARY_DIR}/src/modules/pam_matrix.so")
+
configure_file(services/matrix.in ${CMAKE_CURRENT_BINARY_DIR}/services/matrix @ONLY)
# Some tests use a passdb as argument for pam_matrix
-set(PASSDB_PATH
- ${CMAKE_CURRENT_BINARY_DIR}/passdb_ro)
-configure_file(passdb_ro ${PASSDB_PATH} @ONLY)
+set(PASSDB_RO_PATH ${CMAKE_CURRENT_BINARY_DIR}/passdb_ro)
+configure_file(passdb_ro ${PASSDB_RO_PATH} @ONLY)
configure_file(services/matrix_opt.in ${CMAKE_CURRENT_BINARY_DIR}/services/matrix_opt @ONLY)
+set(PASSDB_PY_PATH ${CMAKE_CURRENT_BINARY_DIR}/passdb_py)
+configure_file(passdb_py ${PASSDB_PY_PATH} @ONLY)
+configure_file(services/matrix_py.in ${CMAKE_CURRENT_BINARY_DIR}/services/matrix_py @ONLY)
+
configure_file(services/pwrap_get_set.in ${CMAKE_CURRENT_BINARY_DIR}/services/pwrap_get_set @ONLY)
if (OSX)
@@ -41,3 +46,10 @@ set_property(
test_pam_wrapper
PROPERTY
ENVIRONMENT ${TEST_ENVIRONMENT})
+
+add_test(pypamtest_test ${CMAKE_CURRENT_SOURCE_DIR}/pypamtest_test.py)
+set_property(
+ TEST
+ pypamtest_test
+ PROPERTY
+ ENVIRONMENT ${TEST_ENVIRONMENT})
diff --git a/tests/pypamtest_test.py b/tests/pypamtest_test.py
new file mode 100755
index 0000000..1e9e66b
--- /dev/null
+++ b/tests/pypamtest_test.py
@@ -0,0 +1,159 @@
+#!/usr/bin/env python3
+
+import unittest
+import os
+import sys
+import os.path
+
+class PyPamTestCase(unittest.TestCase):
+ def assertPamTestResultEqual(self, test_result, err_list, info_list):
+ self.assertTrue(test_result != None)
+ self.assertTrue(hasattr(test_result, 'info'))
+ self.assertTrue(hasattr(test_result, 'errors'))
+ self.assertSequenceEqual(test_result.info, err_list)
+ self.assertSequenceEqual(test_result.errors, info_list)
+
+class PyPamTestImport(unittest.TestCase):
+ def setUp(self):
+ " Make sure we load the in-tree module "
+ self.modpath = os.path.join(os.getcwd(), "../src/python")
+ self.system_path = sys.path[:]
+ sys.path = [ self.modpath ]
+
+ def tearDown(self):
+ " Restore the system path "
+ sys.path = self.system_path
+
+ def testImport(self):
+ " Import the module "
+ try:
+ import pypamtest
+ except ImportError as e:
+ print("Could not load the pypamtest module from %s. Please check if it is compiled" % self.modpath, file=sys.stderr)
+ raise e
+
+class PyPamTestTestCase(unittest.TestCase):
+ def test_constants(self):
+ " Tests the enum was added correctly "
+ self.assertTrue(hasattr(pypamtest, 'PAMTEST_AUTHENTICATE'))
+ self.assertTrue(hasattr(pypamtest, 'PAMTEST_SETCRED'))
+ self.assertTrue(hasattr(pypamtest, 'PAMTEST_ACCOUNT'))
+ self.assertTrue(hasattr(pypamtest, 'PAMTEST_OPEN_SESSION'))
+ self.assertTrue(hasattr(pypamtest, 'PAMTEST_CLOSE_SESSION'))
+ self.assertTrue(hasattr(pypamtest, 'PAMTEST_CHAUTHTOK'))
+
+ self.assertTrue(hasattr(pypamtest, 'PAMTEST_GETENVLIST'))
+ self.assertTrue(hasattr(pypamtest, 'PAMTEST_KEEPHANDLE'))
+
+ def test_members(self):
+ tc = pypamtest.TestCase(pypamtest.PAMTEST_AUTHENTICATE)
+ self.assertEqual(tc.pam_operation, pypamtest.PAMTEST_AUTHENTICATE)
+ self.assertEqual(tc.expected_rv, 0) # PAM_SUCCESS
+ self.assertEqual(tc.flags, 0)
+
+ tc = pypamtest.TestCase(pypamtest.PAMTEST_CHAUTHTOK, 1, 2)
+ self.assertEqual(tc.pam_operation, pypamtest.PAMTEST_CHAUTHTOK)
+ self.assertEqual(tc.expected_rv, 1)
+ self.assertEqual(tc.flags, 2)
+
+ # Testcase members should be immutable after constructing the test
+ # case
+ with self.assertRaises(AttributeError):
+ tc.pam_operation = pypamtest.PAMTEST_AUTHENTICATE
+
+ with self.assertRaises(AttributeError):
+ tc.expected_rv = 2
+
+ with self.assertRaises(AttributeError):
+ tc.flags = 3
+
+ def test_bad_op(self):
+ self.assertRaises(ValueError, pypamtest.TestCase, 666)
+
+# These are not silly tests. They test setup of the object and proper
+# GC function
+class PyPamTestTestResult(PyPamTestCase):
+ def setUp(self):
+ self.list_info = [ "info", "list" ]
+ self.list_error = [ "error", "list" ]
+
+ def test_default(self):
+ res = pypamtest.TestResult()
+ self.assertPamTestResultEqual(res, [], [])
+
+ def test_set_both(self):
+ res = pypamtest.TestResult(self.list_info,
+ self.list_error)
+ self.assertPamTestResultEqual(res,
+ self.list_info,
+ self.list_error)
+
+ def test_repr_default(self):
+ res = pypamtest.TestResult()
+ self.assertEqual(repr(res), "{ errors: { } infos: { } }")
+
+ def test_repr_both(self):
+ res = pypamtest.TestResult(self.list_info,
+ self.list_error)
+ self.assertEqual(repr(res),
+ "{ errors: { {info}{list} } infos: { {info}{list} } }")
+
+class PyPamTestRunTest(unittest.TestCase):
+ def test_run(self):
+ neo_password = "secret"
+ tc = pypamtest.TestCase(pypamtest.PAMTEST_AUTHENTICATE)
+ res = pypamtest.run_pamtest("neo", "matrix_py", [tc], [ neo_password ])
+
+ # No messages from this test -> both info and err should be empty tuples
+ self.assertTrue(res != None)
+ self.assertTrue(hasattr(res, 'info'))
+ self.assertTrue(hasattr(res, 'errors'))
+ # Running with verbose mode so there would be an info message
+ self.assertSequenceEqual(res.info, (u'Authentication succeeded',))
+ self.assertSequenceEqual(res.errors, ())
+
+ def test_repr(self):
+ tc = pypamtest.TestCase(pypamtest.PAMTEST_CHAUTHTOK, 1, 2)
+ r = repr(tc)
+ self.assertEqual(r, "{ pam_operation [5] expected_rv [1] flags [2] }")
+
+ def test_exception(self):
+ neo_password = "wrong_secret"
+ tc = pypamtest.TestCase(pypamtest.PAMTEST_AUTHENTICATE)
+
+ self.assertRaisesRegexp(pypamtest.PamTestError,
+ "Error \[2\]: Test case { pam_operation \[0\] "
+ "expected_rv \[0\] flags \[0\] } "
+ "retured \[7\]",
+ pypamtest.run_pamtest,
+ "neo", "matrix_py", [tc], [ neo_password ])
+
+if __name__ == "__main__":
+ error = 0
+
+ suite = unittest.TestLoader().loadTestsFromTestCase(PyPamTestImport)
+ res = unittest.TextTestRunner().run(suite)
+ if not res.wasSuccessful():
+ error |= 0x1
+ # need to bail out here because module could not be imported
+ sys.exit(error)
+
+ sys.path.insert(0, os.path.join(os.getcwd()))
+ import pypamtest
+
+ suite = unittest.TestLoader().loadTestsFromTestCase(PyPamTestTestCase)
+ res = unittest.TextTestRunner().run(suite)
+ if not res.wasSuccessful():
+ error |= 0x2
+
+ suite = unittest.TestLoader().loadTestsFromTestCase(PyPamTestTestResult)
+ res = unittest.TextTestRunner().run(suite)
+ if not res.wasSuccessful():
+ error |= 0x3
+
+ suite = unittest.TestLoader().loadTestsFromTestCase(PyPamTestRunTest)
+ res = unittest.TextTestRunner().run(suite)
+ if not res.wasSuccessful():
+ error |= 0x4
+
+ sys.exit(error)
diff --git a/tests/services/matrix.in b/tests/services/matrix.in
index 70d2a6c..59fbf99 100644
--- a/tests/services/matrix.in
+++ b/tests/services/matrix.in
@@ -1,5 +1,4 @@
-auth required @CMAKE_CURRENT_BINARY_DIR@/../src/pam_matrix.so
-account required @CMAKE_CURRENT_BINARY_DIR@/../src/pam_matrix.so
-password required @CMAKE_CURRENT_BINARY_DIR@/../src/pam_matrix.so
-session required @CMAKE_CURRENT_BINARY_DIR@/../src/pam_matrix.so
-
+auth required @PAM_MATRIX_PATH@
+account required @PAM_MATRIX_PATH@
+password required @PAM_MATRIX_PATH@
+session required @PAM_MATRIX_PATH@
diff --git a/tests/services/matrix_opt.in b/tests/services/matrix_opt.in
index f1213c1..850c0ae 100644
--- a/tests/services/matrix_opt.in
+++ b/tests/services/matrix_opt.in
@@ -1 +1 @@
-auth required @CMAKE_CURRENT_BINARY_DIR@/../src/pam_matrix.so passdb=@CMAKE_CURRENT_BINARY_DIR@/passdb_ro verbose echo
+auth required @PAM_MATRIX_PATH@ passdb=@PASSDB_RO_PATH@ verbose echo
diff --git a/tests/services/matrix_py.in b/tests/services/matrix_py.in
index e9f2336..24d3f63 100644
--- a/tests/services/matrix_py.in
+++ b/tests/services/matrix_py.in
@@ -1,4 +1,4 @@
-auth required @PAM_MATRIX_PATH@ passdb=@PASSDB_PY_PATH@
+auth required @PAM_MATRIX_PATH@ verbose passdb=@PASSDB_PY_PATH@
account required @PAM_MATRIX_PATH@ passdb=@PASSDB_PY_PATH@
session required @PAM_MATRIX_PATH@ passdb=@PASSDB_PY_PATH@
password required @PAM_MATRIX_PATH@ passdb=@PASSDB_PY_PATH@