2 * Copyright (c) 2015 Andreas Schneider <asn@samba.org>
3 * Copyright (c) 2015 Jakub Hrozek <jakub.hrozek@posteo.se>
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 #include <structmember.h>
22 #include "libpamtest.h"
24 #define PYTHON_MODULE_NAME "pypamtest"
26 #ifndef discard_const_p
27 #if defined(__intptr_t_defined) || defined(HAVE_UINTPTR_T)
28 # define discard_const_p(type, ptr) ((type *)((uintptr_t)(ptr)))
30 # define discard_const_p(type, ptr) ((type *)(ptr))
34 #define __unused __attribute__((__unused__))
36 #if PY_MAJOR_VERSION >= 3
38 #define RETURN_ON_ERROR return NULL
41 #define RETURN_ON_ERROR return
42 #endif /* PY_MAJOR_VERSION */
44 /* We only return up to 16 messages from the PAM conversation */
45 #define PAM_CONV_MSG_MAX 16
48 PyMODINIT_FUNC PyInit_pypamtest(void);
50 PyMODINIT_FUNC initpypamtest(void);
56 enum pamtest_ops pam_operation;
61 /**********************************************************
62 *** module-specific exceptions
63 **********************************************************/
64 static PyObject *PyExc_PamTestError;
66 /**********************************************************
68 **********************************************************/
70 #define REPR_FMT "{ pam_operation [%d] " \
74 static char *py_strdup(const char *string)
78 copy = PyMem_New(char, strlen(string) + 1);
84 return strcpy(copy, string);
87 static PyObject *get_utf8_string(PyObject *obj,
90 const char *a = attrname ? attrname : "attribute";
91 PyObject *obj_utf8 = NULL;
93 if (PyBytes_Check(obj)) {
95 Py_INCREF(obj_utf8); /* Make sure we can DECREF later */
96 } else if (PyUnicode_Check(obj)) {
97 if ((obj_utf8 = PyUnicode_AsUTF8String(obj)) == NULL) {
101 PyErr_Format(PyExc_TypeError, "%s must be a string", a);
108 static void free_cstring_list(const char **list)
116 for (i=0; list[i]; i++) {
117 PyMem_Free(discard_const_p(char, list[i]));
122 static void free_string_list(char **list)
130 for (i=0; list[i]; i++) {
136 static char **new_conv_list(const size_t list_size)
141 if (list_size == 0) {
145 if (list_size + 1 < list_size) {
149 list = PyMem_New(char *, list_size + 1);
153 list[list_size] = NULL;
155 for (i = 0; i < list_size; i++) {
156 list[i] = PyMem_New(char, PAM_MAX_MSG_SIZE);
157 if (list[i] == NULL) {
161 memset(list[i], 0, PAM_MAX_MSG_SIZE);
167 static const char **sequence_as_string_list(PyObject *seq,
168 const char *paramname)
170 const char *p = paramname ? paramname : "attribute values";
177 if (!PySequence_Check(seq)) {
178 PyErr_Format(PyExc_TypeError,
179 "The object must be a sequence\n");
183 len = PySequence_Size(seq);
188 ret = PyMem_New(const char *, (len + 1));
194 for (i = 0; i < len; i++) {
195 item = PySequence_GetItem(seq, i);
200 utf_item = get_utf8_string(item, p);
201 if (utf_item == NULL) {
206 ret[i] = py_strdup(PyBytes_AsString(utf_item));
219 static PyObject *string_list_as_tuple(char **str_list)
226 for (len=0; len < PAM_CONV_MSG_MAX; len++) {
227 if (str_list[len][0] == '\0') {
228 /* unused string, stop counting */
233 tup = PyTuple_New(len);
239 for (i = 0; i < len; i++) {
240 py_str = PyUnicode_FromString(str_list[i]);
241 if (py_str == NULL) {
247 /* PyTuple_SetItem() steals the reference to
248 * py_str, so it's enough to decref the tuple
249 * pointer afterwards */
250 rc = PyTuple_SetItem(tup, i, py_str);
264 set_pypamtest_exception(PyObject *exc,
265 enum pamtest_err perr,
266 struct pam_testcase *tests,
269 PyObject *obj = NULL;
270 /* REPR_FMT contains just %d expansions, so this is safe */
271 char test_repr[256] = { '\0' };
277 const struct pam_testcase *failed = NULL;
284 strerr = pamtest_strerror(perr);
286 if (perr == PAMTEST_ERR_CASE) {
287 failed = _pamtest_failed_case(tests, num_tests);
289 snprintf(test_repr, sizeof(test_repr), REPR_FMT,
290 failed->pam_operation,
296 if (test_repr[0] != '\0' && failed != NULL) {
298 "Error [%d]: Test case %s retured [%d]",
299 perr, test_repr, failed->op_rv);
301 obj = Py_BuildValue(discard_const_p(char, "(i,s)"),
303 strerr ? strerr : "Unknown error");
304 PyErr_SetObject(exc, obj);
307 pypam_str_object.str = test_repr;
308 Py_XDECREF(pypam_str_object.obj);
312 /* Returned when doc(test_case) is invoked */
313 PyDoc_STRVAR(TestCaseObject__doc__,
314 "pamtest test case\n\n"
315 "Represents one operation in PAM transaction. An example is authentication, "
316 "opening a session or password change. Each operation has an expected error "
317 "code. The run_pamtest() function accepts a list of these test case objects\n"
319 "pam_operation: - the PAM operation to run. Use constants from pypamtest "
320 "such as pypamtest.PAMTEST_AUTHENTICATE. This argument is required.\n"
321 "expected_rv: - The PAM return value we expect the operation to return. "
322 "Defaults to 0 (PAM_SUCCESS)\n"
323 "flags: - Additional flags to pass to the PAM operation. Defaults to 0.\n"
327 TestCase_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
329 TestCaseObject *self;
331 (void) args; /* unused */
332 (void) kwds; /* unused */
334 self = (TestCaseObject *)type->tp_alloc(type, 0);
340 return (PyObject *) self;
343 /* The traverse and clear methods must be defined even though they do nothing
344 * otherwise Garbage Collector is not happy
346 static int TestCase_clear(TestCaseObject *self)
348 (void) self; /* unused */
353 static void TestCase_dealloc(TestCaseObject *self)
355 Py_TYPE(self)->tp_free((PyObject *)self);
358 static int TestCase_traverse(TestCaseObject *self,
362 (void) self; /* unused */
363 (void) visit; /* unused */
364 (void) arg; /* unused */
369 static int TestCase_init(TestCaseObject *self,
373 const char * const kwlist[] = { "pam_operation",
377 int pam_operation = -1;
378 int expected_rv = PAM_SUCCESS;
382 ok = PyArg_ParseTupleAndKeywords(args,
385 discard_const_p(char *, kwlist),
393 switch (pam_operation) {
394 case PAMTEST_AUTHENTICATE:
395 case PAMTEST_SETCRED:
396 case PAMTEST_ACCOUNT:
397 case PAMTEST_OPEN_SESSION:
398 case PAMTEST_CLOSE_SESSION:
399 case PAMTEST_CHAUTHTOK:
400 case PAMTEST_GETENVLIST:
401 case PAMTEST_KEEPHANDLE:
404 PyErr_Format(PyExc_ValueError,
405 "Unsupported PAM operation %d",
411 self->expected_rv = expected_rv;
412 self->pam_operation = pam_operation;
418 * This function returns string representation of the object, but one that
419 * can be parsed by a machine.
421 * str() is also string represtentation, but just human-readable.
423 static PyObject *TestCase_repr(TestCaseObject *self)
425 return PyUnicode_FromFormat(REPR_FMT,
431 static PyMemberDef pypamtest_test_case_members[] = {
433 discard_const_p(char, "pam_operation"),
435 offsetof(TestCaseObject, pam_operation),
437 discard_const_p(char, "The PAM operation to run"),
441 discard_const_p(char, "expected_rv"),
443 offsetof(TestCaseObject, expected_rv),
445 discard_const_p(char, "The expected PAM return code"),
449 discard_const_p(char, "flags"),
451 offsetof(TestCaseObject, flags),
453 discard_const_p(char, "Additional flags for the PAM operation"),
456 { NULL, 0, 0, 0, NULL } /* Sentinel */
459 static PyTypeObject pypamtest_test_case = {
460 PyVarObject_HEAD_INIT(NULL, 0)
461 .tp_name = "pypamtest.TestCase",
462 .tp_basicsize = sizeof(TestCaseObject),
463 .tp_new = TestCase_new,
464 .tp_dealloc = (destructor) TestCase_dealloc,
465 .tp_traverse = (traverseproc) TestCase_traverse,
466 .tp_clear = (inquiry) TestCase_clear,
467 .tp_init = (initproc) TestCase_init,
468 .tp_repr = (reprfunc) TestCase_repr,
469 .tp_members = pypamtest_test_case_members,
470 .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
471 .tp_doc = TestCaseObject__doc__
474 PyDoc_STRVAR(TestResultObject__doc__,
475 "pamtest test result\n\n"
476 "The test result object is returned from run_pamtest on success. It contains"
477 "two lists of strings (up to 16 strings each) which contain the info and error"
478 "messages the PAM conversation printed\n\n"
480 "errors: PAM_ERROR_MSG-level messages printed during the PAM conversation\n"
481 "info: PAM_TEXT_INFO-level messages printed during the PAM conversation\n"
487 PyObject *info_msg_list;
488 PyObject *error_msg_list;
492 TestResult_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
494 TestResultObject *self;
496 (void) args; /* unused */
497 (void) kwds; /* unused */
499 self = (TestResultObject *)type->tp_alloc(type, 0);
505 return (PyObject *) self;
508 static int TestResult_clear(TestResultObject *self)
510 (void) self; /* unused */
515 static void TestResult_dealloc(TestResultObject *self)
517 Py_TYPE(self)->tp_free((PyObject *)self);
520 static int TestResult_traverse(TestResultObject *self,
524 (void) self; /* unused */
525 (void) visit; /* unused */
526 (void) arg; /* unused */
531 static int TestResult_init(TestResultObject *self,
535 const char * const kwlist[] = { "info_msg_list",
539 PyObject *py_info_list = NULL;
540 PyObject *py_err_list = NULL;
542 ok = PyArg_ParseTupleAndKeywords(args,
545 discard_const_p(char *, kwlist),
553 ok = PySequence_Check(py_info_list);
555 PyErr_Format(PyExc_TypeError,
556 "List of info messages must be a sequence\n");
560 self->info_msg_list = py_info_list;
561 Py_XINCREF(py_info_list);
563 self->info_msg_list = PyList_New(0);
564 if (self->info_msg_list == NULL) {
571 ok = PySequence_Check(py_err_list);
573 PyErr_Format(PyExc_TypeError,
574 "List of error messages must be a sequence\n");
578 self->error_msg_list = py_err_list;
579 Py_XINCREF(py_err_list);
581 self->error_msg_list = PyList_New(0);
582 if (self->error_msg_list == NULL) {
591 static PyObject *test_result_list_concat(PyObject *list,
592 const char delim_pre,
593 const char delim_post)
600 res = PyUnicode_FromString("");
605 size = PySequence_Size(list);
607 for (i=0; i < size; i++) {
608 item = PySequence_GetItem(list, i);
615 res = PyUnicode_FromFormat("%U%c%U%c",
616 res, delim_pre, item, delim_post);
618 res = PyUnicode_FromFormat("%U%c%s%c",
621 PyString_AsString(item),
630 static PyObject *TestResult_repr(TestResultObject *self)
632 PyObject *u_info = NULL;
633 PyObject *u_error = NULL;
634 PyObject *res = NULL;
636 u_info = test_result_list_concat(self->info_msg_list, '{', '}');
637 u_error = test_result_list_concat(self->info_msg_list, '{', '}');
638 if (u_info == NULL || u_error == NULL) {
644 res = PyUnicode_FromFormat("{ errors: { %U } infos: { %U } }",
651 static PyMemberDef pypamtest_test_result_members[] = {
653 discard_const_p(char, "errors"),
655 offsetof(TestResultObject, error_msg_list),
657 discard_const_p(char,
658 "List of error messages from PAM conversation"),
662 discard_const_p(char, "info"),
664 offsetof(TestResultObject, info_msg_list),
666 discard_const_p(char,
667 "List of info messages from PAM conversation"),
670 { NULL, 0, 0, 0, NULL } /* Sentinel */
673 static PyTypeObject pypamtest_test_result = {
674 PyVarObject_HEAD_INIT(NULL, 0)
675 .tp_name = "pypamtest.TestResult",
676 .tp_basicsize = sizeof(TestResultObject),
677 .tp_new = TestResult_new,
678 .tp_dealloc = (destructor) TestResult_dealloc,
679 .tp_traverse = (traverseproc) TestResult_traverse,
680 .tp_clear = (inquiry) TestResult_clear,
681 .tp_init = (initproc) TestResult_init,
682 .tp_repr = (reprfunc) TestResult_repr,
683 .tp_members = pypamtest_test_result_members,
684 .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
685 .tp_doc = TestResultObject__doc__
688 /**********************************************************
689 *** Methods of the module
690 **********************************************************/
692 static TestResultObject *construct_test_conv_result(char **msg_info, char **msg_err)
694 PyObject *py_msg_info = NULL;
695 PyObject *py_msg_err = NULL;
696 TestResultObject *result = NULL;
697 PyObject *result_args = NULL;
700 py_msg_info = string_list_as_tuple(msg_info);
701 py_msg_err = string_list_as_tuple(msg_err);
702 if (py_msg_info == NULL || py_msg_err == NULL) {
703 /* The exception is raised in string_list_as_tuple() */
704 Py_XDECREF(py_msg_err);
705 Py_XDECREF(py_msg_info);
709 result = (TestResultObject *) TestResult_new(&pypamtest_test_result,
712 if (result == NULL) {
713 /* The exception is raised in TestResult_new */
714 Py_XDECREF(py_msg_err);
715 Py_XDECREF(py_msg_info);
719 result_args = PyTuple_New(2);
720 if (result_args == NULL) {
721 /* The exception is raised in TestResult_new */
723 Py_XDECREF(py_msg_err);
724 Py_XDECREF(py_msg_info);
728 /* Brand new tuples with fixed size don't need error checking */
729 PyTuple_SET_ITEM(result_args, 0, py_msg_info);
730 PyTuple_SET_ITEM(result_args, 1, py_msg_err);
732 rc = TestResult_init(result, result_args, NULL);
733 Py_XDECREF(result_args);
742 static int py_testcase_get(PyObject *py_test,
743 const char *member_name,
746 PyObject* item = NULL;
749 * PyPyObject_GetAttrString() increases the refcount on the
752 item = PyObject_GetAttrString(py_test, member_name);
757 *_value = PyLong_AsLong(item);
763 static int py_testcase_to_cstruct(PyObject *py_test, struct pam_testcase *test)
768 rc = py_testcase_get(py_test, "pam_operation", &value);
772 test->pam_operation = value;
774 rc = py_testcase_get(py_test, "expected_rv", &value);
778 test->expected_rv = value;
780 rc = py_testcase_get(py_test, "flags", &value);
789 static void free_conv_data(struct pamtest_conv_data *conv_data)
791 if (conv_data == NULL) {
795 free_string_list(conv_data->out_err);
796 free_string_list(conv_data->out_info);
797 free_cstring_list(conv_data->in_echo_on);
798 free_cstring_list(conv_data->in_echo_off);
801 /* conv_data must be a pointer to allocated conv_data structure.
803 * Use free_conv_data() to free the contents.
805 static int fill_conv_data(PyObject *py_echo_off,
806 PyObject *py_echo_on,
807 struct pamtest_conv_data *conv_data)
809 conv_data->in_echo_on = NULL;
810 conv_data->in_echo_off = NULL;
811 conv_data->out_err = NULL;
812 conv_data->out_info = NULL;
814 if (py_echo_off != NULL) {
815 conv_data->in_echo_off = sequence_as_string_list(py_echo_off,
817 if (conv_data->in_echo_off == NULL) {
818 free_conv_data(conv_data);
823 if (py_echo_on != NULL) {
824 conv_data->in_echo_on = sequence_as_string_list(py_echo_on,
826 if (conv_data->in_echo_on == NULL) {
827 free_conv_data(conv_data);
832 conv_data->out_info = new_conv_list(PAM_CONV_MSG_MAX);
833 conv_data->out_err = new_conv_list(PAM_CONV_MSG_MAX);
834 if (conv_data->out_info == NULL || conv_data->out_err == NULL) {
835 free_conv_data(conv_data);
842 /* test_list is allocated using PyMem_New and must be freed accordingly.
843 * Returns errno that should be handled into exception in the caller
845 static int py_tc_list_to_cstruct_list(PyObject *py_test_list,
846 Py_ssize_t num_tests,
847 struct pam_testcase **_test_list)
852 struct pam_testcase *test_list;
854 test_list = PyMem_New(struct pam_testcase,
855 num_tests * sizeof(struct pam_testcase));
856 if (test_list == NULL) {
860 for (i = 0; i < num_tests; i++) {
862 * PySequence_GetItem() increases the refcount on the
865 py_test = PySequence_GetItem(py_test_list, i);
866 if (py_test == NULL) {
867 PyMem_Free(test_list);
871 rc = py_testcase_to_cstruct(py_test, &test_list[i]);
874 PyMem_Free(test_list);
879 *_test_list = test_list;
883 PyDoc_STRVAR(RunPamTest__doc__,
885 "This function runs PAM test cases and reports result\n"
887 "service: The PAM service to use in the conversation (string)\n"
888 "username: The user to run PAM conversation as\n"
889 "test_list: Sequence of pypamtest.TestCase objects\n"
890 "echo_off_list: Sequence of strings that will be used by PAM "
891 "conversation for PAM_PROMPT_ECHO_OFF input. These are typically "
893 "echo_on_list: Sequence of strings that will be used by PAM "
894 "conversation for PAM_PROMPT_ECHO_ON input.\n"
897 static PyObject *pypamtest_run_pamtest(PyObject *module, PyObject *args)
901 char *username = NULL;
902 char *service = NULL;
903 PyObject *py_test_list;
904 PyObject *py_echo_off = NULL;
905 PyObject *py_echo_on = NULL;
906 Py_ssize_t num_tests;
907 struct pam_testcase *test_list;
908 enum pamtest_err perr;
909 struct pamtest_conv_data conv_data;
910 TestResultObject *result = NULL;
912 (void) module; /* unused */
914 ok = PyArg_ParseTuple(args,
915 discard_const_p(char, "ssO|OO"),
925 ok = PySequence_Check(py_test_list);
927 PyErr_Format(PyExc_TypeError, "tests must be a sequence");
931 num_tests = PySequence_Size(py_test_list);
932 if (num_tests == -1) {
933 PyErr_Format(PyExc_IOError, "Cannot get sequence length");
937 rc = py_tc_list_to_cstruct_list(py_test_list, num_tests, &test_list);
943 PyErr_Format(PyExc_IOError,
944 "Cannot convert test to C structure");
949 rc = fill_conv_data(py_echo_off, py_echo_on, &conv_data);
951 PyMem_Free(test_list);
956 perr = _pamtest(service, username, &conv_data, test_list, num_tests);
957 if (perr != PAMTEST_ERR_OK) {
958 free_conv_data(&conv_data);
959 set_pypamtest_exception(PyExc_PamTestError,
963 PyMem_Free(test_list);
966 PyMem_Free(test_list);
968 result = construct_test_conv_result(conv_data.out_info,
970 free_conv_data(&conv_data);
971 if (result == NULL) {
972 PyMem_Free(test_list);
976 return (PyObject *)result;
979 static PyMethodDef pypamtest_module_methods[] = {
981 discard_const_p(char, "run_pamtest"),
982 (PyCFunction) pypamtest_run_pamtest,
987 { NULL, NULL, 0, NULL } /* Sentinel */
991 * This is the module structure describing the module and
995 static struct PyModuleDef pypamtestdef = {
996 .m_base = PyModuleDef_HEAD_INIT,
997 .m_name = PYTHON_MODULE_NAME,
999 .m_methods = pypamtest_module_methods,
1003 /**********************************************************
1004 *** Initialize the module
1005 **********************************************************/
1007 #if PY_VERSION_HEX >= 0x02070000 /* >= 2.7.0 */
1008 PyDoc_STRVAR(PamTestError__doc__,
1009 "pypamtest specific exception\n\n"
1010 "This exception is raised if the _pamtest() function fails. If _pamtest() "
1011 "returns PAMTEST_ERR_CASE (a test case returns unexpected error code), then "
1012 "the exception also details which test case failed."
1017 PyMODINIT_FUNC PyInit_pypamtest(void)
1019 PyMODINIT_FUNC initpypamtest(void)
1024 PyTypeObject *type_obj;
1030 m = PyModule_Create(&pypamtestdef);
1035 m = Py_InitModule(discard_const_p(char, PYTHON_MODULE_NAME),
1036 pypamtest_module_methods);
1039 #if PY_VERSION_HEX >= 0x02070000 /* >= 2.7.0 */
1040 PyExc_PamTestError = PyErr_NewExceptionWithDoc(discard_const_p(char, "pypamtest.PamTestError"),
1041 PamTestError__doc__,
1042 PyExc_EnvironmentError,
1045 PyExc_PamTestError = PyErr_NewException(discard_const_p(char, "pypamtest.PamTestError"),
1046 PyExc_EnvironmentError,
1050 if (PyExc_PamTestError == NULL) {
1054 Py_INCREF(PyExc_PamTestError);
1055 ret = PyModule_AddObject(m, discard_const_p(char, "PamTestError"),
1056 PyExc_PamTestError);
1061 ret = PyModule_AddIntMacro(m, PAMTEST_AUTHENTICATE);
1065 ret = PyModule_AddIntMacro(m, PAMTEST_SETCRED);
1069 ret = PyModule_AddIntMacro(m, PAMTEST_ACCOUNT);
1073 ret = PyModule_AddIntMacro(m, PAMTEST_OPEN_SESSION);
1077 ret = PyModule_AddIntMacro(m, PAMTEST_CLOSE_SESSION);
1081 ret = PyModule_AddIntMacro(m, PAMTEST_CHAUTHTOK);
1086 ret = PyModule_AddIntMacro(m, PAMTEST_GETENVLIST);
1090 ret = PyModule_AddIntMacro(m, PAMTEST_KEEPHANDLE);
1095 pypam_object.type_obj = &pypamtest_test_case;
1096 if (PyType_Ready(pypam_object.type_obj) < 0) {
1099 Py_INCREF(pypam_object.obj);
1100 PyModule_AddObject(m, "TestCase", pypam_object.obj);
1102 pypam_object.type_obj = &pypamtest_test_result;
1103 if (PyType_Ready(pypam_object.type_obj) < 0) {
1106 Py_INCREF(pypam_object.obj);
1107 PyModule_AddObject(m, "TestResult", pypam_object.obj);