From cdbc3389480f610341d244f648cc5a3a2f23c67c Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Wed, 4 Nov 2015 16:59:41 +0100 Subject: libpamtest: Add Python bindings So far only for Python3 Pair-Programmed-With: Andreas Schneider --- .travis.yml | 2 +- CMakeLists.txt | 1 + include/libpamtest.h | 2 +- src/CMakeLists.txt | 27 +- src/python/CMakeLists.txt | 14 + src/python/pypamtest.c | 1044 ++++++++++++++++++++++++++++++++++++++++++ tests/CMakeLists.txt | 18 +- tests/pypamtest_test.py | 159 +++++++ tests/services/matrix.in | 9 +- tests/services/matrix_opt.in | 2 +- tests/services/matrix_py.in | 2 +- 11 files changed, 1245 insertions(+), 35 deletions(-) create mode 100644 src/python/CMakeLists.txt create mode 100644 src/python/pypamtest.c create mode 100755 tests/pypamtest_test.py 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 + * Copyright (c) 2015 Jakub Hrozek + * + * 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 "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@ -- cgit v1.2.3