diff --git a/MANIFEST b/MANIFEST index 9da1b9e05..23ac7c32e 100644 --- a/MANIFEST +++ b/MANIFEST @@ -323,6 +323,11 @@ plugins/python/sudo_python_debug.c plugins/python/sudo_python_debug.h plugins/python/sudo_python_module.c plugins/python/sudo_python_module.h +plugins/python/regress/check_python_examples.c +plugins/python/regress/iohelpers.c +plugins/python/regress/iohelpers.h +plugins/python/regress/testhelpers.c +plugins/python/regress/testhelpers.h plugins/sample/Makefile.in plugins/sample/README plugins/sample/sample_plugin.c diff --git a/plugins/python/Makefile.in b/plugins/python/Makefile.in index 664df2db6..486c8cf8b 100644 --- a/plugins/python/Makefile.in +++ b/plugins/python/Makefile.in @@ -26,6 +26,7 @@ srcdir = @srcdir@ devdir = @devdir@ top_builddir = @top_builddir@ top_srcdir = @top_srcdir@ +abs_srcdir = @abs_srcdir@ incdir = $(top_srcdir)/include cross_compiling = @CROSS_COMPILING@ @@ -44,8 +45,10 @@ INSTALL_BACKUP = @INSTALL_BACKUP@ LT_LIBS = $(top_builddir)/lib/util/libsudo_util.la LIBS = $(LT_LIBS) +LIBPYTHONPLUGIN = python_plugin.la + # C preprocessor flags -CPPFLAGS = -I$(incdir) -I$(top_builddir) -I$(top_srcdir) @CPPFLAGS@ @PYTHON_INCLUDE@ +CPPFLAGS = -I$(incdir) -I$(top_builddir) -I$(top_srcdir) -DSRC_DIR=\"$(abs_srcdir)\" @CPPFLAGS@ @PYTHON_INCLUDE@ # Usually -O and/or -g CFLAGS = @CFLAGS@ @@ -122,12 +125,19 @@ LIBOBJDIR = $(top_builddir)/@ac_config_libobj_dir@/ VERSION = @PACKAGE_VERSION@ +TEST_PROGS = check_python_examples + +CHECK_PYTHON_EXAMPLES_OBJS = check_python_examples.o iohelpers.o testhelpers.o + all: python_plugin.la Makefile: $(srcdir)/Makefile.in cd $(top_builddir) && ./config.status --file plugins/python/Makefile -.SUFFIXES: .c .h .i .lo .plog +.SUFFIXES: .c .h .i .lo .plog .o + +.c.o: + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $< .c.lo: $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $< @@ -182,11 +192,9 @@ pvs-log-files: $(POBJS) pvs-studio: $(POBJS) plog-converter $(PVS_LOG_OPTS) $(POBJS) -check: - clean: -$(LIBTOOL) $(LTFLAGS) --mode=clean rm -f *.lo *.o *.la - -rm -f *.i *.plog stamp-* core *.core core.* + -rm -f *.i *.plog stamp-* core *.core core.* $(TEST_PROGS) mostlyclean: clean @@ -200,7 +208,27 @@ realclean: distclean cleandir: realclean +check: $(TEST_PROGS) + @if test X"$(cross_compiling)" != X"yes"; then \ + ./check_python_examples; \ + fi + +check_python_examples: $(CHECK_PYTHON_EXAMPLES_OBJS) $(LIBPYTHONPLUGIN) + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_PYTHON_EXAMPLES_OBJS) $(LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(SSP_LDFLAGS) $(LIBS) $(LIBPYTHONPLUGIN) + # Autogenerated dependencies, do not modify +check_python_examples.o: $(srcdir)/regress/check_python_examples.c + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/regress/check_python_examples.c +check_python_examples.i: $(srcdir)/regress/check_python_examples.c + $(CC) -E -o $@ $(CPPFLAGS) $< +check_python_examples.plog: check_python_examples.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/regress/check_python_examples.c --i-file $< --output-file $@ +iohelpers.o: $(srcdir)/regress/iohelpers.c + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/regress/iohelpers.c +iohelpers.i: $(srcdir)/regress/iohelpers.c + $(CC) -E -o $@ $(CPPFLAGS) $< +iohelpers.plog: iohelpers.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/regress/iohelpers.c --i-file $< --output-file $@ pyhelpers.lo: $(srcdir)/pyhelpers.c $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/pyhelpers.c pyhelpers.i: $(srcdir)/pyhelpers.c @@ -271,3 +299,9 @@ sudo_python_module.i: $(srcdir)/sudo_python_module.c $(CC) -E -o $@ $(CPPFLAGS) $< sudo_python_module.plog: sudo_python_module.i rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/sudo_python_module.c --i-file $< --output-file $@ +testhelpers.o: $(srcdir)/regress/testhelpers.c + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/regress/testhelpers.c +testhelpers.i: $(srcdir)/regress/testhelpers.c + $(CC) -E -o $@ $(CPPFLAGS) $< +testhelpers.plog: testhelpers.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/regress/testhelpers.c --i-file $< --output-file $@ diff --git a/plugins/python/regress/check_python_examples.c b/plugins/python/regress/check_python_examples.c new file mode 100644 index 000000000..e43fe27a8 --- /dev/null +++ b/plugins/python/regress/check_python_examples.c @@ -0,0 +1,718 @@ +#include "testhelpers.h" + +extern struct io_plugin python_io; +extern struct policy_plugin python_policy; +extern struct sudoers_group_plugin group_plugin; + +void +create_io_plugin_options(const char *log_path) +{ + static char logpath_keyvalue[PATH_MAX + 16]; + snprintf(logpath_keyvalue, sizeof(logpath_keyvalue), "LogPath=%s", log_path); + + free(data.plugin_options); + data.plugin_options = create_str_array( + 4, + "ModulePath=" SRC_DIR "/example_io_plugin.py", + "ClassName=SudoIOPlugin", + logpath_keyvalue, + NULL + ); +} + +void +create_group_plugin_options(void) +{ + free(data.plugin_options); + data.plugin_options = create_str_array( + 3, + "ModulePath=" SRC_DIR "/example_group_plugin.py", + "ClassName=SudoGroupPlugin", + NULL + ); +} + +void +create_debugging_plugin_options(void) +{ + free(data.plugin_options); + data.plugin_options = create_str_array( + 3, + "ModulePath=" SRC_DIR "/example_debugging.py", + "ClassName=DebugDemoPlugin", + NULL + ); +} + +void +create_conversation_plugin_options(void) +{ + static char logpath_keyvalue[PATH_MAX + 16]; + snprintf(logpath_keyvalue, sizeof(logpath_keyvalue), "LogPath=%s", data.tmp_dir); + + free(data.plugin_options); + data.plugin_options = create_str_array( + 4, + "ModulePath=" SRC_DIR "/example_conversation.py", + "ClassName=ReasonLoggerIOPlugin", + logpath_keyvalue, + NULL + ); +} + +void +create_policy_plugin_options(void) +{ + free(data.plugin_options); + data.plugin_options = create_str_array( + 3, + "ModulePath=" SRC_DIR "/example_policy_plugin.py", + "ClassName=SudoPolicyPlugin", + NULL + ); +} + +int +init(void) +{ + // always start each test from clean state + memset(&data, 0, sizeof(data)); + + VERIFY_TRUE(asprintf(&data.tmp_dir, TEMP_PATH_TEMPLATE) >= 0); + VERIFY_NOT_NULL(mkdtemp(data.tmp_dir)); + + // by default we test in developer mode, so the python plugin can be loaded + sudo_conf_clear_paths(); + VERIFY_INT(sudo_conf_read(sudo_conf_developer_mode, SUDO_CONF_ALL), true); + + // 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.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); + + return true; +} + +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)); + + free(data.settings); + free(data.user_info); + free(data.command_info); + free(data.plugin_argv); + free(data.user_env); + free(data.plugin_options); + + VERIFY_FALSE(Py_IsInitialized()); + return true; +} + +int +check_example_io_plugin_version_display(int is_verbose) +{ + 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), 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 dependant + VERIFY_STR_CONTAINS(data.stdout_str, "Python interpreter version:"); + VERIFY_STR_CONTAINS(data.stdout_str, "Python io plugin API version"); + } 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; +} + +int +check_example_io_plugin_command_log(void) +{ + create_io_plugin_options(data.tmp_dir); + + free(data.plugin_argv); + data.plugin_argc = 2; + data.plugin_argv = create_str_array(3, "id", "--help", NULL); + + 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), SUDO_RC_OK); + VERIFY_INT(python_io.log_stdin("some standard input", strlen("some standard input")), SUDO_RC_OK); + VERIFY_INT(python_io.log_stdout("some standard output", strlen("some standard output")), SUDO_RC_OK); + VERIFY_INT(python_io.log_stderr("some standard error", strlen("some standard error")), SUDO_RC_OK); + VERIFY_INT(python_io.log_suspend(SIGTSTP), SUDO_RC_OK); + VERIFY_INT(python_io.log_suspend(SIGCONT), SUDO_RC_OK); + VERIFY_INT(python_io.change_winsize(200, 100), SUDO_RC_OK); + VERIFY_INT(python_io.log_ttyin("some tty input", strlen("some tty input")), SUDO_RC_OK); + VERIFY_INT(python_io.log_ttyout("some tty output", strlen("some tty output")), SUDO_RC_OK); + + 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; +} + +int +check_example_io_plugin_failed_to_start_command(void) +{ + create_io_plugin_options(data.tmp_dir); + + free(data.plugin_argv); + data.plugin_argc = 1; + data.plugin_argv = create_str_array(2, "cmd", NULL); + + 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), SUDO_RC_OK); + + 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; +} + +int +check_example_io_plugin_fails_with_python_backtrace(void) +{ + 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), SUDO_RC_ERROR); + + 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; +} + +int +check_example_group_plugin(void) +{ + create_group_plugin_options(); + + 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; +} + +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, "Set developer_mode true\n" + "Debug %s %s/debug.log %s\n", + "python_plugin.so", data.tmp_dir, debug_spec) < 0) + { + printf("Failed to allocate string\n"); + goto cleanup; + } + + if (fwriteall(config_path, content) != true) { + printf("Failed to write '%s'\n", config_path); + goto cleanup; + } + + result = config_path; + +cleanup: + free(content); + + return result; +} + +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_group_plugin_options(); + + 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; +} + +int +check_example_debugging(const char *debug_spec) +{ + 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(); + + 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), SUDO_RC_OK); + 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; +} + +int +check_loading_fails(const char *name) +{ + 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), SUDO_RC_ERROR); + 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; +} + +int +check_loading_fails_with_missing_path(void) +{ + free(data.plugin_options); + data.plugin_options = create_str_array(2, "ClassName=DebugDemoPlugin", NULL); + return check_loading_fails("missing_path"); +} + +int +check_loading_fails_with_missing_classname(void) +{ + free(data.plugin_options); + data.plugin_options = create_str_array(2, "ModulePath=" SRC_DIR "/example_debugging.py", NULL); + return check_loading_fails("missing_classname"); +} + +int +check_loading_fails_with_wrong_classname(void) +{ + free(data.plugin_options); + data.plugin_options = create_str_array(3, "ModulePath=" SRC_DIR "/example_debugging.py", + "ClassName=MispelledPluginName", NULL); + return check_loading_fails("wrong_classname"); +} + +int +check_loading_fails_with_wrong_path(void) +{ + free(data.plugin_options); + data.plugin_options = create_str_array(3, "ModulePath=/wrong_path.py", "ClassName=PluginName", NULL); + return check_loading_fails("wrong_path"); +} + +int +check_loading_fails_plugin_is_not_owned_by_root(void) +{ + sudo_conf_clear_paths(); + VERIFY_INT(sudo_conf_read(sudo_conf_normal_mode, SUDO_CONF_ALL), true); + + create_debugging_plugin_options(); + return check_loading_fails("not_owned_by_root"); +} + +int +check_example_conversation_plugin_reason_log(int simulate_suspend, const char *description) +{ + create_conversation_plugin_options(); + + 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), SUDO_RC_OK); + 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; +} + +int +check_example_conversation_plugin_user_interrupts(void) +{ + create_conversation_plugin_options(); + + 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), SUDO_RC_REJECT); + 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; +} + +int +check_example_policy_plugin_version_display(int is_verbose) +{ + 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), + SUDO_RC_OK); + 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 dependant + VERIFY_STR_CONTAINS(data.stdout_str, "Python interpreter version:"); + VERIFY_STR_CONTAINS(data.stdout_str, "Python policy plugin API version"); + } 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; +} + +int +check_example_policy_plugin_accepted_execution(void) +{ + create_policy_plugin_options(); + + data.plugin_argc = 2; + data.plugin_argv = create_str_array(3, "/bin/whoami", "--help", NULL); + + 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), + SUDO_RC_OK); + + 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), + SUDO_RC_ACCEPT); + + 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), SUDO_RC_ACCEPT); + + // 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")); + + free(env_add); + free(user_env_out); + free(command_info_out); + free(argv_out); + return true; +} + +int +check_example_policy_plugin_failed_execution(void) +{ + create_policy_plugin_options(); + + 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), + SUDO_RC_OK); + + 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), + SUDO_RC_ACCEPT); + + // pwd is unset (user is not part of /etc/passwd) + VERIFY_INT(python_policy.init_session(NULL, &user_env_out), SUDO_RC_ACCEPT); + + 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")); + + free(user_env_out); + free(command_info_out); + free(argv_out); + return true; +} + +int +check_example_policy_plugin_denied_execution(void) +{ + create_policy_plugin_options(); + + 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), + SUDO_RC_OK); + + 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), + SUDO_RC_REJECT); + + 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; +} + +int +check_example_policy_plugin_list(void) +{ + 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), + SUDO_RC_OK); + + snprintf_append(data.stdout_str, MAX_OUTPUT, "-- minimal --\n"); + VERIFY_INT(python_policy.list(data.plugin_argc, data.plugin_argv, false, NULL), SUDO_RC_OK); + + snprintf_append(data.stdout_str, MAX_OUTPUT, "\n-- minimal (verbose) --\n"); + VERIFY_INT(python_policy.list(data.plugin_argc, data.plugin_argv, true, NULL), SUDO_RC_OK); + + snprintf_append(data.stdout_str, MAX_OUTPUT, "\n-- with user --\n"); + VERIFY_INT(python_policy.list(data.plugin_argc, data.plugin_argv, false, "testuser"), SUDO_RC_OK); + + 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"), SUDO_RC_OK); + + snprintf_append(data.stdout_str, MAX_OUTPUT, "\n-- with allowed program --\n"); + 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), SUDO_RC_OK); + + 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), SUDO_RC_OK); + + snprintf_append(data.stdout_str, MAX_OUTPUT, "\n-- with denied program --\n"); + 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), SUDO_RC_OK); + + 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), SUDO_RC_OK); + + 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; +} + +int +check_example_policy_plugin_validate_invalidate(void) +{ + // 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); + + 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), + SUDO_RC_OK); + VERIFY_INT(python_policy.validate(), SUDO_RC_OK); + python_policy.invalidate(true); + python_policy.invalidate(false); + + python_policy.close(0, 0); // no command execution + + VERIFY_LOG_LINES(expected_path("check_example_policy_plugin_validate_invalidate.log")); + VERIFY_STR(data.stderr_str, ""); + VERIFY_STR(data.stdout_str, ""); + return true; +} + +int +check_policy_plugin_callbacks_are_optional(void) +{ + 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), + SUDO_RC_OK); + + 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); + VERIFY_PTR(python_policy.show_version, NULL); + + python_io.close(0, 0); + return true; +} + +int +check_io_plugin_callbacks_are_optional(void) +{ + 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), SUDO_RC_OK); + + 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.show_version, NULL); + VERIFY_PTR(python_io.change_winsize, NULL); + + python_io.close(0, 0); + return true; +} + +int +main(int argc, char *argv[]) +{ + (void) argc; + (void) argv; + + 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_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_example_group_plugin()); + RUN_TEST(check_example_group_plugin_is_able_to_debug()); + + RUN_TEST(check_loading_fails_with_missing_path()); + 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_loading_fails_plugin_is_not_owned_by_root()); + + 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_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_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")); + + return EXIT_SUCCESS; +} diff --git a/plugins/python/regress/iohelpers.c b/plugins/python/regress/iohelpers.c new file mode 100644 index 000000000..67362e0d7 --- /dev/null +++ b/plugins/python/regress/iohelpers.c @@ -0,0 +1,160 @@ +#include "iohelpers.h" + +int +rmdir_recursive(const char *path) +{ + char *cmd = NULL; + int success = false; + + if (asprintf(&cmd, "rm -rf \"%s\"", path) < 0) + return false; + + if (system(cmd) == 0) + success = true; + + free(cmd); + + return success; +} + +int +fwriteall(const char *file_path, const char *string) +{ + int success = false; + + FILE *file = fopen(file_path, "w+"); + if (file == NULL) + goto cleanup; + + size_t size = strlen(string); + if (fwrite(string, 1, size, file) < size) { + goto cleanup; + } + + success = true; + +cleanup: + if (file) + fclose(file); + + return success; +} + +int +freadall(const char *file_path, char *output, size_t max_len) +{ + int rc = false; + FILE *file = fopen(file_path, "rb"); + if (file == NULL) { + printf("Failed to open file '%s'\n", file_path); + goto cleanup; + } + + size_t len = fread(output, 1, max_len - 1, file); + output[len] = '\0'; + + if (ferror(file) != 0) { + printf("Failed to read file '%s' (Error %d)\n", file_path, ferror(file)); + goto cleanup; + } + + if (!feof(file)) { + printf("File '%s' was bigger than allocated buffer %lu", file_path, max_len); + goto cleanup; + } + + rc = true; + +cleanup: + if (file) + fclose(file); + + return rc; +} + +int +vsnprintf_append(char *output, size_t max_output_len, const char *fmt, va_list args) +{ + va_list args2; + va_copy(args2, args); + + size_t output_len = strlen(output); + int rc = vsnprintf(output + output_len, max_output_len - output_len, fmt, args2); + + va_end(args2); + return rc; +} + +int +snprintf_append(char *output, size_t max_output_len, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + int rc = vsnprintf_append(output, max_output_len, fmt, args); + va_end(args); + return rc; +} + +int +str_array_count(char **str_array) +{ + int result = 0; + for (; str_array[result] != NULL; ++result) {} + return result; +} + +void +str_array_snprint(char *out_str, size_t max_len, char **str_array, int array_len) +{ + if (array_len < 0) + array_len = str_array_count(str_array); + + for (int pos = 0; pos < array_len; ++pos) { + snprintf_append(out_str, max_len, "%s%s", pos > 0 ? ", " : "", str_array[pos]); + } +} + +char * +str_replaced(const char *source, size_t dest_len, const char *old, const char *new) +{ + char *result = calloc(1, dest_len); + char *pos = NULL; + size_t old_len = strlen(old); + size_t new_len = strlen(new); + size_t available_len = dest_len; + + while ((pos = strstr(source, old)) != NULL) { + size_t skipped_len = (size_t)(pos - source); + if (available_len <= skipped_len + 1) + goto fail; + + available_len -= skipped_len; + strncat(result, source, skipped_len); + + if (available_len <= new_len + 1) + goto fail; + + available_len -= new_len; + strcat(result, new); + + source = pos + old_len; + } + + if (available_len <= strlen(source) + 1) + goto fail; + strcat(result, source); + + 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); + 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 000000000..65f7833f3 --- /dev/null +++ b/plugins/python/regress/iohelpers.h @@ -0,0 +1,36 @@ +#ifndef PYTHON_IO_HELPERS +#define PYTHON_IO_HELPERS + +#include "config.h" +#include "sudo_compat.h" + +#include +#include +#include +#include +#include +#include + +#include + +#define MAX_OUTPUT (2 << 16) + +int rmdir_recursive(const char *path); + +int fwriteall(const char *file_path, const char *string); +int freadall(const char *file_path, char *output, size_t max_len); + +// allocates new string with the content of 'string' but 'old' replaced to 'new' +// The allocated array will be dest_length size and null terminated correctly. +char *str_replaced(const char *string, size_t dest_length, const char *old, const char *new); + +// same, but "string" must be able to store 'max_length' number of characters including the null terminator +void str_replace_in_place(char *string, size_t max_length, const char *old, const char *new); + +int vsnprintf_append(char *output, size_t max_output_len, const char *fmt, va_list args); +int snprintf_append(char *output, size_t max_output_len, const char *fmt, ...); + +int str_array_count(char **str_array); +void str_array_snprint(char *out_str, size_t max_len, char **str_array, int array_len); + +#endif diff --git a/plugins/python/regress/testdata/sudo.conf.developer_mode b/plugins/python/regress/testdata/sudo.conf.developer_mode new file mode 100644 index 000000000..4da2ad94e --- /dev/null +++ b/plugins/python/regress/testdata/sudo.conf.developer_mode @@ -0,0 +1 @@ +Set developer_mode true diff --git a/plugins/python/regress/testdata/sudo.conf.normal_mode b/plugins/python/regress/testdata/sudo.conf.normal_mode new file mode 100644 index 000000000..b972a6a0b --- /dev/null +++ b/plugins/python/regress/testdata/sudo.conf.normal_mode @@ -0,0 +1 @@ +Set developer_mode false diff --git a/plugins/python/regress/testhelpers.c b/plugins/python/regress/testhelpers.c new file mode 100644 index 000000000..66925e3c0 --- /dev/null +++ b/plugins/python/regress/testhelpers.c @@ -0,0 +1,236 @@ +#include "testhelpers.h" + +const char *sudo_conf_developer_mode = TESTDATA_DIR "sudo.conf.developer_mode"; +const char *sudo_conf_normal_mode = TESTDATA_DIR "sudo.conf.normal_mode"; + +struct passwd example_pwd = { + "pw_name", + "pw_passwd", + (uid_t)1001, + (gid_t)101, + "pw_gecos", + "pw_dir", + "pw_shell" +}; + +struct TestData data; + +static void +clean_output(char *output) +{ + // we replace some output which otherwise would be test run dependant + str_replace_in_place(output, MAX_OUTPUT, data.tmp_dir, TEMP_PATH_TEMPLATE); + str_replace_in_place(output, MAX_OUTPUT, SRC_DIR, "SRC_DIR"); +} + +const char * +expected_path(const char *format, ...) +{ + static char expected_output_file[PATH_MAX]; + int count = snprintf(expected_output_file, PATH_MAX, TESTDATA_DIR); + char *filename = expected_output_file + count; + + va_list args; + va_start(args, format); + vsprintf(filename, 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 *)); + for (size_t i = 0; i < count; ++i) { + result[i] = va_arg(args, char *); + } + + 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_file_name, const char *reference_path) +{ + char actual_path[PATH_MAX]; + snprintf(actual_path, sizeof(actual_path), "%s/%s", data.tmp_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]); + } + + return 0; // simulate user answered just fine +} + +int +fake_conversation_with_suspend(int num_msgs, const struct sudo_conv_message msgs[], + struct sudo_conv_reply replies[], struct sudo_conv_callback *callback) +{ + if (callback != NULL) { + callback->on_suspend(SIGTSTP, callback->closure); + callback->on_resume(SIGCONT, callback->closure); + } + + return fake_conversation(num_msgs, msgs, replies, callback); +} + +int +fake_printf(int msg_type, const char *fmt, ...) +{ + int rc = -1; + va_list args; + va_start(args, fmt); + + char *output = NULL; + switch(msg_type) { + case SUDO_CONV_INFO_MSG: + output = data.stdout_str; + break; + case SUDO_CONV_ERROR_MSG: + output = data.stderr_str; + break; + default: + break; + } + + if (output) + rc = vsnprintf_append(output, MAX_OUTPUT, fmt, args); + + va_end(args); + return rc; +} + +int +verify_log_lines(const char *reference_path) +{ + char stored_path[PATH_MAX]; + snprintf(stored_path, sizeof(stored_path), "%s/%s", data.tmp_dir, "debug.log"); + + FILE *file = fopen(stored_path, "rb"); + if (file == NULL) { + printf("Failed to open file '%s'\n", stored_path); + return false; + } + + char line[1024] = ""; + char stored_str[MAX_OUTPUT] = ""; + while(fgets(line, sizeof(line), file) != NULL) { + const 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) + sprintf(line_end, " object>\n"); + + VERIFY_TRUE(strlen(stored_str) + strlen(line_data) + 1 < sizeof(stored_str)); // we have enough space in buffer + strcat(stored_str, line_data); + } + + 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; +} diff --git a/plugins/python/regress/testhelpers.h b/plugins/python/regress/testhelpers.h new file mode 100644 index 000000000..028f29422 --- /dev/null +++ b/plugins/python/regress/testhelpers.h @@ -0,0 +1,155 @@ +#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/" + +extern const char *sudo_conf_developer_mode; +extern const char *sudo_conf_normal_mode; + +extern struct passwd example_pwd; + +#define TEMP_PATH_TEMPLATE "/tmp/sudo_check_python_exampleXXXXXX" + +extern struct TestData { + char *tmp_dir; + 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 { \ + printf("Running test " #testcase " ... \n"); \ + int rc = EXIT_SUCCESS; \ + if (!init()) { \ + printf("FAILED: initialization of testcase %s at %s:%d\n", #testcase, __FILE__, __LINE__); \ + rc = EXIT_FAILURE; \ + } else \ + if (!testcase) { \ + printf("FAILED: testcase %s at %s:%d\n", #testcase, __FILE__, __LINE__); \ + rc = EXIT_FAILURE; \ + } \ + if (!cleanup(rc == EXIT_SUCCESS)) { \ + printf("FAILED: deitialization of testcase %s at %s:%d\n", #testcase, __FILE__, __LINE__); \ + rc = EXIT_FAILURE; \ + } \ + if (rc != EXIT_SUCCESS) \ + return rc; \ + } 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_file_name, const char *reference_path); + +#define VERIFY_FILE(actual_file_name, reference_path) \ + VERIFY_TRUE(verify_file(actual_file_name, reference_path)) + +int fake_conversation(int num_msgs, const struct sudo_conv_message msgs[], + struct sudo_conv_reply replies[], struct sudo_conv_callback *callback); + +int fake_conversation_with_suspend(int num_msgs, const struct sudo_conv_message msgs[], + struct sudo_conv_reply replies[], struct sudo_conv_callback *callback); + +int fake_printf(int msg_type, const char *fmt, ...); + +int verify_log_lines(const char *reference_path); + +#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)); \ + free(expected_set); \ + } while(false) + +#endif // PYTHON_TESTHELPERS