diff options
Diffstat (limited to 'plugins/python/sudo_python_module.c')
-rw-r--r-- | plugins/python/sudo_python_module.c | 631 |
1 files changed, 631 insertions, 0 deletions
diff --git a/plugins/python/sudo_python_module.c b/plugins/python/sudo_python_module.c new file mode 100644 index 0000000..e9bc6b9 --- /dev/null +++ b/plugins/python/sudo_python_module.c @@ -0,0 +1,631 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2019-2020 Robert Manner <robert.manner@oneidentity.com> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include "sudo_python_module.h" + +#define EXC_VAR(exception_name) sudo_exc_ ## exception_name +#define TYPE_VAR(type_name) &sudo_type_ ## type_name + +// exceptions: +PyObject *sudo_exc_SudoException; +PyObject *sudo_exc_PluginException; +PyObject *sudo_exc_PluginError; +PyObject *sudo_exc_PluginReject; +static PyObject *sudo_exc_ConversationInterrupted; + +// the methods exposed in the "sudo" python module +// "args" is a tuple (~= const list) containing all the unnamed arguments +// "kwargs" is a dict of the keyword arguments or NULL if there are none +static PyObject *python_sudo_log_info(PyObject *py_self, PyObject *py_args, PyObject *py_kwargs); +static PyObject *python_sudo_log_error(PyObject *py_self, PyObject *py_args, PyObject *py_kwargs); +static PyObject *python_sudo_conversation(PyObject *py_self, PyObject *py_args, PyObject *py_kwargs); +static PyObject *python_sudo_options_as_dict(PyObject *py_self, PyObject *py_args); +static PyObject *python_sudo_options_from_dict(PyObject *py_self, PyObject *py_args); + +// Called on module teardown. +static void sudo_module_free(void *self); + +static PyMethodDef sudo_methods[] = { + {"debug", (PyCFunction)python_sudo_debug, METH_VARARGS, "Debug messages which can be saved to file in sudo.conf."}, + {"log_info", (PyCFunction)python_sudo_log_info, METH_VARARGS | METH_KEYWORDS, "Display informational messages."}, + {"log_error", (PyCFunction)python_sudo_log_error, METH_VARARGS | METH_KEYWORDS, "Display error messages."}, + {"conv", (PyCFunction)python_sudo_conversation, METH_VARARGS | METH_KEYWORDS, "Interact with the user"}, + {"options_as_dict", python_sudo_options_as_dict, METH_VARARGS, "Convert a string tuple in key=value format to a dictionary."}, + {"options_from_dict", python_sudo_options_from_dict, METH_VARARGS, "Convert a dictionary to a tuple of strings in key=value format."}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +static struct PyModuleDef sudo_module = { + PyModuleDef_HEAD_INIT, + "sudo", /* name of module */ + NULL, /* module documentation, may be NULL */ + -1, /* size of per-interpreter state of the module, + or -1 if the module keeps state in global variables. */ + sudo_methods, + NULL, /* slots */ + NULL, /* traverse */ + NULL, /* clear */ + sudo_module_free +}; + +CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION +static int +_parse_log_function_args(PyObject *py_args, PyObject *py_kwargs, char **args_joined, const char ** end) +{ + debug_decl(python_sudo_log, PYTHON_DEBUG_INTERNAL); + + int rc = SUDO_RC_ERROR; + PyObject *py_empty = NULL; + + const char *sep = NULL; + py_empty = PyTuple_New(0); + if (py_empty == NULL) + goto cleanup; + + static const char *keywords[] = { "sep", "end", NULL }; + if (py_kwargs != NULL && !PyArg_ParseTupleAndKeywords(py_empty, py_kwargs, "|zz:sudo.log", (char **)keywords, &sep, end)) + goto cleanup; + + if (sep == NULL) + sep = " "; + + if (*end == NULL) + *end = "\n"; + + // this is to mimic the behaviour of python "print" / "log" + *args_joined = py_join_str_list(py_args, sep); + if (!PyErr_Occurred()) // == (*args_joined != NULL), but cpychecker does not understand that + rc = SUDO_RC_OK; + +cleanup: + Py_CLEAR(py_empty); + debug_return_int(rc); +} + +static PyObject * +python_sudo_log(int msg_type, PyObject *Py_UNUSED(py_self), PyObject *py_args, PyObject *py_kwargs) +{ + debug_decl(python_sudo_log, PYTHON_DEBUG_C_CALLS); + py_debug_python_call("sudo", "log", py_args, py_kwargs, PYTHON_DEBUG_C_CALLS); + + int rc = SUDO_RC_ERROR; + + char *args_joined = NULL; + const char *end = NULL; + if (_parse_log_function_args(py_args, py_kwargs, &args_joined, &end) != SUDO_RC_OK) + goto cleanup; + + rc = py_ctx.sudo_log(msg_type, "%s%s", args_joined, end); + if (rc < 0) { + PyErr_Format(sudo_exc_SudoException, "sudo.log: Error displaying message"); + goto cleanup; + } + +cleanup: + free(args_joined); + + PyObject *py_result = PyErr_Occurred() ? NULL : PyLong_FromLong(rc); + + py_debug_python_result("sudo", "log", py_result, PYTHON_DEBUG_C_CALLS); + debug_return_ptr(py_result); +} + +static PyObject * +python_sudo_options_as_dict(PyObject *py_self, PyObject *py_args) +{ + (void) py_self; + + debug_decl(python_sudo_options_as_dict, PYTHON_DEBUG_C_CALLS); + py_debug_python_call("sudo", "options_as_dict", py_args, NULL, PYTHON_DEBUG_C_CALLS); + + PyObject *py_config_tuple = NULL, + *py_result = NULL, + *py_config_tuple_iterator = NULL, + *py_config = NULL, + *py_splitted = NULL, + *py_separator = NULL; + + if (!PyArg_ParseTuple(py_args, "O:sudo.options_as_dict", &py_config_tuple)) + goto cleanup; + + py_config_tuple_iterator = PyObject_GetIter(py_config_tuple); + if (py_config_tuple_iterator == NULL) + goto cleanup; + + py_result = PyDict_New(); + if (py_result == NULL) + goto cleanup; + + py_separator = PyUnicode_FromString("="); + if (py_separator == NULL) + goto cleanup; + + while ((py_config = PyIter_Next(py_config_tuple_iterator)) != NULL) { + py_splitted = PyUnicode_Split(py_config, py_separator, 1); + if (py_splitted == NULL) + goto cleanup; + + PyObject *py_key = PyList_GetItem(py_splitted, 0); // borrowed ref + if (py_key == NULL) + goto cleanup; + + PyObject *py_value = PyList_GetItem(py_splitted, 1); + if (py_value == NULL) { // skip values without a key + Py_CLEAR(py_config); + Py_CLEAR(py_splitted); + PyErr_Clear(); + continue; + } + + if (PyDict_SetItem(py_result, py_key, py_value) != 0) { + goto cleanup; + } + + Py_CLEAR(py_config); + Py_CLEAR(py_splitted); + } + +cleanup: + Py_CLEAR(py_config_tuple_iterator); + Py_CLEAR(py_config); + Py_CLEAR(py_splitted); + Py_CLEAR(py_separator); + + if (PyErr_Occurred()) { + Py_CLEAR(py_result); + } + + py_debug_python_result("sudo", "options_as_dict", py_result, PYTHON_DEBUG_C_CALLS); + debug_return_ptr(py_result); +} + +static PyObject * +python_sudo_options_from_dict(PyObject *py_self, PyObject *py_args) +{ + (void) py_self; + debug_decl(python_sudo_options_from_dict, PYTHON_DEBUG_C_CALLS); + py_debug_python_call("sudo", "options_from_dict", py_args, NULL, PYTHON_DEBUG_C_CALLS); + + PyObject *py_config_dict = NULL, + *py_result = NULL; + + if (!PyArg_ParseTuple(py_args, "O!:sudo.options_from_dict", &PyDict_Type, &py_config_dict)) + goto cleanup; + + Py_ssize_t dict_size = PyDict_Size(py_config_dict); + py_result = PyTuple_New(dict_size); + if (py_result == NULL) + goto cleanup; + + PyObject *py_key = NULL, *py_value = NULL; // -> borrowed references + Py_ssize_t i, pos = 0; + for (i = 0; PyDict_Next(py_config_dict, &pos, &py_key, &py_value); i++) { + PyObject *py_config = PyUnicode_FromFormat("%S%s%S", py_key, "=", py_value); + if (py_config == NULL) + goto cleanup; + + /* Dictionaries are sparse so we cannot use pos as an index. */ + if (PyTuple_SetItem(py_result, i, py_config) != 0) { // this steals a reference, even on error + goto cleanup; + } + } + +cleanup: + if (PyErr_Occurred()) { + Py_CLEAR(py_result); + } + + py_debug_python_result("sudo", "options_from_dict", py_result, PYTHON_DEBUG_C_CALLS); + debug_return_ptr(py_result); +} + +static PyObject * +python_sudo_log_info(PyObject *py_self, PyObject *py_args, PyObject *py_kwargs) +{ + return python_sudo_log(SUDO_CONV_INFO_MSG, py_self, py_args, py_kwargs); +} + +static PyObject * +python_sudo_log_error(PyObject *py_self, PyObject *py_args, PyObject *py_kwargs) +{ + return python_sudo_log(SUDO_CONV_ERROR_MSG, py_self, py_args, py_kwargs); +} + +CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION +static int py_expect_arg_callable(PyObject *py_callable, + const char *func_name, const char *arg_name) +{ + debug_decl(py_expect_arg_callable, PYTHON_DEBUG_INTERNAL); + + if (!PyCallable_Check(py_callable)) { + PyErr_Format(PyExc_ValueError, "%s: %s argument must be python callable (got %s) ", + func_name, arg_name, Py_TYPENAME(py_callable)); + debug_return_int(-1); + } + + debug_return_int(0); +} + +struct py_conv_callback_closure +{ + PyObject *py_on_suspend; + PyObject *py_on_resume; +}; + +static int +_call_conversation_callback(PyObject *py_callback, int signo) +{ + debug_decl(_call_conversation_callback, PYTHON_DEBUG_INTERNAL); + + if (py_callback == NULL || py_callback == Py_None) + debug_return_int(0); // nothing to do + + PyObject *py_result = PyObject_CallFunction(py_callback, "(i)", signo); + + int rc = -1; + + // We treat sudo.RC_OK (1) and None (no exception occurred) as success as well to avoid confusion + if (py_result && (py_result == Py_None || PyLong_AsLong(py_result) >= 0)) + rc = 0; + + Py_CLEAR(py_result); + + if (rc != 0) + py_log_last_error("Error during conversation callback"); + + debug_return_int(rc); +} + +static int +python_sudo_conversation_suspend_cb(int signo, struct py_conv_callback_closure *closure) +{ + return _call_conversation_callback(closure->py_on_suspend, signo); +} + +static int +python_sudo_conversation_resume_cb(int signo, struct py_conv_callback_closure *closure) +{ + return _call_conversation_callback(closure->py_on_resume, signo); +} + +static PyObject * +python_sudo_conversation(PyObject *Py_UNUSED(self), PyObject *py_args, PyObject *py_kwargs) +{ + debug_decl(python_sudo_conversation, PYTHON_DEBUG_C_CALLS); + py_debug_python_call("sudo", "conv", py_args, py_kwargs, PYTHON_DEBUG_C_CALLS); + + PyObject *py_result = NULL, *py_empty = NULL; + Py_ssize_t num_msgs = 0; + struct sudo_conv_message *msgs = NULL; + struct sudo_conv_reply *replies = NULL; + + // Note, they are both borrowed references of py_kwargs + struct py_conv_callback_closure callback_closure = { NULL, NULL }; + + struct sudo_conv_callback callback = { + SUDO_CONV_CALLBACK_VERSION, + &callback_closure, + (sudo_conv_callback_fn_t)python_sudo_conversation_suspend_cb, + (sudo_conv_callback_fn_t)python_sudo_conversation_resume_cb + }; + + py_empty = PyTuple_New(0); + if (py_empty == NULL) + goto cleanup; + + static const char *keywords[] = { "on_suspend", "on_resume", NULL }; + if (py_kwargs != NULL && !PyArg_ParseTupleAndKeywords(py_empty, py_kwargs, "|OO:sudo.conv", (char **)keywords, + &callback_closure.py_on_suspend, + &callback_closure.py_on_resume)) + goto cleanup; + + if (callback_closure.py_on_suspend != NULL && + py_expect_arg_callable(callback_closure.py_on_suspend, "sudo.conv", "on_suspend") < 0) { + goto cleanup; + } + + if (callback_closure.py_on_resume != NULL && + py_expect_arg_callable(callback_closure.py_on_resume, "sudo.conv", "on_resume") < 0) { + goto cleanup; + } + + /* sudo_module_ConvMessages_to_c() returns error if no messages. */ + if (sudo_module_ConvMessages_to_c(py_args, &num_msgs, &msgs) < 0) { + goto cleanup; + } + + replies = calloc((size_t)num_msgs, sizeof(struct sudo_conv_reply)); + if (replies == NULL) + goto cleanup; + py_result = PyTuple_New(num_msgs); + if (py_result == NULL) + goto cleanup; + + if (py_ctx.sudo_conv == NULL) { + PyErr_Format(sudo_exc_SudoException, "%s: conversation is unavailable", + __func__); + goto cleanup; + } + + int rc = py_sudo_conv((int)num_msgs, msgs, replies, &callback); + if (rc != 0) { + PyErr_Format(sudo_exc_ConversationInterrupted, + "%s: conversation was interrupted", __func__, rc); + goto cleanup; + } + + for (Py_ssize_t i = 0; i < num_msgs; ++i) { + char *reply = replies[i].reply; + if (reply != NULL) { + PyObject *py_reply = PyUnicode_FromString(reply); + if (py_reply == NULL) { + goto cleanup; + } + + if (PyTuple_SetItem(py_result, i, py_reply) != 0) { // this steals a reference even on error + PyErr_Format(sudo_exc_SudoException, "%s: failed to set tuple item", __func__); + goto cleanup; + } + + sudo_debug_printf(SUDO_DEBUG_DIAG, "user reply for conversation: '%s'\n", reply); + } + } + +cleanup: + Py_CLEAR(py_empty); + if (replies != NULL) { + for (int i = 0; i < num_msgs; ++i) + free(replies[i].reply); + } + free(msgs); + free(replies); + + if (PyErr_Occurred()) { + Py_CLEAR(py_result); // we return NULL + } + + py_debug_python_result("sudo", "conv", py_result, PYTHON_DEBUG_C_CALLS); + + debug_return_ptr(py_result); +} + +/* + * Create a python class. + * Class name must be a full name including module, eg. "sudo.MyFavouriteClass". + * The resulting class object can be added to a module using PyModule_AddObject. + */ +PyObject * +sudo_module_create_class(const char *class_name, PyMethodDef *class_methods, + PyObject *base_class) +{ + debug_decl(sudo_module_create_class, PYTHON_DEBUG_INTERNAL); + + PyObject *py_base_classes = NULL, *py_class = NULL, *py_member_dict = NULL; + + if (base_class == NULL) { + py_base_classes = PyTuple_New(0); + } else { + py_base_classes = Py_BuildValue("(O)", base_class); + } + + if (py_base_classes == NULL) + goto cleanup; + + py_member_dict = PyDict_New(); + if (py_member_dict == NULL) + goto cleanup; + + for (PyMethodDef *py_def = class_methods; py_def->ml_name != NULL; ++py_def) { + PyObject *py_func = PyCFunction_New(py_def, NULL); + if (py_func == NULL) { + goto cleanup; + } + + // this wrapping makes the function get the 'self' as argument + PyObject *py_method = PyInstanceMethod_New(py_func); + if (py_method == NULL) { + Py_DECREF(py_func); + goto cleanup; + } + + int rc = PyDict_SetItemString(py_member_dict, py_def->ml_name, py_method); + + Py_XDECREF(py_func); + Py_XDECREF(py_method); + + if (rc != 0) + goto cleanup; + } + + py_class = PyObject_CallFunction((PyObject *)&PyType_Type, "(sOO)", + class_name, + py_base_classes, + py_member_dict); + +cleanup: + Py_CLEAR(py_base_classes); + Py_CLEAR(py_member_dict); + + debug_return_ptr(py_class); +} + +CPYCHECKER_STEALS_REFERENCE_TO_ARG(3) +static void +sudo_module_register_enum(PyObject *py_module, const char *enum_name, PyObject *py_constants_dict) +{ + // pseudo code: + // return enum.IntEnum('MyEnum', {'DEFINITION_NAME': DEFINITION_VALUE, ...}) + + debug_decl(sudo_module_register_enum, PYTHON_DEBUG_INTERNAL); + + if (py_constants_dict == NULL) + return; + + PyObject *py_enum_class = NULL; + PyObject *py_enum_module = PyImport_ImportModule("enum"); + if (py_enum_module == NULL) { + Py_CLEAR(py_constants_dict); + debug_return; + } + + py_enum_class = PyObject_CallMethod(py_enum_module, + "IntEnum", "sO", enum_name, + py_constants_dict); + + Py_CLEAR(py_constants_dict); + Py_CLEAR(py_enum_module); + + if (py_enum_class == NULL) { + debug_return; + } + + // PyModule_AddObject steals the reference to py_enum_class on success + if (PyModule_AddObject(py_module, enum_name, py_enum_class) < 0) { + Py_CLEAR(py_enum_class); + } + + debug_return; +} + +PyMODINIT_FUNC +sudo_module_init(void) +{ + debug_decl(sudo_module_init, PYTHON_DEBUG_C_CALLS); + + PyObject *py_module = PyModule_Create(&sudo_module); + + if (py_module == NULL) + debug_return_ptr(NULL); + + // Note: "PyModule_AddObject()" decrements the refcount only on success + + // exceptions + #define MODULE_ADD_EXCEPTION(exception_name, base_exception) \ + do { \ + EXC_VAR(exception_name) = PyErr_NewException("sudo." # exception_name, base_exception, NULL); \ + if (EXC_VAR(exception_name) == NULL || PyModule_AddObject(py_module, # exception_name, EXC_VAR(exception_name)) < 0) { \ + Py_CLEAR(EXC_VAR(exception_name)); \ + goto cleanup; \ + } \ + Py_INCREF(EXC_VAR(exception_name)); \ + } while(0); + + MODULE_ADD_EXCEPTION(SudoException, NULL); + + MODULE_ADD_EXCEPTION(PluginException, NULL); + MODULE_ADD_EXCEPTION(PluginError, EXC_VAR(PluginException)); + MODULE_ADD_EXCEPTION(PluginReject, EXC_VAR(PluginException)); + + MODULE_ADD_EXCEPTION(ConversationInterrupted, EXC_VAR(SudoException)); + + #define MODULE_REGISTER_ENUM(name, key_values) \ + sudo_module_register_enum(py_module, name, py_dict_create_string_int(\ + sizeof(key_values) / sizeof(struct key_value_str_int), key_values)) + + // constants + struct key_value_str_int constants_rc[] = { + {"OK", SUDO_RC_OK}, + {"ACCEPT", SUDO_RC_ACCEPT}, + {"REJECT", SUDO_RC_REJECT}, + {"ERROR", SUDO_RC_ERROR}, + {"USAGE_ERROR", SUDO_RC_USAGE_ERROR} + }; + MODULE_REGISTER_ENUM("RC", constants_rc); + + struct key_value_str_int constants_conv[] = { + {"PROMPT_ECHO_OFF", SUDO_CONV_PROMPT_ECHO_OFF}, + {"PROMPT_ECHO_ON", SUDO_CONV_PROMPT_ECHO_ON}, + {"INFO_MSG", SUDO_CONV_INFO_MSG}, + {"PROMPT_MASK", SUDO_CONV_PROMPT_MASK}, + {"PROMPT_ECHO_OK", SUDO_CONV_PROMPT_ECHO_OK}, + {"PREFER_TTY", SUDO_CONV_PREFER_TTY} + }; + MODULE_REGISTER_ENUM("CONV", constants_conv); + + struct key_value_str_int constants_debug[] = { + {"CRIT", SUDO_DEBUG_CRIT}, + {"ERROR", SUDO_DEBUG_ERROR}, + {"WARN", SUDO_DEBUG_WARN}, + {"NOTICE", SUDO_DEBUG_NOTICE}, + {"DIAG", SUDO_DEBUG_DIAG}, + {"INFO", SUDO_DEBUG_INFO}, + {"TRACE", SUDO_DEBUG_TRACE}, + {"DEBUG", SUDO_DEBUG_DEBUG} + }; + MODULE_REGISTER_ENUM("DEBUG", constants_debug); + + struct key_value_str_int constants_exit_reason[] = { + {"NO_STATUS", SUDO_PLUGIN_NO_STATUS}, + {"WAIT_STATUS", SUDO_PLUGIN_WAIT_STATUS}, + {"EXEC_ERROR", SUDO_PLUGIN_EXEC_ERROR}, + {"SUDO_ERROR", SUDO_PLUGIN_SUDO_ERROR} + }; + MODULE_REGISTER_ENUM("EXIT_REASON", constants_exit_reason); + + struct key_value_str_int constants_plugin_types[] = { + {"POLICY", SUDO_POLICY_PLUGIN}, + {"AUDIT", SUDO_AUDIT_PLUGIN}, + {"IO", SUDO_IO_PLUGIN}, + {"APPROVAL", SUDO_APPROVAL_PLUGIN}, + {"SUDO", SUDO_FRONT_END} + }; + MODULE_REGISTER_ENUM("PLUGIN_TYPE", constants_plugin_types); + + // classes + if (sudo_module_register_conv_message(py_module) != SUDO_RC_OK) + goto cleanup; + + if (sudo_module_register_baseplugin(py_module) != SUDO_RC_OK) + goto cleanup; + +cleanup: + if (PyErr_Occurred()) { + Py_CLEAR(py_module); + Py_CLEAR(sudo_exc_SudoException); + Py_CLEAR(sudo_exc_PluginError); + Py_CLEAR(sudo_exc_PluginReject); + Py_CLEAR(sudo_exc_ConversationInterrupted); + } + + debug_return_ptr(py_module); +} + +static void +sudo_module_free(void *self) +{ + debug_decl(sudo_module_free, PYTHON_DEBUG_C_CALLS); + + // Free exceptions + Py_CLEAR(sudo_exc_SudoException); + Py_CLEAR(sudo_exc_PluginError); + Py_CLEAR(sudo_exc_PluginReject); + Py_CLEAR(sudo_exc_ConversationInterrupted); + + // Free base plugin + Py_CLEAR(sudo_type_Plugin); + + // Free conversation message type + Py_CLEAR(sudo_type_ConvMessage); + + debug_return; +} |