diff options
Diffstat (limited to '')
77 files changed, 2729 insertions, 0 deletions
diff --git a/plugins/python/regress/check_python_examples.c b/plugins/python/regress/check_python_examples.c new file mode 100644 index 0000000..b9b9077 --- /dev/null +++ b/plugins/python/regress/check_python_examples.c @@ -0,0 +1,1616 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 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 "testhelpers.h" +#include <unistd.h> + +#include "sudo_dso.h" + +#define DECL_PLUGIN(type, variable_name) \ + static struct type *variable_name = NULL; \ + static struct type variable_name ## _original + +#define RESTORE_PYTHON_PLUGIN(variable_name) \ + memcpy(variable_name, &(variable_name ## _original), sizeof(variable_name ## _original)) + +#define SAVE_PYTHON_PLUGIN(variable_name) \ + memcpy(&(variable_name ## _original), variable_name, sizeof(variable_name ## _original)) + +static const char *python_plugin_so_path = NULL; +static void *python_plugin_handle = NULL; +DECL_PLUGIN(io_plugin, python_io); +DECL_PLUGIN(policy_plugin, python_policy); +DECL_PLUGIN(approval_plugin, python_approval); +DECL_PLUGIN(audit_plugin, python_audit); +DECL_PLUGIN(sudoers_group_plugin, group_plugin); + +static struct passwd example_pwd; +static bool verbose; + +static int _init_symbols(void); +static int _unlink_symbols(void); + +static void +create_plugin_options(const char *module_name, const char *class_name, const char *extra_option) +{ + char opt_module_path[PATH_MAX + 256]; + char opt_classname[PATH_MAX + 256]; + snprintf(opt_module_path, sizeof(opt_module_path), + "ModulePath=" SRC_DIR "/%s.py", module_name); + + snprintf(opt_classname, sizeof(opt_classname), "ClassName=%s", class_name); + + str_array_free(&data.plugin_options); + size_t count = 3 + (extra_option != NULL); + data.plugin_options = create_str_array(count, opt_module_path, + opt_classname, extra_option, NULL); +} + +static void +create_io_plugin_options(const char *log_path) +{ + char opt_logpath[PATH_MAX + 16]; + snprintf(opt_logpath, sizeof(opt_logpath), "LogPath=%s", log_path); + create_plugin_options("example_io_plugin", "SudoIOPlugin", opt_logpath); +} + +static void +create_debugging_plugin_options(void) +{ + create_plugin_options("example_debugging", "DebugDemoPlugin", NULL); +} + +static void +create_audit_plugin_options(const char *extra_argument) +{ + create_plugin_options("example_audit_plugin", "SudoAuditPlugin", extra_argument); +} + +static void +create_conversation_plugin_options(void) +{ + char opt_logpath[PATH_MAX + 16]; + snprintf(opt_logpath, sizeof(opt_logpath), "LogPath=%s", data.tmp_dir); + create_plugin_options("example_conversation", "ReasonLoggerIOPlugin", opt_logpath); +} + +static void +create_policy_plugin_options(void) +{ + create_plugin_options("example_policy_plugin", "SudoPolicyPlugin", NULL); +} + +static int +init(void) +{ + // always start each test from clean state + memset(&data, 0, sizeof(data)); + + memset(&example_pwd, 0, sizeof(example_pwd)); + example_pwd.pw_name = (char *)"pw_name"; + example_pwd.pw_passwd = (char *)"pw_passwd"; + example_pwd.pw_gecos = (char *)"pw_gecos"; + example_pwd.pw_shell = (char *)"pw_shell"; + example_pwd.pw_dir = (char *)"pw_dir"; + example_pwd.pw_uid = (uid_t)1001; + example_pwd.pw_gid = (gid_t)101; + + VERIFY_TRUE(asprintf(&data.tmp_dir, TEMP_PATH_TEMPLATE) >= 0); + VERIFY_NOT_NULL(mkdtemp(data.tmp_dir)); + + sudo_conf_clear_paths(); + + // some default values for the plugin open: + data.settings = create_str_array(1, NULL); + data.user_info = create_str_array(1, NULL); + data.command_info = create_str_array(1, NULL); + data.plugin_argc = 0; + data.plugin_argv = create_str_array(1, NULL); + data.user_env = create_str_array(1, NULL); + + VERIFY_TRUE(_init_symbols()); + return true; +} + +static int +cleanup(int success) +{ + if (!success) { + printf("\nThe output of the plugin:\n%s", data.stdout_str); + printf("\nThe error output of the plugin:\n%s", data.stderr_str); + } + + VERIFY_TRUE(rmdir_recursive(data.tmp_dir)); + if (data.tmp_dir2) { + VERIFY_TRUE(rmdir_recursive(data.tmp_dir2)); + } + + free(data.tmp_dir); + free(data.tmp_dir2); + + str_array_free(&data.settings); + str_array_free(&data.user_info); + str_array_free(&data.command_info); + str_array_free(&data.plugin_argv); + str_array_free(&data.user_env); + str_array_free(&data.plugin_options); + + return true; +} + +static int +check_example_io_plugin_version_display(int is_verbose) +{ + const char *errstr = NULL; + create_io_plugin_options(data.tmp_dir); + + 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_INT(python_io->show_version(is_verbose), SUDO_RC_OK); + + python_io->close(0, 0); // this should not call the python plugin close as there was no command run invocation + + if (is_verbose) { + // Note: the exact python version is environment dependent + VERIFY_STR_CONTAINS(data.stdout_str, "Python interpreter version:"); + *strstr(data.stdout_str, "Python interpreter version:") = '\0'; + VERIFY_STDOUT(expected_path("check_example_io_plugin_version_display_full.stdout")); + } else { + VERIFY_STDOUT(expected_path("check_example_io_plugin_version_display.stdout")); + } + + VERIFY_STDERR(expected_path("check_example_io_plugin_version_display.stderr")); + VERIFY_FILE("sudo.log", expected_path("check_example_io_plugin_version_display.stored")); + + return true; +} + +static int +check_example_io_plugin_command_log(void) +{ + const char *errstr = NULL; + create_io_plugin_options(data.tmp_dir); + + str_array_free(&data.plugin_argv); + data.plugin_argc = 2; + data.plugin_argv = create_str_array(3, "id", "--help", NULL); + + str_array_free(&data.command_info); + data.command_info = create_str_array(3, "command=/bin/id", "runas_uid=0", 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->log_stdin("some standard input", strlen("some standard input"), &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io->log_stdout("some standard output", strlen("some standard output"), &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io->log_stderr("some standard error", strlen("some standard error"), &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io->log_suspend(SIGTSTP, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io->log_suspend(SIGCONT, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io->change_winsize(200, 100, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io->log_ttyin("some tty input", strlen("some tty input"), &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io->log_ttyout("some tty output", strlen("some tty output"), &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + python_io->close(1, 0); // successful execution, command returned 1 + + VERIFY_STDOUT(expected_path("check_example_io_plugin_command_log.stdout")); + VERIFY_STDERR(expected_path("check_example_io_plugin_command_log.stderr")); + VERIFY_FILE("sudo.log", expected_path("check_example_io_plugin_command_log.stored")); + + return true; +} + +typedef struct io_plugin * (io_clone_func)(void); + +static int +check_example_io_plugin_command_log_multiple(void) +{ + const char *errstr = NULL; + + // verify multiple python io plugin symbols are available + io_clone_func *python_io_clone = (io_clone_func *)sudo_dso_findsym(python_plugin_handle, "python_io_clone"); + VERIFY_PTR_NE(python_io_clone, NULL); + + struct io_plugin *python_io2 = NULL; + + for (int i = 0; i < 7; ++i) { + python_io2 = (*python_io_clone)(); + VERIFY_PTR_NE(python_io2, NULL); + VERIFY_PTR_NE(python_io2, python_io); + } + + // open the first plugin and let it log to tmp_dir + create_io_plugin_options(data.tmp_dir); + + str_array_free(&data.plugin_argv); + data.plugin_argc = 2; + data.plugin_argv = create_str_array(3, "id", "--help", NULL); + + str_array_free(&data.command_info); + data.command_info = create_str_array(3, "command=/bin/id", "runas_uid=0", 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); + + // For verifying the error message of no more plugin. It should be displayed only once. + VERIFY_PTR((*python_io_clone)(), NULL); + VERIFY_PTR((*python_io_clone)(), NULL); + + // open the second plugin with another log directory + VERIFY_TRUE(asprintf(&data.tmp_dir2, TEMP_PATH_TEMPLATE) >= 0); + VERIFY_NOT_NULL(mkdtemp(data.tmp_dir2)); + create_io_plugin_options(data.tmp_dir2); + + str_array_free(&data.plugin_argv); + data.plugin_argc = 1; + data.plugin_argv = create_str_array(2, "whoami", NULL); + + str_array_free(&data.command_info); + data.command_info = create_str_array(3, "command=/bin/whoami", "runas_uid=1", NULL); + + VERIFY_INT(python_io2->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->log_stdin("stdin for plugin 1", strlen("stdin for plugin 1"), &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io2->log_stdin("stdin for plugin 2", strlen("stdin for plugin 2"), &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io->log_stdout("stdout for plugin 1", strlen("stdout for plugin 1"), &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io2->log_stdout("stdout for plugin 2", strlen("stdout for plugin 2"), &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io->log_stderr("stderr for plugin 1", strlen("stderr for plugin 1"), &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io2->log_stderr("stderr for plugin 2", strlen("stderr for plugin 2"), &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io->log_suspend(SIGTSTP, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io2->log_suspend(SIGSTOP, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io->log_suspend(SIGCONT, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io2->log_suspend(SIGCONT, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io->change_winsize(20, 10, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io2->change_winsize(30, 40, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io->log_ttyin("tty input for plugin 1", strlen("tty input for plugin 1"), &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io2->log_ttyin("tty input for plugin 2", strlen("tty input for plugin 2"), &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io->log_ttyout("tty output for plugin 1", strlen("tty output for plugin 1"), &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_io2->log_ttyout("tty output for plugin 2", strlen("tty output for plugin 2"), &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + python_io->close(1, 0); // successful execution, command returned 1 + python_io2->close(2, 0); // command returned 2 + + VERIFY_STDOUT(expected_path("check_example_io_plugin_command_log_multiple.stdout")); + VERIFY_STDERR(expected_path("check_example_io_plugin_command_log_multiple.stderr")); + VERIFY_FILE("sudo.log", expected_path("check_example_io_plugin_command_log_multiple1.stored")); + VERIFY_TRUE(verify_file(data.tmp_dir2, "sudo.log", expected_path("check_example_io_plugin_command_log_multiple2.stored"))); + + return true; +} + +static int +check_example_io_plugin_failed_to_start_command(void) +{ + const char *errstr = NULL; + + create_io_plugin_options(data.tmp_dir); + + str_array_free(&data.plugin_argv); + data.plugin_argc = 1; + data.plugin_argv = create_str_array(2, "cmd", NULL); + + str_array_free(&data.command_info); + data.command_info = create_str_array(3, "command=/usr/share/cmd", "runas_uid=0", 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); + + python_io->close(0, EPERM); // execve returned with error + + VERIFY_STDOUT(expected_path("check_example_io_plugin_failed_to_start_command.stdout")); + VERIFY_STDERR(expected_path("check_example_io_plugin_failed_to_start_command.stderr")); + VERIFY_FILE("sudo.log", expected_path("check_example_io_plugin_failed_to_start_command.stored")); + + return true; +} + +static int +check_example_io_plugin_fails_with_python_backtrace(void) +{ + const char *errstr = NULL; + + create_io_plugin_options("/some/not/writable/directory"); + + 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_ERROR); + VERIFY_PTR(errstr, NULL); + + VERIFY_STDOUT(expected_path("check_example_io_plugin_fails_with_python_backtrace.stdout")); + VERIFY_STDERR(expected_path("check_example_io_plugin_fails_with_python_backtrace.stderr")); + + python_io->close(0, 0); + return true; +} + +static int +check_io_plugin_reports_error(void) +{ + const char *errstr = NULL; + str_array_free(&data.plugin_options); + data.plugin_options = create_str_array( + 3, + "ModulePath=" SRC_DIR "/regress/plugin_errorstr.py", + "ClassName=ConstructErrorPlugin", + 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_ERROR); + + VERIFY_STR(errstr, "Something wrong in plugin constructor"); + errstr = NULL; + + python_io->close(0, 0); + + str_array_free(&data.plugin_options); + data.plugin_options = create_str_array( + 3, + "ModulePath=" SRC_DIR "/regress/plugin_errorstr.py", + "ClassName=ErrorMsgPlugin", + 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->log_stdin("", 0, &errstr), SUDO_RC_ERROR); + VERIFY_STR(errstr, "Something wrong in log_stdin"); + + errstr = (void *)13; + VERIFY_INT(python_io->log_stdout("", 0, &errstr), SUDO_RC_ERROR); + VERIFY_STR(errstr, "Something wrong in log_stdout"); + + errstr = NULL; + VERIFY_INT(python_io->log_stderr("", 0, &errstr), SUDO_RC_ERROR); + VERIFY_STR(errstr, "Something wrong in log_stderr"); + + errstr = NULL; + VERIFY_INT(python_io->log_ttyin("", 0, &errstr), SUDO_RC_ERROR); + VERIFY_STR(errstr, "Something wrong in log_ttyin"); + + errstr = NULL; + VERIFY_INT(python_io->log_ttyout("", 0, &errstr), SUDO_RC_ERROR); + VERIFY_STR(errstr, "Something wrong in log_ttyout"); + + errstr = NULL; + VERIFY_INT(python_io->log_suspend(SIGTSTP, &errstr), SUDO_RC_ERROR); + VERIFY_STR(errstr, "Something wrong in log_suspend"); + + errstr = NULL; + VERIFY_INT(python_io->change_winsize(200, 100, &errstr), SUDO_RC_ERROR); + VERIFY_STR(errstr, "Something wrong in change_winsize"); + + python_io->close(0, 0); + + VERIFY_STR(data.stderr_str, ""); + VERIFY_STR(data.stdout_str, ""); + return true; +} + +static int +check_example_group_plugin(void) +{ + create_plugin_options("example_group_plugin", "SudoGroupPlugin", NULL); + + VERIFY_INT(group_plugin->init(GROUP_API_VERSION, fake_printf, data.plugin_options), SUDO_RC_OK); + + VERIFY_INT(group_plugin->query("test", "mygroup", NULL), SUDO_RC_OK); + VERIFY_INT(group_plugin->query("testuser2", "testgroup", NULL), SUDO_RC_OK); + VERIFY_INT(group_plugin->query("testuser2", "mygroup", NULL), SUDO_RC_REJECT); + VERIFY_INT(group_plugin->query("test", "testgroup", NULL), SUDO_RC_REJECT); + + group_plugin->cleanup(); + VERIFY_STR(data.stderr_str, ""); + VERIFY_STR(data.stdout_str, ""); + return true; +} + +#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION +static const char * +create_debug_config(const char *debug_spec) +{ + char *result = NULL; + + static char config_path[PATH_MAX] = "/"; + snprintf(config_path, sizeof(config_path), "%s/sudo.conf", data.tmp_dir); + + char *content = NULL; + if (asprintf(&content, "Debug %s %s/debug.log %s\n", + "python_plugin.so", data.tmp_dir, debug_spec) < 0) + { + printf("Failed to allocate string\n"); + goto cleanup; + } + + if (fwriteall(config_path, content) != true) { + printf("Failed to write '%s'\n", config_path); + goto cleanup; + } + + result = config_path; + +cleanup: + free(content); + + return result; +} + +static int +check_example_group_plugin_is_able_to_debug(void) +{ + const char *config_path = create_debug_config("py_calls@diag"); + VERIFY_NOT_NULL(config_path); + VERIFY_INT(sudo_conf_read(config_path, SUDO_CONF_ALL), true); + + create_plugin_options("example_group_plugin", "SudoGroupPlugin", NULL); + + group_plugin->init(GROUP_API_VERSION, fake_printf, data.plugin_options); + + group_plugin->query("user", "group", &example_pwd); + + group_plugin->cleanup(); + + VERIFY_STR(data.stderr_str, ""); + VERIFY_STR(data.stdout_str, ""); + + VERIFY_LOG_LINES(expected_path("check_example_group_plugin_is_able_to_debug.log")); + + return true; +} +#endif /* FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION */ + +static int +check_plugin_unload(void) +{ + // You can call this test to avoid having a lot of subinterpreters + // (each plugin->open starts one, and only plugin unlink closes) + // It only verifies that python was shut down correctly. + VERIFY_TRUE(Py_IsInitialized()); + VERIFY_TRUE(_unlink_symbols()); + VERIFY_FALSE(Py_IsInitialized()); // python interpreter could be stopped + return true; +} + +#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION +static int +check_example_debugging(const char *debug_spec) +{ + const char *errstr = NULL; + const char *config_path = create_debug_config(debug_spec); + VERIFY_NOT_NULL(config_path); + VERIFY_INT(sudo_conf_read(config_path, SUDO_CONF_ALL), true); + + create_debugging_plugin_options(); + + str_array_free(&data.settings); + char *debug_flags_setting = NULL; + VERIFY_TRUE(asprintf(&debug_flags_setting, "debug_flags=%s/debug.log %s", data.tmp_dir, debug_spec) >= 0); + + data.settings = create_str_array(3, debug_flags_setting, "plugin_path=python_plugin.so", 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); + python_io->close(0, 0); + + VERIFY_STR(data.stderr_str, ""); + VERIFY_STR(data.stdout_str, ""); + + VERIFY_LOG_LINES(expected_path("check_example_debugging_%s.log", debug_spec)); + + free(debug_flags_setting); + return true; +} +#endif /* FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION */ + +static int +check_loading_fails(const char *name) +{ + 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_ERROR); + VERIFY_PTR(errstr, NULL); + python_io->close(0, 0); + + VERIFY_STDOUT(expected_path("check_loading_fails_%s.stdout", name)); + VERIFY_STDERR(expected_path("check_loading_fails_%s.stderr", name)); + + return true; +} + +static int +check_loading_fails_with_missing_path(void) +{ + str_array_free(&data.plugin_options); + data.plugin_options = create_str_array(2, "ClassName=DebugDemoPlugin", NULL); + return check_loading_fails("missing_path"); +} + +static int +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; +} + +static 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"); +} + +static int +check_loading_fails_with_wrong_classname(void) +{ + create_plugin_options("example_debugging", "MispelledPluginName", NULL); + return check_loading_fails("wrong_classname"); +} + +static int +check_loading_fails_with_wrong_path(void) +{ + str_array_free(&data.plugin_options); + data.plugin_options = create_str_array(3, "ModulePath=/wrong_path.py", "ClassName=PluginName", NULL); + return check_loading_fails("wrong_path"); +} + +static int +check_example_conversation_plugin_reason_log(int simulate_suspend, const char *description) +{ + const char *errstr = NULL; + + create_conversation_plugin_options(); + + str_array_free(&data.plugin_argv); // have a command run + data.plugin_argc = 1; + data.plugin_argv = create_str_array(2, "/bin/whoami", NULL); + + data.conv_replies[0] = "my fake reason"; + data.conv_replies[1] = "my real secret reason"; + + sudo_conv_t conversation = simulate_suspend ? fake_conversation_with_suspend : fake_conversation; + + VERIFY_INT(python_io->open(SUDO_API_VERSION, 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); + python_io->close(0, 0); + + VERIFY_STDOUT(expected_path("check_example_conversation_plugin_reason_log_%s.stdout", description)); + VERIFY_STDERR(expected_path("check_example_conversation_plugin_reason_log_%s.stderr", description)); + VERIFY_CONV(expected_path("check_example_conversation_plugin_reason_log_%s.conversation", description)); + VERIFY_FILE("sudo_reasons.txt", expected_path("check_example_conversation_plugin_reason_log_%s.stored", description)); + return true; +} + +static int +check_example_conversation_plugin_user_interrupts(void) +{ + const char *errstr = NULL; + + create_conversation_plugin_options(); + + str_array_free(&data.plugin_argv); // have a command run + data.plugin_argc = 1; + data.plugin_argv = create_str_array(2, "/bin/whoami", NULL); + + data.conv_replies[0] = NULL; // this simulates user interrupt for the first question + + 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_REJECT); + VERIFY_PTR(errstr, NULL); + python_io->close(0, 0); + + VERIFY_STDOUT(expected_path("check_example_conversation_plugin_user_interrupts.stdout")); + VERIFY_STDERR(expected_path("check_example_conversation_plugin_user_interrupts.stderr")); + VERIFY_CONV(expected_path("check_example_conversation_plugin_user_interrupts.conversation")); + return true; +} + +static int +check_example_policy_plugin_version_display(int is_verbose) +{ + const char *errstr = NULL; + + create_policy_plugin_options(); + + VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings, + data.user_info, data.user_env, data.plugin_options, &errstr), + SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + VERIFY_INT(python_policy->show_version(is_verbose), SUDO_RC_OK); + + python_policy->close(0, 0); // this should not call the python plugin close as there was no command run invocation + + if (is_verbose) { + // Note: the exact python version is environment dependent + VERIFY_STR_CONTAINS(data.stdout_str, "Python interpreter version:"); + *strstr(data.stdout_str, "Python interpreter version:") = '\0'; + VERIFY_STDOUT(expected_path("check_example_policy_plugin_version_display_full.stdout")); + } else { + VERIFY_STDOUT(expected_path("check_example_policy_plugin_version_display.stdout")); + } + + VERIFY_STDERR(expected_path("check_example_policy_plugin_version_display.stderr")); + + return true; +} + +static int +check_example_policy_plugin_accepted_execution(void) +{ + const char *errstr = NULL; + + create_policy_plugin_options(); + + str_array_free(&data.plugin_argv); + data.plugin_argc = 2; + data.plugin_argv = create_str_array(3, "/bin/whoami", "--help", NULL); + + str_array_free(&data.user_env); + data.user_env = create_str_array(3, "USER_ENV1=VALUE1", "USER_ENV2=value2", NULL); + + VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings, + data.user_info, data.user_env, data.plugin_options, &errstr), + SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + char **env_add = create_str_array(3, "REQUESTED_ENV1=VALUE1", "REQUESTED_ENV2=value2", NULL); + + char **argv_out, **user_env_out, **command_info_out; // free to contain garbage + + VERIFY_INT(python_policy->check_policy(data.plugin_argc, data.plugin_argv, env_add, + &command_info_out, &argv_out, &user_env_out, &errstr), + SUDO_RC_ACCEPT); + VERIFY_PTR(errstr, NULL); + + VERIFY_STR_SET(command_info_out, 4, "command=/bin/whoami", "runas_uid=0", "runas_gid=0", NULL); + VERIFY_STR_SET(user_env_out, 5, "USER_ENV1=VALUE1", "USER_ENV2=value2", + "REQUESTED_ENV1=VALUE1", "REQUESTED_ENV2=value2", NULL); + VERIFY_STR_SET(argv_out, 3, "/bin/whoami", "--help", NULL); + + VERIFY_INT(python_policy->init_session(&example_pwd, &user_env_out, &errstr), SUDO_RC_ACCEPT); + VERIFY_PTR(errstr, NULL); + + // init session is able to modify the user env: + VERIFY_STR_SET(user_env_out, 6, "USER_ENV1=VALUE1", "USER_ENV2=value2", + "REQUESTED_ENV1=VALUE1", "REQUESTED_ENV2=value2", "PLUGIN_EXAMPLE_ENV=1", NULL); + + python_policy->close(3, 0); // successful execution returned exit code 3 + + VERIFY_STDOUT(expected_path("check_example_policy_plugin_accepted_execution.stdout")); + VERIFY_STDERR(expected_path("check_example_policy_plugin_accepted_execution.stderr")); + + str_array_free(&env_add); + str_array_free(&user_env_out); + str_array_free(&command_info_out); + str_array_free(&argv_out); + return true; +} + +static int +check_example_policy_plugin_failed_execution(void) +{ + const char *errstr = NULL; + + create_policy_plugin_options(); + + str_array_free(&data.plugin_argv); + data.plugin_argc = 2; + data.plugin_argv = create_str_array(3, "/bin/id", "--help", NULL); + + VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings, + data.user_info, data.user_env, data.plugin_options, &errstr), + SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + char **argv_out, **user_env_out, **command_info_out; // free to contain garbage + + VERIFY_INT(python_policy->check_policy(data.plugin_argc, data.plugin_argv, NULL, + &command_info_out, &argv_out, &user_env_out, &errstr), + SUDO_RC_ACCEPT); + VERIFY_PTR(errstr, NULL); + + // pwd is unset (user is not part of /etc/passwd) + VERIFY_INT(python_policy->init_session(NULL, &user_env_out, &errstr), SUDO_RC_ACCEPT); + VERIFY_PTR(errstr, NULL); + + python_policy->close(12345, ENOENT); // failed to execute + + VERIFY_STDOUT(expected_path("check_example_policy_plugin_failed_execution.stdout")); + VERIFY_STDERR(expected_path("check_example_policy_plugin_failed_execution.stderr")); + + str_array_free(&user_env_out); + str_array_free(&command_info_out); + str_array_free(&argv_out); + return true; +} + +static int +check_example_policy_plugin_denied_execution(void) +{ + const char *errstr = NULL; + + create_policy_plugin_options(); + + str_array_free(&data.plugin_argv); + data.plugin_argc = 1; + data.plugin_argv = create_str_array(2, "/bin/passwd", NULL); + + VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings, + data.user_info, data.user_env, data.plugin_options, &errstr), + SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + char **argv_out, **user_env_out, **command_info_out; // free to contain garbage + + VERIFY_INT(python_policy->check_policy(data.plugin_argc, data.plugin_argv, NULL, + &command_info_out, &argv_out, &user_env_out, &errstr), + SUDO_RC_REJECT); + VERIFY_PTR(errstr, NULL); + + VERIFY_PTR(command_info_out, NULL); + VERIFY_PTR(argv_out, NULL); + VERIFY_PTR(user_env_out, NULL); + + python_policy->close(0, 0); // there was no execution + + VERIFY_STDOUT(expected_path("check_example_policy_plugin_denied_execution.stdout")); + VERIFY_STDERR(expected_path("check_example_policy_plugin_denied_execution.stderr")); + + return true; +} + +static int +check_example_policy_plugin_list(void) +{ + const char *errstr = NULL; + + create_policy_plugin_options(); + + VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings, + data.user_info, data.user_env, data.plugin_options, &errstr), + SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + snprintf_append(data.stdout_str, MAX_OUTPUT, "-- minimal --\n"); + VERIFY_INT(python_policy->list(data.plugin_argc, data.plugin_argv, false, NULL, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + snprintf_append(data.stdout_str, MAX_OUTPUT, "\n-- minimal (verbose) --\n"); + VERIFY_INT(python_policy->list(data.plugin_argc, data.plugin_argv, true, NULL, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + snprintf_append(data.stdout_str, MAX_OUTPUT, "\n-- with user --\n"); + VERIFY_INT(python_policy->list(data.plugin_argc, data.plugin_argv, false, "testuser", &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + snprintf_append(data.stdout_str, MAX_OUTPUT, "\n-- with user (verbose) --\n"); + VERIFY_INT(python_policy->list(data.plugin_argc, data.plugin_argv, true, "testuser", &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + snprintf_append(data.stdout_str, MAX_OUTPUT, "\n-- with allowed program --\n"); + str_array_free(&data.plugin_argv); + data.plugin_argc = 3; + data.plugin_argv = create_str_array(4, "/bin/id", "some", "arguments", NULL); + VERIFY_INT(python_policy->list(data.plugin_argc, data.plugin_argv, false, NULL, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + snprintf_append(data.stdout_str, MAX_OUTPUT, "\n-- with allowed program (verbose) --\n"); + VERIFY_INT(python_policy->list(data.plugin_argc, data.plugin_argv, true, NULL, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + snprintf_append(data.stdout_str, MAX_OUTPUT, "\n-- with denied program --\n"); + str_array_free(&data.plugin_argv); + data.plugin_argc = 1; + data.plugin_argv = create_str_array(2, "/bin/passwd", NULL); + VERIFY_INT(python_policy->list(data.plugin_argc, data.plugin_argv, false, NULL, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + snprintf_append(data.stdout_str, MAX_OUTPUT, "\n-- with denied program (verbose) --\n"); + VERIFY_INT(python_policy->list(data.plugin_argc, data.plugin_argv, true, NULL, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + python_policy->close(0, 0); // there was no execution + + VERIFY_STDOUT(expected_path("check_example_policy_plugin_list.stdout")); + VERIFY_STDERR(expected_path("check_example_policy_plugin_list.stderr")); + + return true; +} + +static int +check_example_policy_plugin_validate_invalidate(void) +{ + const char *errstr = NULL; + +#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + // the plugin does not do any meaningful for these, so using log to validate instead + const char *config_path = create_debug_config("py_calls@diag"); + VERIFY_NOT_NULL(config_path); + VERIFY_INT(sudo_conf_read(config_path, SUDO_CONF_ALL), true); +#endif + + create_policy_plugin_options(); + + VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings, + data.user_info, data.user_env, data.plugin_options, &errstr), + SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + VERIFY_INT(python_policy->validate(&errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + python_policy->invalidate(true); + python_policy->invalidate(false); + + python_policy->close(0, 0); // no command execution + +#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + VERIFY_LOG_LINES(expected_path("check_example_policy_plugin_validate_invalidate.log")); +#endif + VERIFY_STR(data.stderr_str, ""); + VERIFY_STR(data.stdout_str, ""); + return true; +} + +static int +check_policy_plugin_callbacks_are_optional(void) +{ + const char *errstr = NULL; + + create_debugging_plugin_options(); + + VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings, + data.user_info, data.user_env, data.plugin_options, &errstr), + SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + VERIFY_PTR(python_policy->list, NULL); + VERIFY_PTR(python_policy->validate, NULL); + VERIFY_PTR(python_policy->invalidate, NULL); + VERIFY_PTR_NE(python_policy->check_policy, NULL); // (not optional) + VERIFY_PTR(python_policy->init_session, NULL); + + // show_version always displays the plugin, but it is optional in the python layer + VERIFY_PTR_NE(python_policy->show_version, NULL); + VERIFY_INT(python_policy->show_version(1), SUDO_RC_OK); + + python_policy->close(0, 0); + return true; +} + +static int +check_policy_plugin_reports_error(void) +{ + const char *errstr = NULL; + str_array_free(&data.plugin_options); + data.plugin_options = create_str_array( + 3, + "ModulePath=" SRC_DIR "/regress/plugin_errorstr.py", + "ClassName=ConstructErrorPlugin", + NULL + ); + + VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings, + data.user_info, data.user_env, data.plugin_options, &errstr), SUDO_RC_ERROR); + VERIFY_STR(errstr, "Something wrong in plugin constructor"); + errstr = NULL; + + python_policy->close(0, 0); + + str_array_free(&data.plugin_options); + data.plugin_options = create_str_array( + 3, + "ModulePath=" SRC_DIR "/regress/plugin_errorstr.py", + "ClassName=ErrorMsgPlugin", + NULL + ); + + data.plugin_argc = 1; + str_array_free(&data.plugin_argv); + data.plugin_argv = create_str_array(2, "id", NULL); + + VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings, + data.user_info, data.user_env, data.plugin_options, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + char **command_info_out = NULL; + char **argv_out = NULL; + char **user_env_out = NULL; + + VERIFY_INT(python_policy->list(data.plugin_argc, data.plugin_argv, true, NULL, &errstr), SUDO_RC_ERROR); + VERIFY_STR(errstr, "Something wrong in list"); + + errstr = NULL; + VERIFY_INT(python_policy->validate(&errstr), SUDO_RC_ERROR); + VERIFY_STR(errstr, "Something wrong in validate"); + + errstr = NULL; + VERIFY_INT(python_policy->check_policy(data.plugin_argc, data.plugin_argv, data.user_env, + &command_info_out, &argv_out, &user_env_out, &errstr), + SUDO_RC_ERROR); + VERIFY_STR(errstr, "Something wrong in check_policy"); + + errstr = NULL; + VERIFY_INT(python_policy->init_session(&example_pwd, &user_env_out, &errstr), SUDO_RC_ERROR); + VERIFY_STR(errstr, "Something wrong in init_session"); + + python_policy->close(0, 0); + + VERIFY_STR(data.stderr_str, ""); + VERIFY_STR(data.stdout_str, ""); + return true; +} + +static int +check_io_plugin_callbacks_are_optional(void) +{ + const char *errstr = NULL; + + create_debugging_plugin_options(); + + 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_PTR(python_io->log_stdin, NULL); + VERIFY_PTR(python_io->log_stdout, NULL); + VERIFY_PTR(python_io->log_stderr, NULL); + VERIFY_PTR(python_io->log_ttyin, NULL); + VERIFY_PTR(python_io->log_ttyout, NULL); + VERIFY_PTR(python_io->change_winsize, NULL); + + // show_version always displays the plugin, but it is optional in the python layer + VERIFY_PTR_NE(python_io->show_version, NULL); + VERIFY_INT(python_io->show_version(1), SUDO_RC_OK); + + python_io->close(0, 0); + return true; +} + +static int +check_python_plugins_do_not_affect_each_other(void) +{ + const char *errstr = NULL; + + // We test here that one plugin is not able to effect the environment of another + // This is important so they do not ruin or depend on each other's state. + create_plugin_options("regress/plugin_conflict", "ConflictPlugin", "Path=path_for_first_plugin"); + + 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); + + create_plugin_options("regress/plugin_conflict", "ConflictPlugin", "Path=path_for_second_plugin"); + VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings, + data.user_info, data.user_env, data.plugin_options, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + python_io->close(0, 0); + python_policy->close(0, 0); + + VERIFY_STDOUT(expected_path("check_python_plugins_do_not_affect_each_other.stdout")); + VERIFY_STR(data.stderr_str, ""); + return true; +} + +static int +check_example_audit_plugin_receives_accept(void) +{ + create_audit_plugin_options(""); + const char *errstr = NULL; + + str_array_free(&data.plugin_argv); + data.plugin_argv = create_str_array(6, "sudo", "-u", "user", "id", "--help", NULL); + + str_array_free(&data.user_env); + data.user_env = create_str_array(3, "KEY1=VALUE1", "KEY2=VALUE2", NULL); + + str_array_free(&data.user_info); + data.user_info = create_str_array(3, "user=testuser1", "uid=123", NULL); + + VERIFY_INT(python_audit->open(SUDO_API_VERSION, fake_conversation, fake_printf, + data.settings, data.user_info, 3, data.plugin_argv, + data.user_env, data.plugin_options, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + str_array_free(&data.command_info); + data.command_info = create_str_array(2, "command=/sbin/id", NULL); + + str_array_free(&data.plugin_argv); + data.plugin_argv = create_str_array(3, "id", "--help", NULL); + + VERIFY_INT(python_audit->accept("accepter plugin name", SUDO_POLICY_PLUGIN, + data.command_info, data.plugin_argv, + data.user_env, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + python_audit->close(SUDO_PLUGIN_WAIT_STATUS, W_EXITCODE(2, 0)); // process exited with 2 + + VERIFY_STDOUT(expected_path("check_example_audit_plugin_receives_accept.stdout")); + VERIFY_STR(data.stderr_str, ""); + + return true; +} + +static int +check_example_audit_plugin_receives_reject(void) +{ + create_audit_plugin_options(NULL); + const char *errstr = NULL; + + str_array_free(&data.plugin_argv); + data.plugin_argv = create_str_array(3, "sudo", "passwd", NULL); + + str_array_free(&data.user_info); + data.user_info = create_str_array(3, "user=root", "uid=0", NULL); + + VERIFY_INT(python_audit->open(SUDO_API_VERSION, fake_conversation, fake_printf, + data.settings, data.user_info, 1, data.plugin_argv, + data.user_env, data.plugin_options, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + VERIFY_INT(python_audit->reject("rejecter plugin name", SUDO_IO_PLUGIN, + "Rejected just because!", data.command_info, + &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + python_audit->close(SUDO_PLUGIN_NO_STATUS, 0); // program was not run + + VERIFY_STDOUT(expected_path("check_example_audit_plugin_receives_reject.stdout")); + VERIFY_STR(data.stderr_str, ""); + + return true; +} + +static int +check_example_audit_plugin_receives_error(void) +{ + create_audit_plugin_options(""); + const char *errstr = NULL; + + str_array_free(&data.plugin_argv); + data.plugin_argv = create_str_array(5, "sudo", "-u", "user", "id", NULL); + + VERIFY_INT(python_audit->open(SUDO_API_VERSION, fake_conversation, fake_printf, + data.settings, data.user_info, 3, data.plugin_argv, + data.user_env, data.plugin_options, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + str_array_free(&data.command_info); + data.command_info = create_str_array(2, "command=/sbin/id", NULL); + + VERIFY_INT(python_audit->error("errorer plugin name", SUDO_AUDIT_PLUGIN, + "Some error has happened", data.command_info, + &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + python_audit->close(SUDO_PLUGIN_SUDO_ERROR, 222); + + VERIFY_STDOUT(expected_path("check_example_audit_plugin_receives_error.stdout")); + VERIFY_STR(data.stderr_str, ""); + + return true; +} + +typedef struct audit_plugin * (audit_clone_func)(void); + +static int +check_example_audit_plugin_workflow_multiple(void) +{ + // verify multiple python audit plugins are available + audit_clone_func *python_audit_clone = (audit_clone_func *)sudo_dso_findsym( + python_plugin_handle, "python_audit_clone"); + VERIFY_PTR_NE(python_audit_clone, NULL); + + struct audit_plugin *python_audit2 = NULL; + + for (int i = 0; i < 7; ++i) { + python_audit2 = (*python_audit_clone)(); + VERIFY_PTR_NE(python_audit2, NULL); + VERIFY_PTR_NE(python_audit2, python_audit); + } + + const char *errstr = NULL; + + str_array_free(&data.plugin_argv); + data.plugin_argv = create_str_array(6, "sudo", "-u", "user", "id", "--help", NULL); + + str_array_free(&data.user_env); + data.user_env = create_str_array(3, "KEY1=VALUE1", "KEY2=VALUE2", NULL); + + str_array_free(&data.user_info); + data.user_info = create_str_array(3, "user=default", "uid=1000", NULL); + + create_audit_plugin_options("Id=1"); + VERIFY_INT(python_audit->open(SUDO_API_VERSION, fake_conversation, fake_printf, + data.settings, data.user_info, 3, data.plugin_argv, + data.user_env, data.plugin_options, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + // For verifying the error message of no more plugin. It should be displayed only once. + VERIFY_PTR((*python_audit_clone)(), NULL); + VERIFY_PTR((*python_audit_clone)(), NULL); + + create_audit_plugin_options("Id=2"); + VERIFY_INT(python_audit2->open(SUDO_API_VERSION, fake_conversation, fake_printf, + data.settings, data.user_info, 3, data.plugin_argv, + data.user_env, data.plugin_options, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + str_array_free(&data.command_info); + data.command_info = create_str_array(2, "command=/sbin/id", NULL); + + str_array_free(&data.plugin_argv); + data.plugin_argv = create_str_array(3, "id", "--help", NULL); + + VERIFY_INT(python_audit->accept("accepter plugin name", SUDO_POLICY_PLUGIN, + data.command_info, data.plugin_argv, + data.user_env, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + VERIFY_INT(python_audit2->accept("accepter plugin name", SUDO_POLICY_PLUGIN, + data.command_info, data.plugin_argv, + data.user_env, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + python_audit->close(SUDO_PLUGIN_WAIT_STATUS, W_EXITCODE(0, 11)); // process got signal 11 + python_audit2->close(SUDO_PLUGIN_WAIT_STATUS, W_EXITCODE(0, 11)); + + VERIFY_STDOUT(expected_path("check_example_audit_plugin_workflow_multiple.stdout")); + VERIFY_STDERR(expected_path("check_example_audit_plugin_workflow_multiple.stderr")); + + return true; +} + +static int +check_example_audit_plugin_version_display(void) +{ + create_audit_plugin_options(NULL); + const char *errstr = NULL; + + str_array_free(&data.user_info); + data.user_info = create_str_array(3, "user=root", "uid=0", NULL); + + str_array_free(&data.plugin_argv); + data.plugin_argv = create_str_array(3, "sudo", "-V", NULL); + + VERIFY_INT(python_audit->open(SUDO_API_VERSION, fake_conversation, fake_printf, + data.settings, data.user_info, 2, data.plugin_argv, + data.user_env, data.plugin_options, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + VERIFY_INT(python_audit->show_version(false), SUDO_RC_OK); + VERIFY_INT(python_audit->show_version(true), SUDO_RC_OK); + + python_audit->close(SUDO_PLUGIN_SUDO_ERROR, 222); + + VERIFY_STDOUT(expected_path("check_example_audit_plugin_version_display.stdout")); + VERIFY_STR(data.stderr_str, ""); + + return true; +} + +static int +check_audit_plugin_callbacks_are_optional(void) +{ + const char *errstr = NULL; + + create_debugging_plugin_options(); + + VERIFY_INT(python_audit->open(SUDO_API_VERSION, fake_conversation, fake_printf, + data.settings, data.user_info, 2, data.plugin_argv, + data.user_env, data.plugin_options, &errstr), + SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + VERIFY_PTR(python_audit->accept, NULL); + VERIFY_PTR(python_audit->reject, NULL); + VERIFY_PTR(python_audit->error, NULL); + + // show_version always displays the plugin, but it is optional in the python layer + VERIFY_PTR_NE(python_audit->show_version, NULL); + VERIFY_INT(python_audit->show_version(1), SUDO_RC_OK); + + python_audit->close(SUDO_PLUGIN_NO_STATUS, 0); + return true; +} + +static int +check_audit_plugin_reports_error(void) +{ + const char *errstr = NULL; + create_plugin_options("regress/plugin_errorstr", "ConstructErrorPlugin", NULL); + + VERIFY_INT(python_audit->open(SUDO_API_VERSION, fake_conversation, fake_printf, + data.settings, data.user_info, 0, data.plugin_argv, + data.user_env, data.plugin_options, &errstr), SUDO_RC_ERROR); + + VERIFY_STR(errstr, "Something wrong in plugin constructor"); + errstr = NULL; + + python_audit->close(SUDO_PLUGIN_NO_STATUS, 0); + + create_plugin_options("regress/plugin_errorstr", "ErrorMsgPlugin", NULL); + + VERIFY_INT(python_audit->open(SUDO_API_VERSION, fake_conversation, fake_printf, + data.settings, data.user_info, 0, data.plugin_argv, + data.user_env, data.plugin_options, &errstr), SUDO_RC_ERROR); + VERIFY_STR(errstr, "Something wrong in open"); + + errstr = NULL; + VERIFY_INT(python_audit->accept("plugin name", SUDO_POLICY_PLUGIN, + data.command_info, data.plugin_argv, + data.user_env, &errstr), SUDO_RC_ERROR); + VERIFY_STR(errstr, "Something wrong in accept"); + + errstr = NULL; + VERIFY_INT(python_audit->reject("plugin name", SUDO_POLICY_PLUGIN, + "audit message", data.command_info, + &errstr), SUDO_RC_ERROR); + VERIFY_STR(errstr, "Something wrong in reject"); + + errstr = NULL; + VERIFY_INT(python_audit->error("plugin name", SUDO_POLICY_PLUGIN, + "audit message", data.command_info, + &errstr), SUDO_RC_ERROR); + VERIFY_STR(errstr, "Something wrong in error"); + + python_audit->close(SUDO_PLUGIN_NO_STATUS, 0); + + VERIFY_STR(data.stderr_str, ""); + VERIFY_STR(data.stdout_str, ""); + return true; +} + +static int +check_example_approval_plugin(const char *date_str, const char *expected_error) +{ + const char *errstr = NULL; + + create_plugin_options("example_approval_plugin", "BusinessHoursApprovalPlugin", NULL); + + VERIFY_INT(python_approval->open(SUDO_API_VERSION, fake_conversation, fake_printf, + data.settings, data.user_info, 0, data.plugin_argv, + data.user_env, data.plugin_options, &errstr), SUDO_RC_OK); + + VERIFY_TRUE(mock_python_datetime_now("example_approval_plugin", date_str)); + + int expected_rc = (expected_error == NULL) ? SUDO_RC_ACCEPT : SUDO_RC_REJECT; + + VERIFY_INT(python_approval->check(data.command_info, data.plugin_argv, data.user_env, &errstr), + expected_rc); + + if (expected_error == NULL) { + VERIFY_PTR(errstr, NULL); + VERIFY_STR(data.stdout_str, ""); + } else { + VERIFY_STR(errstr, expected_error); + VERIFY_STR_CONTAINS(data.stdout_str, expected_error); // (ends with \n) + } + VERIFY_STR(data.stderr_str, ""); + + python_approval->close(); + + return true; +} + +typedef struct approval_plugin * (approval_clone_func)(void); + +static int +check_multiple_approval_plugin_and_arguments(void) +{ + // verify multiple python approval plugins are available + approval_clone_func *python_approval_clone = (approval_clone_func *)sudo_dso_findsym( + python_plugin_handle, "python_approval_clone"); + VERIFY_PTR_NE(python_approval_clone, NULL); + + struct approval_plugin *python_approval2 = NULL; + + for (int i = 0; i < 7; ++i) { + python_approval2 = (*python_approval_clone)(); + VERIFY_PTR_NE(python_approval2, NULL); + VERIFY_PTR_NE(python_approval2, python_approval); + } + + const char *errstr = NULL; + create_plugin_options("regress/plugin_approval_test", "ApprovalTestPlugin", "Id=1"); + + str_array_free(&data.plugin_argv); + data.plugin_argv = create_str_array(6, "sudo", "-u", "user", "whoami", "--help", NULL); + + str_array_free(&data.user_env); + data.user_env = create_str_array(3, "USER_ENV1=VALUE1", "USER_ENV2=value2", NULL); + + str_array_free(&data.user_info); + data.user_info = create_str_array(3, "INFO1=VALUE1", "info2=value2", NULL); + + str_array_free(&data.settings); + data.settings = create_str_array(3, "SETTING1=VALUE1", "setting2=value2", NULL); + + VERIFY_INT(python_approval->open(SUDO_API_VERSION, fake_conversation, fake_printf, + data.settings, data.user_info, 3, data.plugin_argv, + data.user_env, data.plugin_options, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + // For verifying the error message of no more plugin. It should be displayed only once. + VERIFY_PTR((*python_approval_clone)(), NULL); + VERIFY_PTR((*python_approval_clone)(), NULL); + + create_plugin_options("regress/plugin_approval_test", "ApprovalTestPlugin", "Id=2"); + VERIFY_INT(python_approval2->open(SUDO_API_VERSION, fake_conversation, fake_printf, + data.settings, data.user_info, 3, data.plugin_argv, + data.user_env, data.plugin_options, &errstr), SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + VERIFY_INT(python_approval->show_version(false), SUDO_RC_OK); + VERIFY_INT(python_approval2->show_version(true), SUDO_RC_OK); + + str_array_free(&data.command_info); + data.command_info = create_str_array(3, "CMDINFO1=value1", "CMDINFO2=VALUE2", NULL); + + str_array_free(&data.plugin_argv); + data.plugin_argv = create_str_array(3, "whoami", "--help", NULL); + + VERIFY_INT(python_approval->check(data.command_info, data.plugin_argv, data.user_env, &errstr), + SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + VERIFY_INT(python_approval2->check(data.command_info, data.plugin_argv, data.user_env, &errstr), + SUDO_RC_OK); + VERIFY_PTR(errstr, NULL); + + python_approval->close(); + python_approval2->close(); + + VERIFY_STDOUT(expected_path("check_multiple_approval_plugin_and_arguments.stdout")); + VERIFY_STDERR(expected_path("check_multiple_approval_plugin_and_arguments.stderr")); + + return true; +} + + +static int +_init_symbols(void) +{ + if (python_plugin_handle != NULL) { + // symbols are already loaded, we just restore + RESTORE_PYTHON_PLUGIN(python_io); + RESTORE_PYTHON_PLUGIN(python_policy); + RESTORE_PYTHON_PLUGIN(python_approval); + RESTORE_PYTHON_PLUGIN(python_audit); + RESTORE_PYTHON_PLUGIN(group_plugin); + return true; + } + + // we load the symbols + python_plugin_handle = sudo_dso_load(python_plugin_so_path, SUDO_DSO_LAZY|SUDO_DSO_GLOBAL); + VERIFY_PTR_NE(python_plugin_handle, NULL); + + python_io = sudo_dso_findsym(python_plugin_handle, "python_io"); + VERIFY_PTR_NE(python_io, NULL); + + group_plugin = sudo_dso_findsym(python_plugin_handle, "group_plugin"); + VERIFY_PTR_NE(group_plugin, NULL); + + python_policy = sudo_dso_findsym(python_plugin_handle, "python_policy"); + VERIFY_PTR_NE(python_policy, NULL); + + python_audit = sudo_dso_findsym(python_plugin_handle, "python_audit"); + VERIFY_PTR_NE(python_audit, NULL); + + python_approval = sudo_dso_findsym(python_plugin_handle, "python_approval"); + VERIFY_PTR_NE(python_approval, NULL); + + SAVE_PYTHON_PLUGIN(python_io); + SAVE_PYTHON_PLUGIN(python_policy); + SAVE_PYTHON_PLUGIN(python_approval); + SAVE_PYTHON_PLUGIN(python_audit); + SAVE_PYTHON_PLUGIN(group_plugin); + + return true; +} + +static int +_unlink_symbols(void) +{ + python_io = NULL; + group_plugin = NULL; + python_policy = NULL; + python_approval = NULL; + python_audit = NULL; + VERIFY_INT(sudo_dso_unload(python_plugin_handle), 0); + python_plugin_handle = NULL; + VERIFY_FALSE(Py_IsInitialized()); + return true; +} + +int +main(int argc, char *argv[]) +{ + int ch, errors = 0, ntests = 0; + + while ((ch = getopt(argc, argv, "v")) != -1) { + switch (ch) { + case 'v': + verbose = true; + break; + default: + fprintf(stderr, "usage: %s [-v]\n", getprogname()); + return EXIT_FAILURE; + } + } + argc -= optind; + argv += optind; + + if (argc != 1) { + printf("Please specify the python_plugin.so as argument!\n"); + return EXIT_FAILURE; + } + python_plugin_so_path = argv[0]; + + RUN_TEST(check_example_io_plugin_version_display(true)); + RUN_TEST(check_example_io_plugin_version_display(false)); + RUN_TEST(check_example_io_plugin_command_log()); + RUN_TEST(check_example_io_plugin_command_log_multiple()); + RUN_TEST(check_example_io_plugin_failed_to_start_command()); + RUN_TEST(check_example_io_plugin_fails_with_python_backtrace()); + RUN_TEST(check_io_plugin_callbacks_are_optional()); + RUN_TEST(check_io_plugin_reports_error()); + RUN_TEST(check_plugin_unload()); + + RUN_TEST(check_example_group_plugin()); +#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + RUN_TEST(check_example_group_plugin_is_able_to_debug()); +#endif + 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()); + RUN_TEST(check_plugin_unload()); + + RUN_TEST(check_example_conversation_plugin_reason_log(false, "without_suspend")); + RUN_TEST(check_example_conversation_plugin_reason_log(true, "with_suspend")); + RUN_TEST(check_example_conversation_plugin_user_interrupts()); + RUN_TEST(check_plugin_unload()); + + RUN_TEST(check_example_policy_plugin_version_display(true)); + RUN_TEST(check_example_policy_plugin_version_display(false)); + RUN_TEST(check_example_policy_plugin_accepted_execution()); + RUN_TEST(check_example_policy_plugin_failed_execution()); + RUN_TEST(check_example_policy_plugin_denied_execution()); + RUN_TEST(check_example_policy_plugin_list()); + RUN_TEST(check_example_policy_plugin_validate_invalidate()); + RUN_TEST(check_policy_plugin_callbacks_are_optional()); + RUN_TEST(check_policy_plugin_reports_error()); + RUN_TEST(check_plugin_unload()); + + RUN_TEST(check_example_audit_plugin_receives_accept()); + RUN_TEST(check_example_audit_plugin_receives_reject()); + RUN_TEST(check_example_audit_plugin_receives_error()); + RUN_TEST(check_example_audit_plugin_workflow_multiple()); + RUN_TEST(check_example_audit_plugin_version_display()); + RUN_TEST(check_audit_plugin_callbacks_are_optional()); + RUN_TEST(check_audit_plugin_reports_error()); + RUN_TEST(check_plugin_unload()); + + // Monday, too early + RUN_TEST(check_example_approval_plugin( + "2020-02-10T07:55:23", "That is not allowed outside the business hours!")); + // Monday, good time + RUN_TEST(check_example_approval_plugin("2020-02-10T08:05:23", NULL)); + // Friday, good time + RUN_TEST(check_example_approval_plugin("2020-02-14T17:59:23", NULL)); + // Friday, too late + RUN_TEST(check_example_approval_plugin( + "2020-02-10T18:05:23", "That is not allowed outside the business hours!")); + // Saturday + RUN_TEST(check_example_approval_plugin( + "2020-02-15T08:05:23", "That is not allowed on the weekend!")); + RUN_TEST(check_multiple_approval_plugin_and_arguments()); + + RUN_TEST(check_python_plugins_do_not_affect_each_other()); + RUN_TEST(check_plugin_unload()); + +#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + RUN_TEST(check_example_debugging("plugin@err")); + RUN_TEST(check_example_debugging("plugin@info")); + RUN_TEST(check_example_debugging("load@diag")); + RUN_TEST(check_example_debugging("sudo_cb@info")); + RUN_TEST(check_example_debugging("c_calls@diag")); + RUN_TEST(check_example_debugging("c_calls@info")); + RUN_TEST(check_example_debugging("py_calls@diag")); + RUN_TEST(check_example_debugging("py_calls@info")); + RUN_TEST(check_example_debugging("plugin@err")); + RUN_TEST(check_plugin_unload()); +#endif /* FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION */ + + if (ntests != 0) { + printf("%s: %d tests run, %d errors, %d%% success rate\n", + getprogname(), ntests, errors, (ntests - errors) * 100 / ntests); + } + + return errors; +} diff --git a/plugins/python/regress/iohelpers.c b/plugins/python/regress/iohelpers.c new file mode 100644 index 0000000..d481100 --- /dev/null +++ b/plugins/python/regress/iohelpers.c @@ -0,0 +1,180 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 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 "iohelpers.h" + +int +rmdir_recursive(const char *path) +{ + char *cmd = NULL; + int success = false; + + if (asprintf(&cmd, "rm -rf \"%s\"", path) < 0) + return false; + + if (system(cmd) == 0) + success = true; + + free(cmd); + + return success; +} + +int +fwriteall(const char *file_path, const char *string) +{ + int success = false; + + FILE *file = fopen(file_path, "w+"); + if (file == NULL) + goto cleanup; + + size_t size = strlen(string); + if (fwrite(string, 1, size, file) < size) { + goto cleanup; + } + + success = true; + +cleanup: + if (file) + fclose(file); + + return success; +} + +int +freadall(const char *file_path, char *output, size_t max_len) +{ + int rc = false; + FILE *file = fopen(file_path, "rb"); + if (file == NULL) { + printf("Failed to open file '%s'\n", file_path); + goto cleanup; + } + + size_t len = fread(output, 1, max_len - 1, file); + output[len] = '\0'; + + if (ferror(file) != 0) { + printf("Failed to read file '%s' (Error %d)\n", file_path, ferror(file)); + goto cleanup; + } + + if (!feof(file)) { + printf("File '%s' was bigger than allocated buffer %zu", file_path, max_len); + goto cleanup; + } + + rc = true; + +cleanup: + if (file) + fclose(file); + + return rc; +} + +int +vsnprintf_append(char *output, size_t max_output_len, const char *fmt, va_list args) +{ + va_list args2; + va_copy(args2, args); + + size_t output_len = strlen(output); + int rc = vsnprintf(output + output_len, max_output_len - output_len, fmt, args2); + + va_end(args2); + return rc; +} + +int +snprintf_append(char *output, size_t max_output_len, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + int rc = vsnprintf_append(output, max_output_len, fmt, args); + va_end(args); + return rc; +} + +int +str_array_count(char **str_array) +{ + int result = 0; + for (; str_array[result] != NULL; ++result) {} + return result; +} + +void +str_array_snprint(char *out_str, size_t max_len, char **str_array, int array_len) +{ + if (array_len < 0) + array_len = str_array_count(str_array); + + for (int pos = 0; pos < array_len; ++pos) { + snprintf_append(out_str, max_len, "%s%s", pos > 0 ? ", " : "", str_array[pos]); + } +} + +char * +str_replaced(const char *source, size_t dest_len, const char *old, const char *new) +{ + char *result = malloc(dest_len); + char *dest = result; + char *pos = NULL; + size_t old_len = strlen(old); + + if (result == NULL) + return NULL; + + while ((pos = strstr(source, old)) != NULL) { + size_t len = snprintf(dest, dest_len, + "%.*s%s", (int)(pos - source), source, new); + if (len >= dest_len) + goto fail; + + dest_len -= len; + dest += len; + source = pos + old_len; + } + + if (strlcpy(dest, source, dest_len) >= dest_len) + goto fail; + + return result; + +fail: + free(result); + return strdup("str_replace_all failed, string too long"); +} + +void +str_replace_in_place(char *string, size_t max_length, const char *old, const char *new) +{ + char *replaced = str_replaced(string, max_length, old, new); + if (replaced != NULL) { + strlcpy(string, replaced, max_length); + free(replaced); + } +} diff --git a/plugins/python/regress/iohelpers.h b/plugins/python/regress/iohelpers.h new file mode 100644 index 0000000..ed21d56 --- /dev/null +++ b/plugins/python/regress/iohelpers.h @@ -0,0 +1,58 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 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. + */ + +#ifndef PYTHON_IO_HELPERS +#define PYTHON_IO_HELPERS + +#include <config.h> + +#include <stdio.h> +#include <stdlib.h> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#include <string.h> +#include <stdarg.h> +#include <signal.h> +#include <pwd.h> + +#include "sudo_compat.h" + +#define MAX_OUTPUT (2 << 16) + +int rmdir_recursive(const char *path); + +int fwriteall(const char *file_path, const char *string); +int freadall(const char *file_path, char *output, size_t max_len); + +// allocates new string with the content of 'string' but 'old' replaced to 'new' +// The allocated array will be dest_length size and null terminated correctly. +char *str_replaced(const char *string, size_t dest_length, const char *old, const char *new); + +// same, but "string" must be able to store 'max_length' number of characters including the null terminator +void str_replace_in_place(char *string, size_t max_length, const char *old, const char *new); + +int vsnprintf_append(char *output, size_t max_output_len, const char *fmt, va_list args); +int snprintf_append(char *output, size_t max_output_len, const char *fmt, ...); + +int str_array_count(char **str_array); +void str_array_snprint(char *out_str, size_t max_len, char **str_array, int array_len); + +#endif diff --git a/plugins/python/regress/plugin_approval_test.py b/plugins/python/regress/plugin_approval_test.py new file mode 100644 index 0000000..69ea668 --- /dev/null +++ b/plugins/python/regress/plugin_approval_test.py @@ -0,0 +1,22 @@ +import sudo +import json + + +class ApprovalTestPlugin(sudo.Plugin): + def __init__(self, plugin_options, **kwargs): + id = sudo.options_as_dict(plugin_options).get("Id", "") + super().__init__(plugin_options=plugin_options, **kwargs) + self._id = "(APPROVAL {})".format(id) + sudo.log_info("{} Constructed:".format(self._id)) + sudo.log_info(json.dumps(self.__dict__, indent=4, sort_keys=True)) + + def __del__(self): + sudo.log_info("{} Destructed successfully".format(self._id)) + + def check(self, *args): + sudo.log_info("{} Check was called with arguments: " + "{}".format(self._id, args)) + + def show_version(self, *args): + sudo.log_info("{} Show version was called with arguments: " + "{}".format(self._id, args)) diff --git a/plugins/python/regress/plugin_conflict.py b/plugins/python/regress/plugin_conflict.py new file mode 100644 index 0000000..3632193 --- /dev/null +++ b/plugins/python/regress/plugin_conflict.py @@ -0,0 +1,11 @@ +import sudo + +import sys + +sys.path = [] + +class ConflictPlugin(sudo.Plugin): + def __init__(self, plugin_options, **kwargs): + sudo.log_info("PATH before: {} (should be empty)".format(sys.path)) + sys.path = [sudo.options_as_dict(plugin_options).get("Path")] + sudo.log_info("PATH set: {}".format(sys.path)) diff --git a/plugins/python/regress/plugin_errorstr.py b/plugins/python/regress/plugin_errorstr.py new file mode 100644 index 0000000..fcbd71d --- /dev/null +++ b/plugins/python/regress/plugin_errorstr.py @@ -0,0 +1,18 @@ +import sudo + + +# The purpose of this class is that all methods you call on its object +# raises a PluginError with a message containing the name of the called method. +# Eg. if you call "ErrorMsgPlugin().some_method()" it will raise +# "Something wrong in some_method" +class ErrorMsgPlugin(sudo.Plugin): + def __getattr__(self, name): + def raiser_func(*args): + raise sudo.PluginError("Something wrong in " + name) + + return raiser_func + + +class ConstructErrorPlugin(sudo.Plugin): + def __init__(self, **kwargs): + raise sudo.PluginError("Something wrong in plugin constructor") diff --git a/plugins/python/regress/testdata/check_example_audit_plugin_receives_accept.stdout b/plugins/python/regress/testdata/check_example_audit_plugin_receives_accept.stdout new file mode 100644 index 0000000..2c83972 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_audit_plugin_receives_accept.stdout @@ -0,0 +1,7 @@ +(AUDIT) -- Started by user testuser1 (123) -- +(AUDIT) Requested command: id --help +(AUDIT) Accepted command: /sbin/id --help +(AUDIT) By the plugin: accepter plugin name (type=POLICY) +(AUDIT) Environment: KEY1=VALUE1 KEY2=VALUE2 +(AUDIT) Command returned with exit code 2 +(AUDIT) -- Finished -- diff --git a/plugins/python/regress/testdata/check_example_audit_plugin_receives_error.stdout b/plugins/python/regress/testdata/check_example_audit_plugin_receives_error.stdout new file mode 100644 index 0000000..cb7068d --- /dev/null +++ b/plugins/python/regress/testdata/check_example_audit_plugin_receives_error.stdout @@ -0,0 +1,5 @@ +(AUDIT) -- Started by user ??? (???) -- +(AUDIT) Requested command: id +(AUDIT) Plugin errorer plugin name (type=AUDIT) got an error: Some error has happened +(AUDIT) Sudo has run into an error: 222 +(AUDIT) -- Finished -- diff --git a/plugins/python/regress/testdata/check_example_audit_plugin_receives_reject.stdout b/plugins/python/regress/testdata/check_example_audit_plugin_receives_reject.stdout new file mode 100644 index 0000000..c6e8a5a --- /dev/null +++ b/plugins/python/regress/testdata/check_example_audit_plugin_receives_reject.stdout @@ -0,0 +1,5 @@ +(AUDIT) -- Started by user root (0) -- +(AUDIT) Requested command: passwd +(AUDIT) Rejected by plugin rejecter plugin name (type=IO): Rejected just because! +(AUDIT) The command was not executed +(AUDIT) -- Finished -- diff --git a/plugins/python/regress/testdata/check_example_audit_plugin_version_display.stdout b/plugins/python/regress/testdata/check_example_audit_plugin_version_display.stdout new file mode 100644 index 0000000..1586f46 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_audit_plugin_version_display.stdout @@ -0,0 +1,6 @@ +(AUDIT) -- Started by user root (0) -- +Python Example Audit Plugin +Python audit plugin (API 1.0): SudoAuditPlugin (loaded from 'SRC_DIR/example_audit_plugin.py') +Python Example Audit Plugin (version=1.0) +(AUDIT) Sudo has run into an error: 222 +(AUDIT) -- Finished -- diff --git a/plugins/python/regress/testdata/check_example_audit_plugin_workflow_multiple.stderr b/plugins/python/regress/testdata/check_example_audit_plugin_workflow_multiple.stderr new file mode 100644 index 0000000..1d7d4a1 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_audit_plugin_workflow_multiple.stderr @@ -0,0 +1 @@ +sudo: loading more than 8 sudo python audit plugins is not supported diff --git a/plugins/python/regress/testdata/check_example_audit_plugin_workflow_multiple.stdout b/plugins/python/regress/testdata/check_example_audit_plugin_workflow_multiple.stdout new file mode 100644 index 0000000..aa5ee58 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_audit_plugin_workflow_multiple.stdout @@ -0,0 +1,14 @@ +(AUDIT1) -- Started by user default (1000) -- +(AUDIT1) Requested command: id --help +(AUDIT2) -- Started by user default (1000) -- +(AUDIT2) Requested command: id --help +(AUDIT1) Accepted command: /sbin/id --help +(AUDIT1) By the plugin: accepter plugin name (type=POLICY) +(AUDIT1) Environment: KEY1=VALUE1 KEY2=VALUE2 +(AUDIT2) Accepted command: /sbin/id --help +(AUDIT2) By the plugin: accepter plugin name (type=POLICY) +(AUDIT2) Environment: KEY1=VALUE1 KEY2=VALUE2 +(AUDIT1) Command exited due to signal 11 +(AUDIT1) -- Finished -- +(AUDIT2) Command exited due to signal 11 +(AUDIT2) -- Finished -- diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.conversation b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.conversation new file mode 100644 index 0000000..43bd2e7 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.conversation @@ -0,0 +1,3 @@ +Question count: 2 +Question 0: <<Reason: >> (timeout: 120, msg_type=2) +Question 1: <<Secret reason: >> (timeout: 120, msg_type=5) diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stderr b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stderr new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stderr diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stdout b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stdout new file mode 100644 index 0000000..9d515c9 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stdout @@ -0,0 +1,3 @@ +Please provide your reason for executing ('/bin/whoami',) +conversation suspend: signal SIGTSTP +conversation resume: signal was SIGCONT diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stored b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stored new file mode 100644 index 0000000..c0ab857 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stored @@ -0,0 +1,3 @@ +Executed /bin/whoami +Reason: my fake reason +Hidden reason: my real secret reason diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.conversation b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.conversation new file mode 100644 index 0000000..43bd2e7 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.conversation @@ -0,0 +1,3 @@ +Question count: 2 +Question 0: <<Reason: >> (timeout: 120, msg_type=2) +Question 1: <<Secret reason: >> (timeout: 120, msg_type=5) diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stderr b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stderr new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stderr diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stdout b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stdout new file mode 100644 index 0000000..7bbfa3f --- /dev/null +++ b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stdout @@ -0,0 +1 @@ +Please provide your reason for executing ('/bin/whoami',) diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stored b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stored new file mode 100644 index 0000000..c0ab857 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stored @@ -0,0 +1,3 @@ +Executed /bin/whoami +Reason: my fake reason +Hidden reason: my real secret reason diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.conv b/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.conv new file mode 100644 index 0000000..59d7202 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.conv @@ -0,0 +1,2 @@ +Question count: 2 +Question 0: <<Reason: >> (timeout: 120, msg_type=2) diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.conversation b/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.conversation new file mode 100644 index 0000000..59d7202 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.conversation @@ -0,0 +1,2 @@ +Question count: 2 +Question 0: <<Reason: >> (timeout: 120, msg_type=2) diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.stderr b/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.stderr new file mode 100644 index 0000000..8a4a528 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.stderr @@ -0,0 +1 @@ +You did not answer in time diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.stdout b/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.stdout new file mode 100644 index 0000000..7bbfa3f --- /dev/null +++ b/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.stdout @@ -0,0 +1 @@ +Please provide your reason for executing ('/bin/whoami',) diff --git a/plugins/python/regress/testdata/check_example_debugging_c_calls@diag.log b/plugins/python/regress/testdata/check_example_debugging_c_calls@diag.log new file mode 100644 index 0000000..c5cd98c --- /dev/null +++ b/plugins/python/regress/testdata/check_example_debugging_c_calls@diag.log @@ -0,0 +1,6 @@ +sudo.debug was called with arguments: (DEBUG.ERROR, 'My demo purpose plugin shows this ERROR level debug message') +sudo.debug was called with arguments: (DEBUG.INFO, 'My demo purpose plugin shows this INFO level debug message') +LogHandler.emit was called +LogHandler.emit was called +sudo.options_as_dict was called with arguments: (('ModulePath=SRC_DIR/example_debugging.py', 'ClassName=DebugDemoPlugin'),) +sudo.options_as_dict returned result: [('ClassName', 'DebugDemoPlugin'), ('ModulePath', 'SRC_DIR/example_debugging.py')] diff --git a/plugins/python/regress/testdata/check_example_debugging_c_calls@info.log b/plugins/python/regress/testdata/check_example_debugging_c_calls@info.log new file mode 100644 index 0000000..7258618 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_debugging_c_calls@info.log @@ -0,0 +1,11 @@ +__init__ @ SRC_DIR/example_debugging.py:58 calls C function: +sudo.debug was called with arguments: (DEBUG.ERROR, 'My demo purpose plugin shows this ERROR level debug message') +__init__ @ SRC_DIR/example_debugging.py:63 calls C function: +sudo.debug was called with arguments: (DEBUG.INFO, 'My demo purpose plugin shows this INFO level debug message') +handle @ logging/__init__.py calls C function: +LogHandler.emit was called +handle @ logging/__init__.py calls C function: +LogHandler.emit was called +__init__ @ SRC_DIR/example_debugging.py:85 calls C function: +sudo.options_as_dict was called with arguments: (('ModulePath=SRC_DIR/example_debugging.py', 'ClassName=DebugDemoPlugin'),) +sudo.options_as_dict returned result: [('ClassName', 'DebugDemoPlugin'), ('ModulePath', 'SRC_DIR/example_debugging.py')] diff --git a/plugins/python/regress/testdata/check_example_debugging_load@diag.log b/plugins/python/regress/testdata/check_example_debugging_load@diag.log new file mode 100644 index 0000000..15b4bbe --- /dev/null +++ b/plugins/python/regress/testdata/check_example_debugging_load@diag.log @@ -0,0 +1,3 @@ +importing module: SRC_DIR/example_debugging.py +Extending python 'path' with 'SRC_DIR' +Deinit was called for a python plugin diff --git a/plugins/python/regress/testdata/check_example_debugging_plugin@err.log b/plugins/python/regress/testdata/check_example_debugging_plugin@err.log new file mode 100644 index 0000000..aec31ec --- /dev/null +++ b/plugins/python/regress/testdata/check_example_debugging_plugin@err.log @@ -0,0 +1,2 @@ +My demo purpose plugin shows this ERROR level debug message +Python log system shows this ERROR level debug message diff --git a/plugins/python/regress/testdata/check_example_debugging_plugin@info.log b/plugins/python/regress/testdata/check_example_debugging_plugin@info.log new file mode 100644 index 0000000..ed72f35 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_debugging_plugin@info.log @@ -0,0 +1,8 @@ +__init__ @ SRC_DIR/example_debugging.py:58 debugs: +My demo purpose plugin shows this ERROR level debug message +__init__ @ SRC_DIR/example_debugging.py:63 debugs: +My demo purpose plugin shows this INFO level debug message +handle @ logging/__init__.py debugs: +Python log system shows this ERROR level debug message +handle @ logging/__init__.py debugs: +Python log system shows this INFO level debug message diff --git a/plugins/python/regress/testdata/check_example_debugging_py_calls@diag.log b/plugins/python/regress/testdata/check_example_debugging_py_calls@diag.log new file mode 100644 index 0000000..97a89ef --- /dev/null +++ b/plugins/python/regress/testdata/check_example_debugging_py_calls@diag.log @@ -0,0 +1,2 @@ +DebugDemoPlugin.__init__ was called with arguments: () [('plugin_options', ('ModulePath=SRC_DIR/example_debugging.py', 'ClassName=DebugDemoPlugin')), ('settings', ('debug_flags=/tmp/sudo_check_python_exampleXXXXXX/debug.log py_calls@diag', 'plugin_path=python_plugin.so')), ('user_env', ()), ('user_info', ()), ('version', '1.0')] +DebugDemoPlugin.__init__ returned result: <example_debugging.DebugDemoPlugin object> diff --git a/plugins/python/regress/testdata/check_example_debugging_py_calls@info.log b/plugins/python/regress/testdata/check_example_debugging_py_calls@info.log new file mode 100644 index 0000000..ae39daf --- /dev/null +++ b/plugins/python/regress/testdata/check_example_debugging_py_calls@info.log @@ -0,0 +1,9 @@ +DebugDemoPlugin.__init__ was called with arguments: () [('plugin_options', ('ModulePath=SRC_DIR/example_debugging.py', 'ClassName=DebugDemoPlugin')), ('settings', ('debug_flags=/tmp/sudo_check_python_exampleXXXXXX/debug.log py_calls@info', 'plugin_path=python_plugin.so')), ('user_env', ()), ('user_info', ()), ('version', '1.0')] +DebugDemoPlugin.__init__ returned result: <example_debugging.DebugDemoPlugin object> +DebugDemoPlugin function 'log_ttyin' is not implemented +DebugDemoPlugin function 'log_ttyout' is not implemented +DebugDemoPlugin function 'log_stdin' is not implemented +DebugDemoPlugin function 'log_stdout' is not implemented +DebugDemoPlugin function 'log_stderr' is not implemented +DebugDemoPlugin function 'change_winsize' is not implemented +DebugDemoPlugin function 'log_suspend' is not implemented diff --git a/plugins/python/regress/testdata/check_example_debugging_sudo_cb@info.log b/plugins/python/regress/testdata/check_example_debugging_sudo_cb@info.log new file mode 100644 index 0000000..908066b --- /dev/null +++ b/plugins/python/regress/testdata/check_example_debugging_sudo_cb@info.log @@ -0,0 +1 @@ +Skipping close call, because there was no command run diff --git a/plugins/python/regress/testdata/check_example_group_plugin_is_able_to_debug.log b/plugins/python/regress/testdata/check_example_group_plugin_is_able_to_debug.log new file mode 100644 index 0000000..6def462 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_group_plugin_is_able_to_debug.log @@ -0,0 +1,4 @@ +SudoGroupPlugin.__init__ was called with arguments: () [('args', ('ModulePath=SRC_DIR/example_group_plugin.py', 'ClassName=SudoGroupPlugin')), ('version', '1.0')] +SudoGroupPlugin.__init__ returned result: <example_group_plugin.SudoGroupPlugin object> +SudoGroupPlugin.query was called with arguments: ('user', 'group', ('pw_name', 'pw_passwd', 1001, 101, 'pw_gecos', 'pw_dir', 'pw_shell')) +SudoGroupPlugin.query returned result: 0 diff --git a/plugins/python/regress/testdata/check_example_io_plugin_command_log.stderr b/plugins/python/regress/testdata/check_example_io_plugin_command_log.stderr new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_io_plugin_command_log.stderr diff --git a/plugins/python/regress/testdata/check_example_io_plugin_command_log.stdout b/plugins/python/regress/testdata/check_example_io_plugin_command_log.stdout new file mode 100644 index 0000000..7e94c91 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_io_plugin_command_log.stdout @@ -0,0 +1 @@ +Example sudo python plugin will log to /tmp/sudo_check_python_exampleXXXXXX/sudo.log diff --git a/plugins/python/regress/testdata/check_example_io_plugin_command_log.stored b/plugins/python/regress/testdata/check_example_io_plugin_command_log.stored new file mode 100644 index 0000000..73fdc5d --- /dev/null +++ b/plugins/python/regress/testdata/check_example_io_plugin_command_log.stored @@ -0,0 +1,16 @@ + -- Plugin STARTED -- +EXEC id --help +EXEC info [ + "command=/bin/id", + "runas_uid=0" +] +STD IN some standard input +STD OUT some standard output +STD ERR some standard error +SUSPEND SIGTSTP +SUSPEND SIGCONT +WINSIZE 200x100 +TTY IN some tty input +TTY OUT some tty output +CLOSE Command returned 1 + -- Plugin DESTROYED -- diff --git a/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple.stderr b/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple.stderr new file mode 100644 index 0000000..f519805 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple.stderr @@ -0,0 +1 @@ +sudo: loading more than 8 sudo python IO plugins is not supported diff --git a/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple.stdout b/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple.stdout new file mode 100644 index 0000000..e9dbd67 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple.stdout @@ -0,0 +1,2 @@ +Example sudo python plugin will log to /tmp/sudo_check_python_exampleXXXXXX/sudo.log +Example sudo python plugin will log to /tmp/sudo_check_python_exampleXXXXXX2/sudo.log diff --git a/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple1.stored b/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple1.stored new file mode 100644 index 0000000..bc60c38 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple1.stored @@ -0,0 +1,16 @@ + -- Plugin STARTED -- +EXEC id --help +EXEC info [ + "command=/bin/id", + "runas_uid=0" +] +STD IN stdin for plugin 1 +STD OUT stdout for plugin 1 +STD ERR stderr for plugin 1 +SUSPEND SIGTSTP +SUSPEND SIGCONT +WINSIZE 20x10 +TTY IN tty input for plugin 1 +TTY OUT tty output for plugin 1 +CLOSE Command returned 1 + -- Plugin DESTROYED -- diff --git a/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple2.stored b/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple2.stored new file mode 100644 index 0000000..ed3fdc8 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple2.stored @@ -0,0 +1,16 @@ + -- Plugin STARTED -- +EXEC whoami +EXEC info [ + "command=/bin/whoami", + "runas_uid=1" +] +STD IN stdin for plugin 2 +STD OUT stdout for plugin 2 +STD ERR stderr for plugin 2 +SUSPEND SIGSTOP +SUSPEND SIGCONT +WINSIZE 30x40 +TTY IN tty input for plugin 2 +TTY OUT tty output for plugin 2 +CLOSE Command returned 2 + -- Plugin DESTROYED -- diff --git a/plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stderr b/plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stderr new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stderr diff --git a/plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stdout b/plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stdout new file mode 100644 index 0000000..7e94c91 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stdout @@ -0,0 +1 @@ +Example sudo python plugin will log to /tmp/sudo_check_python_exampleXXXXXX/sudo.log diff --git a/plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stored b/plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stored new file mode 100644 index 0000000..1b99398 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stored @@ -0,0 +1,8 @@ + -- Plugin STARTED -- +EXEC cmd +EXEC info [ + "command=/usr/share/cmd", + "runas_uid=0" +] +CLOSE Failed to execute, execve returned 1 (EPERM) + -- Plugin DESTROYED -- diff --git a/plugins/python/regress/testdata/check_example_io_plugin_fails_with_python_backtrace.stderr b/plugins/python/regress/testdata/check_example_io_plugin_fails_with_python_backtrace.stderr new file mode 100644 index 0000000..1dd42a6 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_io_plugin_fails_with_python_backtrace.stderr @@ -0,0 +1 @@ +Failed to construct plugin instance: [Errno 2] No such file or directory: '/some/not/writable/directory/sudo.log' diff --git a/plugins/python/regress/testdata/check_example_io_plugin_fails_with_python_backtrace.stdout b/plugins/python/regress/testdata/check_example_io_plugin_fails_with_python_backtrace.stdout new file mode 100644 index 0000000..10b0e23 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_io_plugin_fails_with_python_backtrace.stdout @@ -0,0 +1,7 @@ +Example sudo python plugin will log to /some/not/writable/directory/sudo.log +Traceback: + File "SRC_DIR/example_io_plugin.py", line 64, in __init__ + self._open_log_file(path.join(log_path, "sudo.log")) + File "SRC_DIR/example_io_plugin.py", line 134, in _open_log_file + self._log_file = open(log_path, "a") + diff --git a/plugins/python/regress/testdata/check_example_io_plugin_version_display.stderr b/plugins/python/regress/testdata/check_example_io_plugin_version_display.stderr new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_io_plugin_version_display.stderr diff --git a/plugins/python/regress/testdata/check_example_io_plugin_version_display.stdout b/plugins/python/regress/testdata/check_example_io_plugin_version_display.stdout new file mode 100644 index 0000000..07e998a --- /dev/null +++ b/plugins/python/regress/testdata/check_example_io_plugin_version_display.stdout @@ -0,0 +1,2 @@ +Example sudo python plugin will log to /tmp/sudo_check_python_exampleXXXXXX/sudo.log +Python Example IO Plugin version: 1.0 diff --git a/plugins/python/regress/testdata/check_example_io_plugin_version_display.stored b/plugins/python/regress/testdata/check_example_io_plugin_version_display.stored new file mode 100644 index 0000000..45f9b9e --- /dev/null +++ b/plugins/python/regress/testdata/check_example_io_plugin_version_display.stored @@ -0,0 +1,2 @@ + -- Plugin STARTED -- + -- Plugin DESTROYED -- diff --git a/plugins/python/regress/testdata/check_example_io_plugin_version_display_full.stdout b/plugins/python/regress/testdata/check_example_io_plugin_version_display_full.stdout new file mode 100644 index 0000000..cfb3921 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_io_plugin_version_display_full.stdout @@ -0,0 +1,3 @@ +Example sudo python plugin will log to /tmp/sudo_check_python_exampleXXXXXX/sudo.log +Python io plugin (API 1.0): SudoIOPlugin (loaded from 'SRC_DIR/example_io_plugin.py') +Python Example IO Plugin version: 1.0 diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_accepted_execution.stderr b/plugins/python/regress/testdata/check_example_policy_plugin_accepted_execution.stderr new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_policy_plugin_accepted_execution.stderr diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_accepted_execution.stdout b/plugins/python/regress/testdata/check_example_policy_plugin_accepted_execution.stdout new file mode 100644 index 0000000..c63a26e --- /dev/null +++ b/plugins/python/regress/testdata/check_example_policy_plugin_accepted_execution.stdout @@ -0,0 +1 @@ +The command returned with exit_status 3 diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_denied_execution.stderr b/plugins/python/regress/testdata/check_example_policy_plugin_denied_execution.stderr new file mode 100644 index 0000000..6db9b2c --- /dev/null +++ b/plugins/python/regress/testdata/check_example_policy_plugin_denied_execution.stderr @@ -0,0 +1 @@ +You are not allowed to run this command! diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_denied_execution.stdout b/plugins/python/regress/testdata/check_example_policy_plugin_denied_execution.stdout new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_policy_plugin_denied_execution.stdout diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_failed_execution.stderr b/plugins/python/regress/testdata/check_example_policy_plugin_failed_execution.stderr new file mode 100644 index 0000000..e8d7034 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_policy_plugin_failed_execution.stderr @@ -0,0 +1 @@ +Failed to execute command, execve syscall returned 2 (ENOENT) diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_failed_execution.stdout b/plugins/python/regress/testdata/check_example_policy_plugin_failed_execution.stdout new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_policy_plugin_failed_execution.stdout diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_list.stderr b/plugins/python/regress/testdata/check_example_policy_plugin_list.stderr new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_policy_plugin_list.stderr diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_list.stdout b/plugins/python/regress/testdata/check_example_policy_plugin_list.stdout new file mode 100644 index 0000000..48c5baf --- /dev/null +++ b/plugins/python/regress/testdata/check_example_policy_plugin_list.stdout @@ -0,0 +1,25 @@ +-- minimal -- +Only the following commands are allowed: id, whoami + +-- minimal (verbose) -- +Only the following commands are allowed: id, whoami + +-- with user -- +Only the following commands are allowed: id, whoami as user 'testuser' + +-- with user (verbose) -- +Only the following commands are allowed: id, whoami as user 'testuser' + +-- with allowed program -- +You are allowed to execute command '/bin/id' + +-- with allowed program (verbose) -- +You are allowed to execute command '/bin/id' +Only the following commands are allowed: id, whoami + +-- with denied program -- +You are NOT allowed to execute command '/bin/passwd' + +-- with denied program (verbose) -- +You are NOT allowed to execute command '/bin/passwd' +Only the following commands are allowed: id, whoami diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_validate_invalidate.log b/plugins/python/regress/testdata/check_example_policy_plugin_validate_invalidate.log new file mode 100644 index 0000000..6f1479a --- /dev/null +++ b/plugins/python/regress/testdata/check_example_policy_plugin_validate_invalidate.log @@ -0,0 +1,8 @@ +SudoPolicyPlugin.__init__ was called with arguments: () [('plugin_options', ('ModulePath=SRC_DIR/example_policy_plugin.py', 'ClassName=SudoPolicyPlugin')), ('settings', ()), ('user_env', ()), ('user_info', ()), ('version', '1.0')] +SudoPolicyPlugin.__init__ returned result: <example_policy_plugin.SudoPolicyPlugin object> +SudoPolicyPlugin.validate was called with arguments: () +SudoPolicyPlugin.validate returned result: None +SudoPolicyPlugin.invalidate was called with arguments: (1,) +SudoPolicyPlugin.invalidate returned result: None +SudoPolicyPlugin.invalidate was called with arguments: (0,) +SudoPolicyPlugin.invalidate returned result: None diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_version_display.stderr b/plugins/python/regress/testdata/check_example_policy_plugin_version_display.stderr new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_policy_plugin_version_display.stderr diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_version_display.stdout b/plugins/python/regress/testdata/check_example_policy_plugin_version_display.stdout new file mode 100644 index 0000000..1cc1edd --- /dev/null +++ b/plugins/python/regress/testdata/check_example_policy_plugin_version_display.stdout @@ -0,0 +1 @@ +Python Example Policy Plugin version: 1.0 diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_version_display_full.stdout b/plugins/python/regress/testdata/check_example_policy_plugin_version_display_full.stdout new file mode 100644 index 0000000..a23cf12 --- /dev/null +++ b/plugins/python/regress/testdata/check_example_policy_plugin_version_display_full.stdout @@ -0,0 +1,2 @@ +Python policy plugin (API 1.0): SudoPolicyPlugin (loaded from 'SRC_DIR/example_policy_plugin.py') +Python Example Policy Plugin version: 1.0 diff --git a/plugins/python/regress/testdata/check_loading_fails_missing_classname.stderr b/plugins/python/regress/testdata/check_loading_fails_missing_classname.stderr new file mode 100644 index 0000000..c207e2f --- /dev/null +++ b/plugins/python/regress/testdata/check_loading_fails_missing_classname.stderr @@ -0,0 +1,3 @@ +No plugin class is specified for python module 'SRC_DIR/regress/plugin_errorstr.py'. Use 'ClassName' configuration option in 'sudo.conf' +Possible plugins: ConstructErrorPlugin, ErrorMsgPlugin +Failed during loading plugin class diff --git a/plugins/python/regress/testdata/check_loading_fails_missing_classname.stdout b/plugins/python/regress/testdata/check_loading_fails_missing_classname.stdout new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/python/regress/testdata/check_loading_fails_missing_classname.stdout diff --git a/plugins/python/regress/testdata/check_loading_fails_missing_path.stderr b/plugins/python/regress/testdata/check_loading_fails_missing_path.stderr new file mode 100644 index 0000000..05bc634 --- /dev/null +++ b/plugins/python/regress/testdata/check_loading_fails_missing_path.stderr @@ -0,0 +1,2 @@ +No python module path is specified. Use 'ModulePath' plugin config option in 'sudo.conf' +Failed during loading plugin class diff --git a/plugins/python/regress/testdata/check_loading_fails_missing_path.stdout b/plugins/python/regress/testdata/check_loading_fails_missing_path.stdout new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/python/regress/testdata/check_loading_fails_missing_path.stdout diff --git a/plugins/python/regress/testdata/check_loading_fails_not_owned_by_root.stderr b/plugins/python/regress/testdata/check_loading_fails_not_owned_by_root.stderr new file mode 100644 index 0000000..7ba1bc9 --- /dev/null +++ b/plugins/python/regress/testdata/check_loading_fails_not_owned_by_root.stderr @@ -0,0 +1 @@ +Failed during loading plugin class: File 'SRC_DIR/example_debugging.py' must be owned by uid 0 diff --git a/plugins/python/regress/testdata/check_loading_fails_not_owned_by_root.stdout b/plugins/python/regress/testdata/check_loading_fails_not_owned_by_root.stdout new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/python/regress/testdata/check_loading_fails_not_owned_by_root.stdout diff --git a/plugins/python/regress/testdata/check_loading_fails_wrong_classname.stderr b/plugins/python/regress/testdata/check_loading_fails_wrong_classname.stderr new file mode 100644 index 0000000..a4c519a --- /dev/null +++ b/plugins/python/regress/testdata/check_loading_fails_wrong_classname.stderr @@ -0,0 +1,2 @@ +Failed to find plugin class 'MispelledPluginName' +Failed during loading plugin class diff --git a/plugins/python/regress/testdata/check_loading_fails_wrong_classname.stdout b/plugins/python/regress/testdata/check_loading_fails_wrong_classname.stdout new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/python/regress/testdata/check_loading_fails_wrong_classname.stdout diff --git a/plugins/python/regress/testdata/check_loading_fails_wrong_path.stderr b/plugins/python/regress/testdata/check_loading_fails_wrong_path.stderr new file mode 100644 index 0000000..3087ba8 --- /dev/null +++ b/plugins/python/regress/testdata/check_loading_fails_wrong_path.stderr @@ -0,0 +1 @@ +Failed during loading plugin class: No module named 'wrong_path' diff --git a/plugins/python/regress/testdata/check_loading_fails_wrong_path.stdout b/plugins/python/regress/testdata/check_loading_fails_wrong_path.stdout new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/python/regress/testdata/check_loading_fails_wrong_path.stdout 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 0000000..f7a1a6f --- /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') diff --git a/plugins/python/regress/testdata/check_multiple_approval_plugin_and_arguments.stderr b/plugins/python/regress/testdata/check_multiple_approval_plugin_and_arguments.stderr new file mode 100644 index 0000000..6dfb141 --- /dev/null +++ b/plugins/python/regress/testdata/check_multiple_approval_plugin_and_arguments.stderr @@ -0,0 +1 @@ +sudo: loading more than 8 sudo python approval plugins is not supported diff --git a/plugins/python/regress/testdata/check_multiple_approval_plugin_and_arguments.stdout b/plugins/python/regress/testdata/check_multiple_approval_plugin_and_arguments.stdout new file mode 100644 index 0000000..2589025 --- /dev/null +++ b/plugins/python/regress/testdata/check_multiple_approval_plugin_and_arguments.stdout @@ -0,0 +1,67 @@ +(APPROVAL 1) Constructed: +{ + "_id": "(APPROVAL 1)", + "plugin_options": [ + "ModulePath=SRC_DIR/regress/plugin_approval_test.py", + "ClassName=ApprovalTestPlugin", + "Id=1" + ], + "settings": [ + "SETTING1=VALUE1", + "setting2=value2" + ], + "submit_argv": [ + "sudo", + "-u", + "user", + "whoami", + "--help" + ], + "submit_optind": 3, + "user_env": [ + "USER_ENV1=VALUE1", + "USER_ENV2=value2" + ], + "user_info": [ + "INFO1=VALUE1", + "info2=value2" + ], + "version": "1.21" +} +(APPROVAL 2) Constructed: +{ + "_id": "(APPROVAL 2)", + "plugin_options": [ + "ModulePath=SRC_DIR/regress/plugin_approval_test.py", + "ClassName=ApprovalTestPlugin", + "Id=2" + ], + "settings": [ + "SETTING1=VALUE1", + "setting2=value2" + ], + "submit_argv": [ + "sudo", + "-u", + "user", + "whoami", + "--help" + ], + "submit_optind": 3, + "user_env": [ + "USER_ENV1=VALUE1", + "USER_ENV2=value2" + ], + "user_info": [ + "INFO1=VALUE1", + "info2=value2" + ], + "version": "1.21" +} +(APPROVAL 1) Show version was called with arguments: (0,) +Python approval plugin (API 1.0): ApprovalTestPlugin (loaded from 'SRC_DIR/regress/plugin_approval_test.py') +(APPROVAL 2) Show version was called with arguments: (1,) +(APPROVAL 1) Check was called with arguments: (('CMDINFO1=value1', 'CMDINFO2=VALUE2'), ('whoami', '--help'), ('USER_ENV1=VALUE1', 'USER_ENV2=value2')) +(APPROVAL 2) Check was called with arguments: (('CMDINFO1=value1', 'CMDINFO2=VALUE2'), ('whoami', '--help'), ('USER_ENV1=VALUE1', 'USER_ENV2=value2')) +(APPROVAL 1) Destructed successfully +(APPROVAL 2) Destructed successfully diff --git a/plugins/python/regress/testdata/check_python_plugins_do_not_affect_each_other.stdout b/plugins/python/regress/testdata/check_python_plugins_do_not_affect_each_other.stdout new file mode 100644 index 0000000..cd5bef9 --- /dev/null +++ b/plugins/python/regress/testdata/check_python_plugins_do_not_affect_each_other.stdout @@ -0,0 +1,4 @@ +PATH before: [] (should be empty) +PATH set: ['path_for_first_plugin'] +PATH before: [] (should be empty) +PATH set: ['path_for_second_plugin'] diff --git a/plugins/python/regress/testhelpers.c b/plugins/python/regress/testhelpers.c new file mode 100644 index 0000000..42971bb --- /dev/null +++ b/plugins/python/regress/testhelpers.c @@ -0,0 +1,346 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 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 "testhelpers.h" + +struct TestData data; + +/* + * Starting with Python 3.11, backtraces may contain a line with + * '^' characters to bring attention to the important part of the + * line. + */ +static void +remove_underline(char *output) +{ + char *cp, *ep; + + // Remove lines that only consist of '^' and white space. + cp = output; + ep = output + strlen(output); + for (;;) { + size_t len = strspn(cp, "^ \t"); + if (len > 0 && cp[len] == '\n') { + /* Prune out lines that are "underlining". */ + memmove(cp, cp + len + 1, ep - cp); + if (*cp == '\0') + break; + } else { + /* No match, move to the next line. */ + cp = strchr(cp, '\n'); + if (cp == NULL) + break; + cp++; + } + } +} + +static void +clean_output(char *output) +{ + // we replace some output which otherwise would be test run dependent + str_replace_in_place(output, MAX_OUTPUT, data.tmp_dir, TEMP_PATH_TEMPLATE); + + if (data.tmp_dir2) + str_replace_in_place(output, MAX_OUTPUT, data.tmp_dir2, TEMP_PATH_TEMPLATE "2"); + + str_replace_in_place(output, MAX_OUTPUT, SRC_DIR, "SRC_DIR"); + + remove_underline(output); +} + +const char * +expected_path(const char *format, ...) +{ + static char expected_output_file[PATH_MAX]; + size_t dirlen = strlcpy(expected_output_file, TESTDATA_DIR, sizeof(expected_output_file)); + + va_list args; + va_start(args, format); + vsnprintf(expected_output_file + dirlen, PATH_MAX - dirlen, format, args); + va_end(args); + + return expected_output_file; +} + +char ** +create_str_array(size_t count, ...) +{ + va_list args; + + va_start(args, count); + + char **result = calloc(count, sizeof(char *)); + if (result != NULL) { + for (size_t i = 0; i < count; ++i) { + const char *str = va_arg(args, char *); + if (str != NULL) { + result[i] = strdup(str); + if (result[i] == NULL) { + while (i > 0) { + free(result[--i]); + } + free(result); + result = NULL; + break; + } + } + } + } + + va_end(args); + return result; +} + +int +is_update(void) +{ + static int result = -1; + if (result < 0) { + const char *update = getenv("UPDATE_TESTDATA"); + result = (update && strcmp(update, "1") == 0) ? 1 : 0; + } + return result; +} + +int +verify_content(char *actual_content, const char *reference_path) +{ + clean_output(actual_content); + + if (is_update()) { + VERIFY_TRUE(fwriteall(reference_path, actual_content)); + } else { + char expected_output[MAX_OUTPUT] = ""; + if (!freadall(reference_path, expected_output, sizeof(expected_output))) { + printf("Error: Missing test data at '%s'\n", reference_path); + return false; + } + VERIFY_STR(actual_content, expected_output); + } + + return true; +} + +int +verify_file(const char *actual_dir, const char *actual_file_name, const char *reference_path) +{ + char actual_path[PATH_MAX]; + snprintf(actual_path, sizeof(actual_path), "%s/%s", actual_dir, actual_file_name); + + char actual_str[MAX_OUTPUT]; + if (!freadall(actual_path, actual_str, sizeof(actual_str))) { + printf("Expected that file '%s' gets created, but it was not\n", actual_path); + return false; + } + + int rc = verify_content(actual_str, reference_path); + return rc; +} + +int +fake_conversation(int num_msgs, const struct sudo_conv_message msgs[], + struct sudo_conv_reply replies[], struct sudo_conv_callback *callback) +{ + (void) callback; + snprintf_append(data.conv_str, MAX_OUTPUT, "Question count: %d\n", num_msgs); + for (int i = 0; i < num_msgs; ++i) { + const struct sudo_conv_message *msg = &msgs[i]; + snprintf_append(data.conv_str, MAX_OUTPUT, "Question %d: <<%s>> (timeout: %d, msg_type=%d)\n", + i, msg->msg, msg->timeout, msg->msg_type); + + if (data.conv_replies[i] == NULL) + return 1; // simulates user interruption (conversation error) + + replies[i].reply = strdup(data.conv_replies[i]); + if (replies[i].reply == NULL) + return 1; // memory allocation error + } + + return 0; // simulate user answered just fine +} + +int +fake_conversation_with_suspend(int num_msgs, const struct sudo_conv_message msgs[], + struct sudo_conv_reply replies[], struct sudo_conv_callback *callback) +{ + if (callback != NULL) { + callback->on_suspend(SIGTSTP, callback->closure); + callback->on_resume(SIGCONT, callback->closure); + } + + return fake_conversation(num_msgs, msgs, replies, callback); +} + +int +fake_printf(int msg_type, const char *fmt, ...) +{ + int rc = -1; + va_list args; + va_start(args, fmt); + + char *output = NULL; + switch(msg_type) { + case SUDO_CONV_INFO_MSG: + output = data.stdout_str; + break; + case SUDO_CONV_ERROR_MSG: + output = data.stderr_str; + break; + default: + break; + } + + if (output) + rc = vsnprintf_append(output, MAX_OUTPUT, fmt, args); + + va_end(args); + return rc; +} + +int +verify_log_lines(const char *reference_path) +{ + char stored_path[PATH_MAX]; + snprintf(stored_path, sizeof(stored_path), "%s/%s", data.tmp_dir, "debug.log"); + + FILE *file = fopen(stored_path, "rb"); + if (file == NULL) { + printf("Failed to open file '%s'\n", stored_path); + return false; + } + + char line[1024] = ""; + char stored_str[MAX_OUTPUT] = ""; + while (fgets(line, sizeof(line), file) != NULL) { + char *line_data = strstr(line, "] "); // this skips the timestamp and pid at the beginning + VERIFY_NOT_NULL(line_data); // malformed log line + line_data += 2; + + char *line_end = strstr(line_data, " object at "); // this skips checking the pointer hex + if (line_end) + snprintf(line_end, sizeof(line) - (line_end - line), " object>\n"); + + if (strncmp(line_data, "handle @ /", sizeof("handle @ /") - 1) == 0) { + char *start = line_data + sizeof("handle @ ") - 1; + + // normalize path to logging/__init__.py + char *logging = strstr(start, "logging/"); + if (logging != NULL) { + memmove(start, logging, strlen(logging) + 1); + } + + // remove line number + char *colon = strchr(start, ':'); + if (colon != NULL) { + size_t len = strspn(colon + 1, "0123456789"); + if (len != 0) + memmove(colon, colon + len + 1, strlen(colon + len + 1) + 1); + } + } else if (strncmp(line_data, "LogHandler.emit was called ", 27) == 0) { + // LogHandler.emit argument details vary based on python version + line_data[26] = '\n'; + line_data[27] = '\0'; + } else { + // Python 3.11 uses 0 instead of the symbolic REJECT in backtraces + char *cp = strstr(line_data, ": REJECT"); + if (cp != NULL) { + // Convert ": REJECT" to ": 0" + rest of line + memcpy(cp, ": 0", 3); + memmove(cp + 3, cp + 8, strlen(cp + 8) + 1); + } + } + + VERIFY_TRUE(strlcat(stored_str, line_data, sizeof(stored_str)) < sizeof(stored_str)); // we have enough space in buffer + } + + clean_output(stored_str); + + VERIFY_TRUE(verify_content(stored_str, reference_path)); + return true; +} + +int +verify_str_set(char **actual_set, char **expected_set, const char *actual_variable_name) +{ + VERIFY_NOT_NULL(actual_set); + VERIFY_NOT_NULL(expected_set); + + int actual_len = str_array_count(actual_set); + int expected_len = str_array_count(expected_set); + + int matches = false; + if (actual_len == expected_len) { + int actual_pos = 0; + for (; actual_pos < actual_len; ++actual_pos) { + char *actual_item = actual_set[actual_pos]; + + int expected_pos = 0; + for (; expected_pos < expected_len; ++expected_pos) { + if (strcmp(actual_item, expected_set[expected_pos]) == 0) + break; + } + + if (expected_pos == expected_len) { + // matching item was not found + break; + } + } + + matches = (actual_pos == actual_len); + } + + if (!matches) { + char actual_set_str[MAX_OUTPUT] = ""; + char expected_set_str[MAX_OUTPUT] = ""; + str_array_snprint(actual_set_str, MAX_OUTPUT, actual_set, actual_len); + str_array_snprint(expected_set_str, MAX_OUTPUT, expected_set, expected_len); + + VERIFY_PRINT_MSG("%s", actual_variable_name, actual_set_str, "expected", + expected_set_str, "expected to contain the same elements as"); + return false; + } + + return true; +} + +int +mock_python_datetime_now(const char *plugin_name, const char *date_str) +{ + char *cmd = NULL; + int len; + len = asprintf(&cmd, + "import %s\n" // the plugin has its own submodule + "from datetime import datetime\n" // store the real datetime + "import time\n" + "from unittest.mock import Mock\n" + "%s.datetime = Mock()\n" // replace plugin's datetime + "%s.datetime.now = lambda: datetime.strptime('%s', '%%Y-%%m-%%dT%%H:%%M:%%S')\n", + plugin_name, plugin_name, plugin_name, date_str); + if (len == -1) + return false; + VERIFY_PTR_NE(cmd, NULL); + VERIFY_INT(PyRun_SimpleString(cmd), 0); + free(cmd); + return true; +} diff --git a/plugins/python/regress/testhelpers.h b/plugins/python/regress/testhelpers.h new file mode 100644 index 0000000..2dd1d54 --- /dev/null +++ b/plugins/python/regress/testhelpers.h @@ -0,0 +1,175 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 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. + */ + +#ifndef PYTHON_TESTHELPERS +#define PYTHON_TESTHELPERS + +#include "iohelpers.h" + +#include "../pyhelpers.h" + +#include "sudo_conf.h" + +// just for the IDE +#ifndef SRC_DIR +#define SRC_DIR "" +#endif +#define TESTDATA_DIR SRC_DIR "/regress/testdata/" + +#define TEMP_PATH_TEMPLATE "/tmp/sudo_check_python_exampleXXXXXX" + +extern struct TestData { + char *tmp_dir; + char *tmp_dir2; + char stdout_str[MAX_OUTPUT]; + char stderr_str[MAX_OUTPUT]; + + char conv_str[MAX_OUTPUT]; + const char *conv_replies[8]; + + // some example test data used by multiple test cases: + char ** settings; + char ** user_info; + char ** command_info; + char ** plugin_argv; + int plugin_argc; + char ** user_env; + char ** plugin_options; +} data; + +const char * expected_path(const char *format, ...); + +char ** create_str_array(size_t count, ...); + +#define RUN_TEST(testcase) \ + do { \ + int success = 1; \ + ntests++; \ + if (verbose) { \ + printf("Running test " #testcase " ... \n"); \ + } \ + if (!init()) { \ + printf("FAILED: initialization of testcase %s at %s:%d\n", #testcase, __FILE__, __LINE__); \ + success = 0; \ + } else \ + if (!testcase) { \ + printf("FAILED: testcase %s at %s:%d\n", #testcase, __FILE__, __LINE__); \ + success = 0; \ + } \ + if (!cleanup(success)) { \ + printf("FAILED: deinitialization of testcase %s at %s:%d\n", #testcase, __FILE__, __LINE__); \ + success = 0; \ + } \ + if (!success) { \ + errors++; \ + } \ + } while(false) + +#define VERIFY_PRINT_MSG(fmt, actual_str, actual, expected_str, expected, expected_to_be_message) \ + printf("Expectation failed at %s:%d:\n actual is <<" fmt ">>: %s\n %s <<" fmt ">>: %s\n", \ + __FILE__, __LINE__, actual, actual_str, expected_to_be_message, expected, expected_str) + +#define VERIFY_CUSTOM(fmt, type, actual, expected, invert) \ + do { \ + type actual_value = (type)(actual); \ + int failed = (actual_value != expected); \ + if (invert) \ + failed = !failed; \ + if (failed) { \ + VERIFY_PRINT_MSG(fmt, #actual, actual_value, #expected, expected, invert ? "not expected to be" : "expected to be"); \ + return false; \ + } \ + } while(false) + +#define VERIFY_EQ(fmt, type, actual, expected) VERIFY_CUSTOM(fmt, type, actual, expected, false) +#define VERIFY_NE(fmt, type, actual, not_expected) VERIFY_CUSTOM(fmt, type, actual, not_expected, true) + +#define VERIFY_INT(actual, expected) VERIFY_EQ("%d", int, actual, expected) + +#define VERIFY_PTR(actual, expected) VERIFY_EQ("%p", const void *, (const void *)actual, (const void *)expected) +#define VERIFY_PTR_NE(actual, not_expected) VERIFY_NE("%p", const void *, (const void *)actual, (const void *)not_expected) + +#define VERIFY_TRUE(actual) VERIFY_NE("%d", int, actual, 0) +#define VERIFY_FALSE(actual) VERIFY_INT(actual, false) + +#define VERIFY_NOT_NULL(actual) VERIFY_NE("%p", const void *, actual, NULL) + +#define VERIFY_STR(actual, expected) \ + do { \ + const char *actual_str = actual; \ + if (!actual_str || strcmp(actual_str, expected) != 0) { \ + VERIFY_PRINT_MSG("%s", #actual, actual_str ? actual_str : "(null)", #expected, expected, "expected to be"); \ + return false; \ + } \ + } while(false) + +#define VERIFY_STR_CONTAINS(actual, expected) \ + do { \ + const char *actual_str = actual; \ + if (!actual_str || strstr(actual_str, expected) == NULL) { \ + VERIFY_PRINT_MSG("%s", #actual, actual_str ? actual_str : "(null)", #expected, expected, "expected to contain the string"); \ + return false; \ + } \ + } while(false) + +int is_update(void); + +int verify_content(char *actual_content, const char *reference_path); + +#define VERIFY_CONTENT(actual_output, reference_path) \ + VERIFY_TRUE(verify_content(actual_output, reference_path)) + +#define VERIFY_STDOUT(reference_path) \ + VERIFY_CONTENT(data.stdout_str, reference_path) + +#define VERIFY_STDERR(reference_path) \ + VERIFY_CONTENT(data.stderr_str, reference_path) + +#define VERIFY_CONV(reference_name) \ + VERIFY_CONTENT(data.conv_str, reference_name) + +int verify_file(const char *actual_dir, const char *actual_file_name, const char *reference_path); + +#define VERIFY_FILE(actual_file_name, reference_path) \ + VERIFY_TRUE(verify_file(data.tmp_dir, actual_file_name, reference_path)) + +int fake_conversation(int num_msgs, const struct sudo_conv_message msgs[], + struct sudo_conv_reply replies[], struct sudo_conv_callback *callback); + +int fake_conversation_with_suspend(int num_msgs, const struct sudo_conv_message msgs[], + struct sudo_conv_reply replies[], struct sudo_conv_callback *callback); + +int fake_printf(int msg_type, const char *fmt, ...); + +int verify_log_lines(const char *reference_path); + +int mock_python_datetime_now(const char *plugin_name, const char *date_str); + +#define VERIFY_LOG_LINES(reference_path) \ + VERIFY_TRUE(verify_log_lines(reference_path)) + +int verify_str_set(char **actual_set, char **expected_set, const char *actual_variable_name); + +#define VERIFY_STR_SET(actual_set, ...) \ + do { \ + char **expected_set = create_str_array(__VA_ARGS__); \ + VERIFY_TRUE(verify_str_set(actual_set, expected_set, #actual_set)); \ + str_array_free(&expected_set); \ + } while(false) + +#endif // PYTHON_TESTHELPERS |