diff --git a/doc/sudo_plugin_python.man.in b/doc/sudo_plugin_python.man.in index 6b92d48ac..65a852f41 100644 --- a/doc/sudo_plugin_python.man.in +++ b/doc/sudo_plugin_python.man.in @@ -186,7 +186,11 @@ It must be either an absolute path or a path relative to the sudo Python plugin directory: "@plugindir@/python". .TP 6n ClassName -The name of the class implementing the sudo Python plugin. +(Optional.) The name of the class implementing the sudo Python plugin. +If not supplied, the one and only sudo.Plugin that is present in the module +will be used. +If there are multiple such plugins in the module (or none), it +will result in an error. .SS "Policy plugin API" Policy plugins must be registered in sudo.conf(@mansectform@). diff --git a/doc/sudo_plugin_python.mdoc.in b/doc/sudo_plugin_python.mdoc.in index 395f2f211..9cb657871 100644 --- a/doc/sudo_plugin_python.mdoc.in +++ b/doc/sudo_plugin_python.mdoc.in @@ -156,7 +156,11 @@ The path of a python file which contains the class of the sudo Python plugin. It must be either an absolute path or a path relative to the sudo Python plugin directory: "@plugindir@/python". .It ClassName -The name of the class implementing the sudo Python plugin. +(Optional.) The name of the class implementing the sudo Python plugin. +If not supplied, the one and only sudo.Plugin that is present in the module +will be used. +If there are multiple such plugins in the module (or none), it +will result in an error. .El .Ss Policy plugin API Policy plugins must be registered in diff --git a/plugins/python/python_plugin_common.c b/plugins/python/python_plugin_common.c index e5f44244d..b5c7a7ed9 100644 --- a/plugins/python/python_plugin_common.c +++ b/plugins/python/python_plugin_common.c @@ -399,6 +399,82 @@ _python_plugin_set_path(struct PluginContext *plugin_ctx, const char *path) return SUDO_RC_OK; } +/* Returns the list of sudo.Plugins in a module */ +static PyObject * +_python_plugin_class_list(PyObject *py_module) { + PyObject *py_module_dict = PyModule_GetDict(py_module); // Note: borrowed + PyObject *key, *value; // Note: borrowed + Py_ssize_t pos = 0; + PyObject *py_plugin_list = PyList_New(0); + + while (PyDict_Next(py_module_dict, &pos, &key, &value)) { + if (PyObject_IsSubclass(value, (PyObject *)sudo_type_Plugin) == 1) { + if (PyList_Append(py_plugin_list, key) != 0) + goto cleanup; + } else { + PyErr_Clear(); + } + } + +cleanup: + if (PyErr_Occurred()) { + Py_CLEAR(py_plugin_list); + } + return py_plugin_list; +} + +/* Gets a sudo.Plugin class from the specified module. The argument "plugin_class" + * can be NULL in which case it loads the one and only "sudo.Plugin" present + * in the module (if so), or displays helpful error message. */ +static PyObject * +_python_plugin_get_class(const char *plugin_path, PyObject *py_module, const char *plugin_class) +{ + debug_decl(python_plugin_init, PYTHON_DEBUG_PLUGIN_LOAD); + PyObject *py_plugin_list = NULL, *py_class = NULL; + + if (plugin_class == NULL) { + py_plugin_list = _python_plugin_class_list(py_module); + if (py_plugin_list == NULL) { + goto cleanup; + } + + if (PyList_Size(py_plugin_list) == 1) { + PyObject *py_plugin_name = PyList_GetItem(py_plugin_list, 0); // Note: borrowed + plugin_class = PyUnicode_AsUTF8(py_plugin_name); + } + } + + if (plugin_class == NULL) { + py_sudo_log(SUDO_CONV_ERROR_MSG, "No plugin class is specified for python module '%s'. " + "Use 'ClassName' configuration option in 'sudo.conf'\n", plugin_path); + if (py_plugin_list != NULL) { + char *possible_plugins = py_join_str_list(py_plugin_list, ", "); + if (possible_plugins != NULL) { + py_sudo_log(SUDO_CONV_ERROR_MSG, "Possible plugins: %s\n", possible_plugins); + free(possible_plugins); + } + } + goto cleanup; + } + + sudo_debug_printf(SUDO_DEBUG_DEBUG, "Using plugin class '%s'", plugin_class); + py_class = PyObject_GetAttrString(py_module, plugin_class); + if (py_class == NULL) { + py_sudo_log(SUDO_CONV_ERROR_MSG, "Failed to find plugin class '%s'\n", plugin_class); + goto cleanup; + } + + if (!PyObject_IsSubclass(py_class, (PyObject *)sudo_type_Plugin)) { + py_sudo_log(SUDO_CONV_ERROR_MSG, "Plugin class '%s' does not inherit from 'sudo.Plugin'\n", plugin_class); + Py_CLEAR(py_class); + goto cleanup; + } + +cleanup: + Py_CLEAR(py_plugin_list); + debug_return_ptr(py_class); +} + int python_plugin_init(struct PluginContext *plugin_ctx, char * const plugin_options[], unsigned int version) @@ -419,8 +495,7 @@ python_plugin_init(struct PluginContext *plugin_ctx, char * const plugin_options PyThreadState_Swap(plugin_ctx->py_interpreter); if (!sudo_conf_developer_mode() && sudo_module_register_importblocker() < 0) { - py_log_last_error(NULL); - debug_return_int(SUDO_RC_ERROR); + goto cleanup; } if (_python_plugin_set_path(plugin_ctx, _lookup_value(plugin_options, "ModulePath")) != SUDO_RC_OK) { @@ -433,23 +508,9 @@ python_plugin_init(struct PluginContext *plugin_ctx, char * const plugin_options goto cleanup; } - const char *plugin_class = _lookup_value(plugin_options, "ClassName"); - if (plugin_class == NULL) { - py_sudo_log(SUDO_CONV_ERROR_MSG, "No plugin class is specified for python module '%s'. " - "Use 'ClassName' configuration option in 'sudo.conf'\n", plugin_ctx->plugin_path); - goto cleanup; - } - - sudo_debug_printf(SUDO_DEBUG_DEBUG, "Using plugin class '%s'", plugin_class); - plugin_ctx->py_class = PyObject_GetAttrString(plugin_ctx->py_module, plugin_class); + plugin_ctx->py_class = _python_plugin_get_class(plugin_ctx->plugin_path, plugin_ctx->py_module, + _lookup_value(plugin_options, "ClassName")); if (plugin_ctx->py_class == NULL) { - py_sudo_log(SUDO_CONV_ERROR_MSG, "Failed to find plugin class '%s'\n", plugin_class); - goto cleanup; - } - - if (!PyObject_IsSubclass(plugin_ctx->py_class, (PyObject *)sudo_type_Plugin)) { - py_sudo_log(SUDO_CONV_ERROR_MSG, "Plugin class '%s' does not inherit from 'sudo.Plugin'\n", plugin_class); - Py_CLEAR(plugin_ctx->py_class); goto cleanup; } diff --git a/plugins/python/regress/check_python_examples.c b/plugins/python/regress/check_python_examples.c index de5a41390..ca1e60016 100644 --- a/plugins/python/regress/check_python_examples.c +++ b/plugins/python/regress/check_python_examples.c @@ -584,10 +584,31 @@ check_loading_fails_with_missing_path(void) } int -check_loading_fails_with_missing_classname(void) +check_loading_succeeds_with_missing_classname(void) { str_array_free(&data.plugin_options); data.plugin_options = create_str_array(2, "ModulePath=" SRC_DIR "/example_debugging.py", NULL); + + const char *errstr = NULL; + + VERIFY_INT(python_io->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings, + data.user_info, data.command_info, data.plugin_argc, data.plugin_argv, + data.user_env, data.plugin_options, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io->show_version(1), SUDO_RC_OK); + python_io->close(0, 0); + + VERIFY_STDOUT(expected_path("check_loading_succeeds_with_missing_classname.stdout")); + VERIFY_STR(data.stderr_str, ""); + + return true; +} + +int +check_loading_fails_with_missing_classname(void) +{ + str_array_free(&data.plugin_options); + data.plugin_options = create_str_array(2, "ModulePath=" SRC_DIR "/regress/plugin_errorstr.py", NULL); return check_loading_fails("missing_classname"); } @@ -1511,6 +1532,7 @@ main(int argc, char *argv[]) RUN_TEST(check_plugin_unload()); RUN_TEST(check_loading_fails_with_missing_path()); + RUN_TEST(check_loading_succeeds_with_missing_classname()); RUN_TEST(check_loading_fails_with_missing_classname()); RUN_TEST(check_loading_fails_with_wrong_classname()); RUN_TEST(check_loading_fails_with_wrong_path()); diff --git a/plugins/python/regress/testdata/check_loading_fails_missing_classname.stderr b/plugins/python/regress/testdata/check_loading_fails_missing_classname.stderr index b76ba36ad..81e4e95cb 100644 --- a/plugins/python/regress/testdata/check_loading_fails_missing_classname.stderr +++ b/plugins/python/regress/testdata/check_loading_fails_missing_classname.stderr @@ -1,2 +1,3 @@ -No plugin class is specified for python module 'SRC_DIR/example_debugging.py'. Use 'ClassName' configuration option in 'sudo.conf' +No plugin class is specified for python module 'SRC_DIR/regress/plugin_errorstr.py'. Use 'ClassName' configuration option in 'sudo.conf' +Possible plugins: ErrorMsgPlugin, ConstructErrorPlugin Failed during loading plugin class diff --git a/plugins/python/regress/testdata/check_loading_succeeds_with_missing_classname.stdout b/plugins/python/regress/testdata/check_loading_succeeds_with_missing_classname.stdout new file mode 100644 index 000000000..f7a1a6f43 --- /dev/null +++ b/plugins/python/regress/testdata/check_loading_succeeds_with_missing_classname.stdout @@ -0,0 +1 @@ +Python io plugin (API 1.0): DebugDemoPlugin (loaded from 'SRC_DIR/example_debugging.py')