summaryrefslogtreecommitdiffstats
path: root/plugins/python/regress
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--plugins/python/regress/check_python_examples.c1619
-rw-r--r--plugins/python/regress/iohelpers.c182
-rw-r--r--plugins/python/regress/iohelpers.h58
-rw-r--r--plugins/python/regress/plugin_approval_test.py22
-rw-r--r--plugins/python/regress/plugin_conflict.py11
-rw-r--r--plugins/python/regress/plugin_errorstr.py18
-rw-r--r--plugins/python/regress/testdata/check_example_audit_plugin_receives_accept.stdout7
-rw-r--r--plugins/python/regress/testdata/check_example_audit_plugin_receives_error.stdout5
-rw-r--r--plugins/python/regress/testdata/check_example_audit_plugin_receives_reject.stdout5
-rw-r--r--plugins/python/regress/testdata/check_example_audit_plugin_version_display.stdout6
-rw-r--r--plugins/python/regress/testdata/check_example_audit_plugin_workflow_multiple.stderr1
-rw-r--r--plugins/python/regress/testdata/check_example_audit_plugin_workflow_multiple.stdout14
-rw-r--r--plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.conversation3
-rw-r--r--plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stderr0
-rw-r--r--plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stdout3
-rw-r--r--plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stored3
-rw-r--r--plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.conversation3
-rw-r--r--plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stderr0
-rw-r--r--plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stdout1
-rw-r--r--plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stored3
-rw-r--r--plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.conv2
-rw-r--r--plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.conversation2
-rw-r--r--plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.stderr1
-rw-r--r--plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.stdout1
-rw-r--r--plugins/python/regress/testdata/check_example_debugging_c_calls@diag.log6
-rw-r--r--plugins/python/regress/testdata/check_example_debugging_c_calls@info.log11
-rw-r--r--plugins/python/regress/testdata/check_example_debugging_load@diag.log3
-rw-r--r--plugins/python/regress/testdata/check_example_debugging_plugin@err.log2
-rw-r--r--plugins/python/regress/testdata/check_example_debugging_plugin@info.log8
-rw-r--r--plugins/python/regress/testdata/check_example_debugging_py_calls@diag.log2
-rw-r--r--plugins/python/regress/testdata/check_example_debugging_py_calls@info.log9
-rw-r--r--plugins/python/regress/testdata/check_example_debugging_sudo_cb@info.log1
-rw-r--r--plugins/python/regress/testdata/check_example_group_plugin_is_able_to_debug.log4
-rw-r--r--plugins/python/regress/testdata/check_example_io_plugin_command_log.stderr0
-rw-r--r--plugins/python/regress/testdata/check_example_io_plugin_command_log.stdout1
-rw-r--r--plugins/python/regress/testdata/check_example_io_plugin_command_log.stored16
-rw-r--r--plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple.stderr1
-rw-r--r--plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple.stdout2
-rw-r--r--plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple1.stored16
-rw-r--r--plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple2.stored16
-rw-r--r--plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stderr0
-rw-r--r--plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stdout1
-rw-r--r--plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stored8
-rw-r--r--plugins/python/regress/testdata/check_example_io_plugin_fails_with_python_backtrace.stderr1
-rw-r--r--plugins/python/regress/testdata/check_example_io_plugin_fails_with_python_backtrace.stdout7
-rw-r--r--plugins/python/regress/testdata/check_example_io_plugin_version_display.stderr0
-rw-r--r--plugins/python/regress/testdata/check_example_io_plugin_version_display.stdout2
-rw-r--r--plugins/python/regress/testdata/check_example_io_plugin_version_display.stored2
-rw-r--r--plugins/python/regress/testdata/check_example_io_plugin_version_display_full.stdout3
-rw-r--r--plugins/python/regress/testdata/check_example_policy_plugin_accepted_execution.stderr0
-rw-r--r--plugins/python/regress/testdata/check_example_policy_plugin_accepted_execution.stdout1
-rw-r--r--plugins/python/regress/testdata/check_example_policy_plugin_denied_execution.stderr1
-rw-r--r--plugins/python/regress/testdata/check_example_policy_plugin_denied_execution.stdout0
-rw-r--r--plugins/python/regress/testdata/check_example_policy_plugin_failed_execution.stderr1
-rw-r--r--plugins/python/regress/testdata/check_example_policy_plugin_failed_execution.stdout0
-rw-r--r--plugins/python/regress/testdata/check_example_policy_plugin_list.stderr0
-rw-r--r--plugins/python/regress/testdata/check_example_policy_plugin_list.stdout25
-rw-r--r--plugins/python/regress/testdata/check_example_policy_plugin_validate_invalidate.log8
-rw-r--r--plugins/python/regress/testdata/check_example_policy_plugin_version_display.stderr0
-rw-r--r--plugins/python/regress/testdata/check_example_policy_plugin_version_display.stdout1
-rw-r--r--plugins/python/regress/testdata/check_example_policy_plugin_version_display_full.stdout2
-rw-r--r--plugins/python/regress/testdata/check_loading_fails_missing_classname.stderr3
-rw-r--r--plugins/python/regress/testdata/check_loading_fails_missing_classname.stdout0
-rw-r--r--plugins/python/regress/testdata/check_loading_fails_missing_path.stderr2
-rw-r--r--plugins/python/regress/testdata/check_loading_fails_missing_path.stdout0
-rw-r--r--plugins/python/regress/testdata/check_loading_fails_not_owned_by_root.stderr1
-rw-r--r--plugins/python/regress/testdata/check_loading_fails_not_owned_by_root.stdout0
-rw-r--r--plugins/python/regress/testdata/check_loading_fails_wrong_classname.stderr2
-rw-r--r--plugins/python/regress/testdata/check_loading_fails_wrong_classname.stdout0
-rw-r--r--plugins/python/regress/testdata/check_loading_fails_wrong_path.stderr1
-rw-r--r--plugins/python/regress/testdata/check_loading_fails_wrong_path.stdout0
-rw-r--r--plugins/python/regress/testdata/check_loading_succeeds_with_missing_classname.stdout1
-rw-r--r--plugins/python/regress/testdata/check_multiple_approval_plugin_and_arguments.stderr1
-rw-r--r--plugins/python/regress/testdata/check_multiple_approval_plugin_and_arguments.stdout67
-rw-r--r--plugins/python/regress/testdata/check_python_plugins_do_not_affect_each_other.stdout4
-rw-r--r--plugins/python/regress/testhelpers.c356
-rw-r--r--plugins/python/regress/testhelpers.h175
77 files changed, 2744 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..108d85d
--- /dev/null
+++ b/plugins/python/regress/check_python_examples.c
@@ -0,0 +1,1619 @@
+/*
+ * 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)
+ {
+ puts("Failed to allocate string");
+ 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];
+
+ // Unbuffer stdout so we don't miss output during a crash.
+ setvbuf(stdout, NULL, _IONBF, 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..7b85814
--- /dev/null
+++ b/plugins/python/regress/iohelpers.c
@@ -0,0 +1,182 @@
+/*
+ * 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"
+#include <sudo_fatal.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) {
+ sudo_warn_nodebug("failed to open file '%s'", file_path);
+ goto cleanup;
+ }
+
+ size_t len = fread(output, 1, max_len - 1, file);
+ output[len] = '\0';
+
+ if (ferror(file) != 0) {
+ sudo_warn_nodebug("failed to read file '%s'", file_path);
+ goto cleanup;
+ }
+
+ if (!feof(file)) {
+ sudo_warn_nodebug("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 * restrict output, size_t max_output_len, const char * restrict 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 * restrict output, size_t max_output_len, const char * restrict 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 = (size_t)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..b0fc5ab
--- /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 * restrict output, size_t max_output_len, const char * restrict fmt, va_list args);
+int snprintf_append(char * restrict output, size_t max_output_len, const char * restrict 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..352f421
--- /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..d569291
--- /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..574b058
--- /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..9c0ba9e
--- /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..b80c5e6
--- /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..1a73a59
--- /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..b7bbe76
--- /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..b25007a
--- /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..db4ac27
--- /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..ee55fb9
--- /dev/null
+++ b/plugins/python/regress/testhelpers.c
@@ -0,0 +1,356 @@
+/*
+ * 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, (size_t)(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 * restrict 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) - (size_t)(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);
+ } else {
+ // Python 3.12 may use <RC.REJECT: 0> instead of 0
+ cp = strstr(line_data, "<RC.REJECT: 0>");
+ if (cp != NULL) {
+ *cp = '0';
+ memmove(cp + 1, cp + 14, strlen(cp + 14) + 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..8f78d9f
--- /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 * restrict 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