summaryrefslogtreecommitdiffstats
path: root/plugins/python
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--plugins/python/Makefile.in538
-rw-r--r--plugins/python/example_approval_plugin.py18
-rw-r--r--plugins/python/example_audit_plugin.py84
-rw-r--r--plugins/python/example_conversation.py98
-rw-r--r--plugins/python/example_debugging.py85
-rw-r--r--plugins/python/example_group_plugin.py45
-rw-r--r--plugins/python/example_io_plugin.py153
-rw-r--r--plugins/python/example_policy_plugin.py172
-rw-r--r--plugins/python/lsan_suppr.txt1
-rw-r--r--plugins/python/pyhelpers.c588
-rw-r--r--plugins/python/pyhelpers.h104
-rw-r--r--plugins/python/pyhelpers_cpychecker.h45
-rw-r--r--plugins/python/python_baseplugin.c87
-rw-r--r--plugins/python/python_convmessage.c154
-rw-r--r--plugins/python/python_loghandler.c190
-rw-r--r--plugins/python/python_plugin.exp8
-rw-r--r--plugins/python/python_plugin_approval.c196
-rw-r--r--plugins/python/python_plugin_approval_multi.inc57
-rw-r--r--plugins/python/python_plugin_audit.c281
-rw-r--r--plugins/python/python_plugin_audit_multi.inc78
-rw-r--r--plugins/python/python_plugin_common.c755
-rw-r--r--plugins/python/python_plugin_common.h85
-rw-r--r--plugins/python/python_plugin_group.c114
-rw-r--r--plugins/python/python_plugin_io.c276
-rw-r--r--plugins/python/python_plugin_io_multi.inc99
-rw-r--r--plugins/python/python_plugin_policy.c289
-rw-r--r--plugins/python/regress/check_python_examples.c1616
-rw-r--r--plugins/python/regress/iohelpers.c180
-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.c346
-rw-r--r--plugins/python/regress/testhelpers.h175
-rw-r--r--plugins/python/sudo_python_debug.c129
-rw-r--r--plugins/python/sudo_python_debug.h46
-rw-r--r--plugins/python/sudo_python_module.c612
-rw-r--r--plugins/python/sudo_python_module.h61
107 files changed, 8177 insertions, 0 deletions
diff --git a/plugins/python/Makefile.in b/plugins/python/Makefile.in
new file mode 100644
index 0000000..e79f570
--- /dev/null
+++ b/plugins/python/Makefile.in
@@ -0,0 +1,538 @@
+#
+# SPDX-License-Identifier: ISC
+#
+# Copyright (c) 2019-2020 Todd C. Miller <Todd.Miller@sudo.ws>
+#
+# 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.
+#
+# @configure_input@
+#
+
+#### Start of system configuration section. ####
+
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+
+srcdir = @srcdir@
+abs_srcdir = @abs_srcdir@
+top_builddir = @top_builddir@
+abs_top_builddir = @abs_top_builddir@
+top_srcdir = @top_srcdir@
+abs_top_srcdir = @abs_top_srcdir@
+devdir = @devdir@
+scriptdir = $(top_srcdir)/scripts
+incdir = $(top_srcdir)/include
+cross_compiling = @CROSS_COMPILING@
+
+# Compiler & tools to use
+CC = @CC@
+LIBTOOL = @LIBTOOL@
+EGREP = @EGREP@
+SED = @SED@
+AWK = @AWK@
+
+# Our install program supports extra flags...
+INSTALL = $(SHELL) $(scriptdir)/install-sh -c
+INSTALL_OWNER = -o $(install_uid) -g $(install_gid)
+INSTALL_BACKUP = @INSTALL_BACKUP@
+
+# Libraries
+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) -DPLUGIN_DIR=\"$(plugindir)\" -DSRC_DIR=\"$(abs_srcdir)\" @CPPFLAGS@ @PYTHON_INCLUDE@
+
+# Usually -O and/or -g
+CFLAGS = @CFLAGS@
+
+# Flags to pass to the link stage
+LDFLAGS = @LDFLAGS@ @PYTHON_LIBS@
+LT_LDFLAGS = @LT_LDFLAGS@ @LT_LDEXPORTS@
+
+# Flags to pass to libtool
+LTFLAGS = --tag=disable-static
+
+# Address sanitizer flags
+ASAN_CFLAGS = @ASAN_CFLAGS@
+ASAN_LDFLAGS = @ASAN_LDFLAGS@
+
+# PIE flags
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+
+# Stack smashing protection flags
+HARDENING_CFLAGS = @HARDENING_CFLAGS@
+HARDENING_LDFLAGS = @HARDENING_LDFLAGS@
+
+# cppcheck options, usually set in the top-level Makefile
+CPPCHECK_OPTS = -q --enable=warning,performance,portability --suppress=constStatement --suppress=compareBoolExpressionWithInt --error-exitcode=1 --inline-suppr -Dva_copy=va_copy -U__cplusplus -UQUAD_MAX -UQUAD_MIN -UUQUAD_MAX -U_POSIX_HOST_NAME_MAX -U_POSIX_PATH_MAX -U__NBBY -DNSIG=64
+
+# splint options, usually set in the top-level Makefile
+SPLINT_OPTS = -D__restrict= -checks
+
+# PVS-studio options
+PVS_CFG = $(top_srcdir)/PVS-Studio.cfg
+PVS_IGNORE = 'V707,V011,V002,V536'
+PVS_LOG_OPTS = -a 'GA:1,2' -e -t errorfile -d $(PVS_IGNORE)
+
+# Where to install things...
+prefix = @prefix@
+exec_prefix = @exec_prefix@
+bindir = @bindir@
+sbindir = @sbindir@
+sysconfdir = @sysconfdir@
+libexecdir = @libexecdir@
+datarootdir = @datarootdir@
+localstatedir = @localstatedir@
+plugindir = @plugindir@
+docdir = @docdir@
+exampledir = @exampledir@
+
+# File mode and map file to use for shared libraries/objects
+shlib_enable = @SHLIB_ENABLE@
+shlib_mode = @SHLIB_MODE@
+shlib_exp = $(srcdir)/python_plugin.exp
+shlib_map = python_plugin.map
+shlib_opt = python_plugin.opt
+
+# User and group ids the installed files should be "owned" by
+install_uid = 0
+install_gid = 0
+
+#### End of system configuration section. ####
+
+SHELL = @SHELL@
+
+EXAMPLES = example_approval_plugin.py example_audit_plugin.py \
+ example_conversation.py example_debugging.py \
+ example_group_plugin.py example_io_plugin.py example_policy_plugin.py
+
+OBJS = python_plugin_common.lo python_plugin_policy.lo python_plugin_io.lo \
+ python_plugin_group.lo pyhelpers.lo python_loghandler.lo \
+ python_convmessage.lo sudo_python_module.lo sudo_python_debug.lo \
+ python_baseplugin.lo python_plugin_audit.lo python_plugin_approval.lo
+
+IOBJS = $(OBJS:.lo=.i)
+
+POBJS = $(IOBJS:.i=.plog)
+
+LIBOBJDIR = $(top_builddir)/@ac_config_libobj_dir@/
+
+VERSION = @PACKAGE_VERSION@
+
+TEST_PROGS = check_python_examples
+TEST_VERBOSE =
+
+CHECK_PYTHON_EXAMPLES_OBJS = check_python_examples.o iohelpers.o testhelpers.o pyhelpers.o sudo_python_debug.o
+
+all: python_plugin.la
+
+depend:
+ $(scriptdir)/mkdep.pl --srcdir=$(abs_top_srcdir) \
+ --builddir=$(abs_top_builddir) plugins/python/Makefile.in
+ cd $(top_builddir) && ./config.status --file plugins/python/Makefile
+
+Makefile: $(srcdir)/Makefile.in
+ cd $(top_builddir) && ./config.status --file plugins/python/Makefile
+
+.SUFFIXES: .c .h .i .lo .plog .o
+
+.c.o:
+ $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $<
+
+.c.lo:
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $<
+
+.c.i:
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+
+.i.plog:
+ ifile=$<; rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $${ifile%i}c --i-file $< --output-file $@
+
+$(shlib_map): $(shlib_exp)
+ @$(AWK) 'BEGIN { print "{\n\tglobal:" } { print "\t\t"$$0";" } END { print "\tlocal:\n\t\t*;\n};" }' $(shlib_exp) > $@
+
+$(shlib_opt): $(shlib_exp)
+ @$(SED) 's/^/+e /' $(shlib_exp) > $@
+
+python_plugin.la: $(OBJS) $(LT_LIBS) @LT_LDDEP@
+ $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) $(LDFLAGS) $(ASAN_LDFLAGS) $(HARDENING_LDFLAGS) $(LT_LDFLAGS) -o $@ $(OBJS) $(LIBS) -module -avoid-version -rpath $(plugindir) -shrext .so
+
+pre-install:
+
+install: install-plugin install-doc
+
+install-dirs:
+ $(SHELL) $(scriptdir)/mkinstalldirs $(DESTDIR)$(plugindir) $(DESTDIR)$(exampledir)
+
+install-binaries:
+
+install-includes:
+
+install-doc: install-dirs
+ for f in $(EXAMPLES); do $(INSTALL) $(INSTALL_OWNER) -m 0644 $(srcdir)/$$f $(DESTDIR)$(exampledir); done
+
+install-plugin: install-dirs python_plugin.la
+ if [ X"$(shlib_enable)" = X"yes" ]; then \
+ INSTALL_BACKUP='$(INSTALL_BACKUP)' $(LIBTOOL) $(LTFLAGS) --mode=install $(INSTALL) $(INSTALL_OWNER) -m $(shlib_mode) python_plugin.la $(DESTDIR)$(plugindir); \
+ fi
+
+install-fuzzer:
+
+uninstall:
+ -$(LIBTOOL) $(LTFLAGS) --mode=uninstall rm -f $(DESTDIR)$(plugindir)/python_plugin.la
+ -test -z "$(INSTALL_BACKUP)" || \
+ rm -f $(DESTDIR)$(plugindir)/python_plugin.so$(INSTALL_BACKUP)
+
+splint:
+ splint $(SPLINT_OPTS) -I$(incdir) -I$(top_builddir) -I$(top_srcdir) $(srcdir)/*.c
+
+cppcheck:
+ cppcheck $(CPPCHECK_OPTS) -I$(incdir) -I$(top_builddir) -I$(top_srcdir) $(srcdir)/*.c
+
+pvs-log-files: $(POBJS)
+
+pvs-studio: $(POBJS)
+ plog-converter $(PVS_LOG_OPTS) $(POBJS)
+
+clean:
+ -$(LIBTOOL) $(LTFLAGS) --mode=clean rm -f *.lo *.o *.la
+ -rm -f *.i *.plog stamp-* core *.core core.* $(TEST_PROGS)
+
+mostlyclean: clean
+
+distclean: clean
+ -rm -rf Makefile .libs $(shlib_map) $(shlib_opt)
+
+clobber: distclean
+
+realclean: distclean
+ rm -f TAGS tags
+
+cleandir: realclean
+
+fuzz:
+
+check-fuzzer:
+
+check: $(TEST_PROGS) check-fuzzer
+ @if test X"$(cross_compiling)" != X"yes"; then \
+ l=`locale -a 2>&1 | $(EGREP) -i '^C\.UTF-?8$$' | $(SED) 1q` || true; \
+ test -n "$$l" || l="C"; \
+ LC_ALL="$$l"; export LC_ALL; \
+ unset LANG || LANG=; \
+ unset LANGUAGE || LANGUAGE=; \
+ MALLOC_OPTIONS=S; export MALLOC_OPTIONS; \
+ MALLOC_CONF="abort:true,junk:true"; export MALLOC_CONF; \
+ LSAN_OPTIONS=suppressions=$(srcdir)/lsan_suppr.txt \
+ ./check_python_examples ".libs/python_plugin.so"; \
+ fi
+
+check-verbose:
+ exec $(MAKE) $(MFLAGS) TEST_VERBOSE=-v FUZZ_VERBOSE=-verbosity=1 check
+
+update_test_data: $(TEST_PROGS)
+ @if test X"$(cross_compiling)" != X"yes"; then \
+ UPDATE_TESTDATA=1 ./check_python_examples ".libs/python_plugin.so"; \
+ fi
+
+check_python_examples: $(CHECK_PYTHON_EXAMPLES_OBJS) $(LIBPYTHONPLUGIN)
+ $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_PYTHON_EXAMPLES_OBJS) $(LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(HARDENING_LDFLAGS) $(LIBS)
+
+.PHONY: clean mostlyclean distclean cleandir clobber realclean
+
+# Autogenerated dependencies, do not modify
+check_python_examples.o: $(srcdir)/regress/check_python_examples.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_dso.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(srcdir)/pyhelpers.h \
+ $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/regress/iohelpers.h \
+ $(srcdir)/regress/testhelpers.h \
+ $(srcdir)/sudo_python_debug.h $(top_builddir)/config.h
+ $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/regress/check_python_examples.c
+check_python_examples.i: $(srcdir)/regress/check_python_examples.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_dso.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(srcdir)/pyhelpers.h \
+ $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/regress/iohelpers.h \
+ $(srcdir)/regress/testhelpers.h \
+ $(srcdir)/sudo_python_debug.h $(top_builddir)/config.h
+ $(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 $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(srcdir)/regress/iohelpers.h \
+ $(top_builddir)/config.h
+ $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/regress/iohelpers.c
+iohelpers.i: $(srcdir)/regress/iohelpers.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(srcdir)/regress/iohelpers.h \
+ $(top_builddir)/config.h
+ $(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 $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+ $(srcdir)/pyhelpers.h $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/sudo_python_debug.h $(top_builddir)/config.h \
+ $(top_builddir)/pathnames.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/pyhelpers.c
+pyhelpers.i: $(srcdir)/pyhelpers.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+ $(srcdir)/pyhelpers.h $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/sudo_python_debug.h $(top_builddir)/config.h \
+ $(top_builddir)/pathnames.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+pyhelpers.plog: pyhelpers.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/pyhelpers.c --i-file $< --output-file $@
+pyhelpers.o: $(srcdir)/pyhelpers.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+ $(srcdir)/pyhelpers.h $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/sudo_python_debug.h $(top_builddir)/config.h \
+ $(top_builddir)/pathnames.h
+ $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/pyhelpers.c
+python_baseplugin.lo: $(srcdir)/python_baseplugin.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+ $(srcdir)/pyhelpers.h $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/sudo_python_debug.h \
+ $(srcdir)/sudo_python_module.h $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/python_baseplugin.c
+python_baseplugin.i: $(srcdir)/python_baseplugin.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+ $(srcdir)/pyhelpers.h $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/sudo_python_debug.h \
+ $(srcdir)/sudo_python_module.h $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+python_baseplugin.plog: python_baseplugin.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/python_baseplugin.c --i-file $< --output-file $@
+python_convmessage.lo: $(srcdir)/python_convmessage.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(srcdir)/pyhelpers.h \
+ $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/sudo_python_debug.h \
+ $(srcdir)/sudo_python_module.h $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/python_convmessage.c
+python_convmessage.i: $(srcdir)/python_convmessage.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(srcdir)/pyhelpers.h \
+ $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/sudo_python_debug.h \
+ $(srcdir)/sudo_python_module.h $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+python_convmessage.plog: python_convmessage.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/python_convmessage.c --i-file $< --output-file $@
+python_loghandler.lo: $(srcdir)/python_loghandler.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+ $(srcdir)/pyhelpers.h $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/sudo_python_debug.h \
+ $(srcdir)/sudo_python_module.h $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/python_loghandler.c
+python_loghandler.i: $(srcdir)/python_loghandler.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+ $(srcdir)/pyhelpers.h $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/sudo_python_debug.h \
+ $(srcdir)/sudo_python_module.h $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+python_loghandler.plog: python_loghandler.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/python_loghandler.c --i-file $< --output-file $@
+python_plugin_approval.lo: $(srcdir)/python_plugin_approval.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(srcdir)/pyhelpers.h \
+ $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/python_plugin_approval_multi.inc \
+ $(srcdir)/python_plugin_common.h \
+ $(srcdir)/sudo_python_debug.h \
+ $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/python_plugin_approval.c
+python_plugin_approval.i: $(srcdir)/python_plugin_approval.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(srcdir)/pyhelpers.h \
+ $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/python_plugin_approval_multi.inc \
+ $(srcdir)/python_plugin_common.h \
+ $(srcdir)/sudo_python_debug.h \
+ $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+python_plugin_approval.plog: python_plugin_approval.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/python_plugin_approval.c --i-file $< --output-file $@
+python_plugin_audit.lo: $(srcdir)/python_plugin_audit.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(srcdir)/pyhelpers.h \
+ $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/python_plugin_audit_multi.inc \
+ $(srcdir)/python_plugin_common.h \
+ $(srcdir)/sudo_python_debug.h $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/python_plugin_audit.c
+python_plugin_audit.i: $(srcdir)/python_plugin_audit.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(srcdir)/pyhelpers.h \
+ $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/python_plugin_audit_multi.inc \
+ $(srcdir)/python_plugin_common.h \
+ $(srcdir)/sudo_python_debug.h $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+python_plugin_audit.plog: python_plugin_audit.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/python_plugin_audit.c --i-file $< --output-file $@
+python_plugin_common.lo: $(srcdir)/python_plugin_common.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+ $(srcdir)/pyhelpers.h \
+ $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/python_plugin_common.h \
+ $(srcdir)/sudo_python_debug.h \
+ $(srcdir)/sudo_python_module.h $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/python_plugin_common.c
+python_plugin_common.i: $(srcdir)/python_plugin_common.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+ $(srcdir)/pyhelpers.h \
+ $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/python_plugin_common.h \
+ $(srcdir)/sudo_python_debug.h \
+ $(srcdir)/sudo_python_module.h $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+python_plugin_common.plog: python_plugin_common.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/python_plugin_common.c --i-file $< --output-file $@
+python_plugin_group.lo: $(srcdir)/python_plugin_group.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(srcdir)/pyhelpers.h \
+ $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/python_plugin_common.h \
+ $(srcdir)/sudo_python_debug.h $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/python_plugin_group.c
+python_plugin_group.i: $(srcdir)/python_plugin_group.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(srcdir)/pyhelpers.h \
+ $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/python_plugin_common.h \
+ $(srcdir)/sudo_python_debug.h $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+python_plugin_group.plog: python_plugin_group.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/python_plugin_group.c --i-file $< --output-file $@
+python_plugin_io.lo: $(srcdir)/python_plugin_io.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+ $(srcdir)/pyhelpers.h $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/python_plugin_common.h \
+ $(srcdir)/python_plugin_io_multi.inc \
+ $(srcdir)/sudo_python_debug.h $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/python_plugin_io.c
+python_plugin_io.i: $(srcdir)/python_plugin_io.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+ $(srcdir)/pyhelpers.h $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/python_plugin_common.h \
+ $(srcdir)/python_plugin_io_multi.inc \
+ $(srcdir)/sudo_python_debug.h $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+python_plugin_io.plog: python_plugin_io.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/python_plugin_io.c --i-file $< --output-file $@
+python_plugin_policy.lo: $(srcdir)/python_plugin_policy.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(srcdir)/pyhelpers.h \
+ $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/python_plugin_common.h \
+ $(srcdir)/sudo_python_debug.h $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/python_plugin_policy.c
+python_plugin_policy.i: $(srcdir)/python_plugin_policy.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(srcdir)/pyhelpers.h \
+ $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/python_plugin_common.h \
+ $(srcdir)/sudo_python_debug.h $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+python_plugin_policy.plog: python_plugin_policy.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/python_plugin_policy.c --i-file $< --output-file $@
+sudo_python_debug.lo: $(srcdir)/sudo_python_debug.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \
+ $(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(srcdir)/sudo_python_debug.h \
+ $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/sudo_python_debug.c
+sudo_python_debug.i: $(srcdir)/sudo_python_debug.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \
+ $(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(srcdir)/sudo_python_debug.h \
+ $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+sudo_python_debug.plog: sudo_python_debug.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/sudo_python_debug.c --i-file $< --output-file $@
+sudo_python_debug.o: $(srcdir)/sudo_python_debug.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \
+ $(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(srcdir)/sudo_python_debug.h \
+ $(top_builddir)/config.h
+ $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/sudo_python_debug.c
+sudo_python_module.lo: $(srcdir)/sudo_python_module.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(srcdir)/pyhelpers.h \
+ $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/sudo_python_debug.h \
+ $(srcdir)/sudo_python_module.h $(top_builddir)/config.h
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/sudo_python_module.c
+sudo_python_module.i: $(srcdir)/sudo_python_module.c \
+ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(srcdir)/pyhelpers.h \
+ $(srcdir)/pyhelpers_cpychecker.h \
+ $(srcdir)/sudo_python_debug.h \
+ $(srcdir)/sudo_python_module.h $(top_builddir)/config.h
+ $(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 $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(srcdir)/pyhelpers.h \
+ $(srcdir)/pyhelpers_cpychecker.h $(srcdir)/regress/iohelpers.h \
+ $(srcdir)/regress/testhelpers.h $(srcdir)/sudo_python_debug.h \
+ $(top_builddir)/config.h
+ $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/regress/testhelpers.c
+testhelpers.i: $(srcdir)/regress/testhelpers.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(srcdir)/pyhelpers.h \
+ $(srcdir)/pyhelpers_cpychecker.h $(srcdir)/regress/iohelpers.h \
+ $(srcdir)/regress/testhelpers.h $(srcdir)/sudo_python_debug.h \
+ $(top_builddir)/config.h
+ $(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/example_approval_plugin.py b/plugins/python/example_approval_plugin.py
new file mode 100644
index 0000000..56ac865
--- /dev/null
+++ b/plugins/python/example_approval_plugin.py
@@ -0,0 +1,18 @@
+import sudo
+
+from datetime import datetime
+
+
+class BusinessHoursApprovalPlugin(sudo.Plugin):
+ def check(self, command_info: tuple, run_argv: tuple,
+ run_env: tuple) -> int:
+ error_msg = ""
+ now = datetime.now()
+ if now.weekday() >= 5:
+ error_msg = "That is not allowed on the weekend!"
+ if now.hour < 8 or now.hour > 17:
+ error_msg = "That is not allowed outside the business hours!"
+
+ if error_msg:
+ sudo.log_info(error_msg)
+ raise sudo.PluginReject(error_msg)
diff --git a/plugins/python/example_audit_plugin.py b/plugins/python/example_audit_plugin.py
new file mode 100644
index 0000000..d582e28
--- /dev/null
+++ b/plugins/python/example_audit_plugin.py
@@ -0,0 +1,84 @@
+import sudo
+
+import os
+
+
+VERSION = 1.0
+
+
+class SudoAuditPlugin(sudo.Plugin):
+ def __init__(self, plugin_options, user_info, **kwargs):
+ # For loading multiple times, an optional "Id" can be specified
+ # as argument to identify the log lines
+ plugin_id = sudo.options_as_dict(plugin_options).get("Id", "")
+ self._log_line_prefix = "(AUDIT{}) ".format(plugin_id)
+
+ user_info_dict = sudo.options_as_dict(user_info)
+ user = user_info_dict.get("user", "???")
+ uid = user_info_dict.get("uid", "???")
+ self._log("-- Started by user {} ({}) -- ".format(user, uid))
+
+ def __del__(self):
+ self._log("-- Finished --")
+
+ def open(self, submit_optind: int, submit_argv: tuple) -> int:
+ # To cut out the sudo options, use "submit_optind":
+ program_args = submit_argv[submit_optind:]
+ if program_args:
+ self._log("Requested command: " + " ".join(program_args))
+
+ def accept(self, plugin_name, plugin_type,
+ command_info, run_argv, run_envp) -> int:
+ info = sudo.options_as_dict(command_info)
+ cmd = list(run_argv)
+ cmd[0] = info.get("command")
+ self._log("Accepted command: {}".format(" ".join(cmd)))
+ self._log(" By the plugin: {} (type={})".format(
+ plugin_name, self.__plugin_type_str(plugin_type)))
+
+ self._log(" Environment: " + " ".join(run_envp))
+
+ def reject(self, plugin_name, plugin_type, audit_msg, command_info) -> int:
+ self._log("Rejected by plugin {} (type={}): {}".format(
+ plugin_name, self.__plugin_type_str(plugin_type), audit_msg))
+
+ def error(self, plugin_name, plugin_type, audit_msg, command_info) -> int:
+ self._log("Plugin {} (type={}) got an error: {}".format(
+ plugin_name, self.__plugin_type_str(plugin_type), audit_msg))
+
+ def close(self, status_kind: int, status: int) -> None:
+ if status_kind == sudo.EXIT_REASON.NO_STATUS:
+ self._log("The command was not executed")
+
+ elif status_kind == sudo.EXIT_REASON.WAIT_STATUS:
+ if os.WIFEXITED(status):
+ self._log("Command returned with exit code "
+ "{}".format(os.WEXITSTATUS(status)))
+ elif os.WIFSIGNALED(status):
+ self._log("Command exited due to signal "
+ "{}".format(os.WTERMSIG(status)))
+ else:
+ raise sudo.PluginError("Failed to understand wait exit status")
+
+ elif status_kind == sudo.EXIT_REASON.EXEC_ERROR:
+ self._log("Sudo has failed to execute the command, "
+ "execve returned {}".format(status))
+
+ elif status_kind == sudo.EXIT_REASON.SUDO_ERROR:
+ self._log("Sudo has run into an error: {}".format(status))
+
+ else:
+ raise Exception("Command returned unknown status kind {}".format(
+ status_kind))
+
+ def show_version(self, is_verbose: bool) -> int:
+ version_str = " (version=1.0)" if is_verbose else ""
+ sudo.log_info("Python Example Audit Plugin" + version_str)
+
+ def _log(self, string):
+ # For the example, we just log to output (this could be a file)
+ sudo.log_info(self._log_line_prefix, string)
+
+ @staticmethod
+ def __plugin_type_str(plugin_type):
+ return sudo.PLUGIN_TYPE(plugin_type).name
diff --git a/plugins/python/example_conversation.py b/plugins/python/example_conversation.py
new file mode 100644
index 0000000..8e68d36
--- /dev/null
+++ b/plugins/python/example_conversation.py
@@ -0,0 +1,98 @@
+import sudo
+import signal
+from os import path
+
+
+class ReasonLoggerIOPlugin(sudo.Plugin):
+ """
+ An example sudo plugin demonstrating how to use the sudo conversation API.
+
+ From the python plugin, you can ask something from the user using the
+ "sudo.conv" function. It expects one or more "sudo.ConvMessage" instances
+ which specifies how the interaction has to look like.
+
+ sudo.ConvMessage has the following fields (see help(sudo.ConvMessage)):
+ msg_type: int Specifies the type of the conversation.
+ See sudo.CONV.* constants below.
+ timeout: int The maximum amount of time for the conversation
+ in seconds. After the timeout exceeds, the "sudo.conv"
+ function will raise sudo.ConversationInterrupted
+ exception.
+ msg: str The message to display for the user.
+
+ To specify the conversion type you can use the following constants:
+ sudo.CONV.PROMPT_ECHO_OFF
+ sudo.CONV.PROMPT_ECHO_ON
+ sudo.CONV.ERROR_MSG
+ sudo.CONV.INFO_MSG
+ sudo.CONV.PROMPT_MASK
+ sudo.CONV.PROMPT_ECHO_OK
+ sudo.CONV.PREFER_TTY
+ """
+
+ def open(self, argv, command_info):
+ try:
+ conv_timeout = 120 # in seconds
+ sudo.log_info("Please provide your reason "
+ "for executing {}".format(argv))
+
+ # We ask two questions, the second is not visible on screen,
+ # so the user can hide a hidden message in case of criminals are
+ # forcing him for running the command.
+ # You can either specify the arguments in strict order (timeout
+ # being optional), or use named arguments.
+ message1 = sudo.ConvMessage(sudo.CONV.PROMPT_ECHO_ON,
+ "Reason: ",
+ conv_timeout)
+ message2 = sudo.ConvMessage(msg="Secret reason: ",
+ timeout=conv_timeout,
+ msg_type=sudo.CONV.PROMPT_MASK)
+ reply1, reply2 = sudo.conv(message1, message2,
+ on_suspend=self.on_conversation_suspend,
+ on_resume=self.on_conversation_resume)
+
+ with open(self._log_file_path(), "a") as file:
+ print("Executed", ' '.join(argv), file=file)
+ print("Reason:", reply1, file=file)
+ print("Hidden reason:", reply2, file=file)
+
+ except sudo.ConversationInterrupted:
+ sudo.log_error("You did not answer in time")
+ return sudo.RC.REJECT
+
+ def on_conversation_suspend(self, signum):
+ # This is just an example of how to do something on conversation
+ # suspend. You can skip specifying 'on_suspend' argument if there
+ # is no need
+ sudo.log_info("conversation suspend: signal",
+ self._signal_name(signum))
+
+ def on_conversation_resume(self, signum):
+ # This is just an example of how to do something on conversation
+ # resume. You can skip specifying 'on_resume' argument if there
+ # is no need
+ sudo.log_info("conversation resume: signal was",
+ self._signal_name(signum))
+
+ # helper functions:
+ if hasattr(signal, "Signals"):
+ @classmethod
+ def _signal_name(cls, signum: int):
+ try:
+ return signal.Signals(signum).name
+ except Exception:
+ return "{}".format(signum)
+ else:
+ @classmethod
+ def _signal_name(cls, signum: int):
+ for n, v in sorted(signal.__dict__.items()):
+ if v != signum:
+ continue
+ if n.startswith("SIG") and not n.startswith("SIG_"):
+ return n
+ return "{}".format(signum)
+
+ def _log_file_path(self):
+ options_dict = sudo.options_as_dict(self.plugin_options)
+ log_path = options_dict.get("LogPath", "/tmp")
+ return path.join(log_path, "sudo_reasons.txt")
diff --git a/plugins/python/example_debugging.py b/plugins/python/example_debugging.py
new file mode 100644
index 0000000..01310c6
--- /dev/null
+++ b/plugins/python/example_debugging.py
@@ -0,0 +1,85 @@
+import sudo
+
+import logging
+
+
+class DebugDemoPlugin(sudo.Plugin):
+ """
+ An example sudo plugin demonstrating the debugging capabilities.
+
+ You can install it as an extra IO plugin for example by adding the
+ following line to sudo.conf:
+ Plugin python_io python_plugin.so \
+ ModulePath=<path>/example_debugging.py \
+ ClassName=DebugDemoPlugin
+
+ To see the plugin's debug output, use the following line in sudo.conf:
+ Debug python_plugin.so \
+ /var/log/sudo_python_debug plugin@trace,c_calls@trace
+ ^ ^-- the options for the logging
+ ^----- the output will be placed here
+
+ The options for the logging is in format of multiple "subsystem@level"
+ separated by commas (",").
+ The most interesting subsystems are:
+ plugin Shows each call of sudo.debug API in the log
+ - py_calls Logs whenever a C function calls into the python module.
+ (For example calling this __init__ function.)
+ c_calls Logs whenever python calls into a C sudo API function
+
+ You can also specify "all" as subsystem name to get the debug messages of
+ all subsystems.
+
+ Other subsystems available:
+ internal logs internal functions of the python language wrapper
+ sudo_cb logs when sudo calls into its plugin API
+ load logs python plugin loading / unloading
+
+ Log levels
+ crit sudo.DEBUG.CRIT --> only critical messages
+ err sudo.DEBUG.ERROR
+ warn sudo.DEBUG.WARN
+ notice sudo.DEBUG.NOTICE
+ diag sudo.DEBUG.DIAG
+ info sudo.DEBUG.INFO
+ trace sudo.DEBUG.TRACE
+ debug sudo.DEBUG.DEBUG --> very extreme verbose debugging
+
+ See the sudo.conf manual for more details ("man sudo.conf").
+
+ """
+
+ def __init__(self, plugin_options, **kwargs):
+ # Specify: "py_calls@info" debug option to show the call to this
+ # constructor and the arguments passed in
+
+ # Specifying "plugin@err" debug option will show this message
+ # (or any more verbose level)
+ sudo.debug(sudo.DEBUG.ERROR, "My demo purpose plugin shows "
+ "this ERROR level debug message")
+
+ # Specifying "plugin@info" debug option will show this message
+ # (or any more verbose level)
+ sudo.debug(sudo.DEBUG.INFO, "My demo purpose plugin shows "
+ "this INFO level debug message")
+
+ # You can also use python log system, because sudo sets its log handler
+ # on the root logger.
+ # Note that the level of python logging is separate than the one set in
+ # sudo.conf. If using the python logger, each will have effect.
+ logger = logging.getLogger()
+ logger.setLevel(logging.INFO)
+ logger.error("Python log system shows this ERROR level debug message")
+ logger.info("Python log system shows this INFO level debug message")
+
+ # If you raise the level to info or below, the call of the debug
+ # will also be logged.
+ # An example output you will see in the debug log file:
+ # Dec 5 15:19:19 sudo[123040] __init__ @ /.../example_debugging.py:54 debugs:
+ # Dec 5 15:19:19 sudo[123040] My demo purpose plugin shows this ERROR level debug message
+
+ # Specify: "c_calls@diag" debug option to show this call and its
+ # arguments. If you specify info debug level instead ("c_calls@info"),
+ # you will also see the python function and line from which you called
+ # the 'options_as_dict' function.
+ self.plugin_options = sudo.options_as_dict(plugin_options)
diff --git a/plugins/python/example_group_plugin.py b/plugins/python/example_group_plugin.py
new file mode 100644
index 0000000..bb53fdb
--- /dev/null
+++ b/plugins/python/example_group_plugin.py
@@ -0,0 +1,45 @@
+import sudo
+
+
+class SudoGroupPlugin(sudo.Plugin):
+ """Example sudo input/output plugin
+
+ Demonstrates how to use the sudo group plugin API. Typing annotations are
+ just here for the help on the syntax (requires python >= 3.5).
+
+ On detailed description of the functions refer to sudo_plugin manual (man
+ sudo_plugin).
+
+ Most functions can express error or reject through their "int" return value
+ as documented in the manual. The sudo module also has constants for these:
+ sudo.RC.ACCEPT / sudo.RC.OK 1
+ sudo.RC.REJECT 0
+ sudo.RC.ERROR -1
+ sudo.RC.USAGE_ERROR -2
+
+ If the plugin encounters an error, instead of just returning sudo.RC.ERROR
+ result code it can also add a message describing the problem.
+ This can be done by raising the special exception:
+ raise sudo.PluginError("Message")
+ This added message will be used by the audit plugins.
+
+ If the function returns "None" (for example does not call return), it will
+ be considered sudo.RC.OK. If an exception other than sudo.PluginError is
+ raised, its backtrace will be shown to the user and the plugin function
+ returns sudo.RC.ERROR. If that is not acceptable, catch it.
+ """
+
+ # -- Plugin API functions --
+ def query(self, user: str, group: str, user_pwd: tuple):
+ """Query if user is part of the specified group.
+
+ Beware that user_pwd can be None if user is not present in the password
+ database. Otherwise it is a tuple convertible to pwd.struct_passwd.
+ """
+ hardcoded_user_groups = {
+ "testgroup": ["testuser1", "testuser2"],
+ "mygroup": ["test"]
+ }
+
+ group_has_user = user in hardcoded_user_groups.get(group, [])
+ return sudo.RC.ACCEPT if group_has_user else sudo.RC.REJECT
diff --git a/plugins/python/example_io_plugin.py b/plugins/python/example_io_plugin.py
new file mode 100644
index 0000000..dc4c6a7
--- /dev/null
+++ b/plugins/python/example_io_plugin.py
@@ -0,0 +1,153 @@
+import sudo
+
+from os import path
+import errno
+import signal
+import sys
+import json
+
+
+VERSION = 1.0
+
+
+class SudoIOPlugin(sudo.Plugin):
+ """Example sudo input/output plugin
+
+ Demonstrates how to use the sudo IO plugin API. All functions are added as
+ an example on their syntax, but note that all of them are optional.
+
+ On detailed description of the functions refer to sudo_plugin manual (man
+ sudo_plugin).
+
+ Most functions can express error or reject through their "int" return value
+ as documented in the manual. The sudo module also has constants for these:
+ sudo.RC.ACCEPT / sudo.RC.OK 1
+ sudo.RC.REJECT 0
+ sudo.RC.ERROR -1
+ sudo.RC.USAGE_ERROR -2
+
+ If the plugin encounters an error, instead of just returning sudo.RC.ERROR
+ result code it can also add a message describing the problem.
+ This can be done by raising the special exception:
+ raise sudo.PluginError("Message")
+ This added message will be used by the audit plugins.
+
+ If the function returns "None" (for example does not call return), it will
+ be considered sudo.RC.OK. If an exception other than sudo.PluginError is
+ raised, its backtrace will be shown to the user and the plugin function
+ returns sudo.RC.ERROR. If that is not acceptable, catch it.
+ """
+
+ # -- Plugin API functions --
+
+ def __init__(self, version: str,
+ plugin_options: tuple, **kwargs):
+ """The constructor of the IO plugin.
+
+ Other variables you can currently use as arguments are:
+ user_env: tuple
+ settings: tuple
+ user_info: tuple
+
+ For their detailed description, see the open() call of the C plugin API
+ in the sudo manual ("man sudo").
+ """
+ if not version.startswith("1."):
+ raise sudo.SudoException(
+ "This plugin plugin is not compatible with python plugin"
+ "API version {}".format(version))
+
+ # convert tuple of "key=value"s to dict
+ plugin_options = sudo.options_as_dict(plugin_options)
+
+ log_path = plugin_options.get("LogPath", "/tmp")
+ self._open_log_file(path.join(log_path, "sudo.log"))
+ self._log("", "-- Plugin STARTED --")
+
+ def __del__(self):
+ if hasattr(self, "_log_file"):
+ self._log("", "-- Plugin DESTROYED --")
+ self._log_file.close()
+
+ def open(self, argv: tuple,
+ command_info: tuple) -> int:
+ """Receives the command the user wishes to run.
+
+ This function works the same as open() call of the C IO plugin API (see
+ sudo manual), except that:
+ - It only gets called before the user would execute some command (and
+ not for a version query for example).
+ - Other arguments of the C open() call are received through the
+ constructor.
+ """
+ self._log("EXEC", " ".join(argv))
+ self._log("EXEC info", json.dumps(command_info, indent=4))
+
+ return sudo.RC.ACCEPT
+
+ def log_ttyout(self, buf: str) -> int:
+ return self._log("TTY OUT", buf.strip())
+
+ def log_ttyin(self, buf: str) -> int:
+ return self._log("TTY IN", buf.strip())
+
+ def log_stdin(self, buf: str) -> int:
+ return self._log("STD IN", buf.strip())
+
+ def log_stdout(self, buf: str) -> int:
+ return self._log("STD OUT", buf.strip())
+
+ def log_stderr(self, buf: str) -> int:
+ return self._log("STD ERR", buf.strip())
+
+ def change_winsize(self, line: int, cols: int) -> int:
+ self._log("WINSIZE", "{}x{}".format(line, cols))
+
+ def log_suspend(self, signo: int) -> int:
+ signal_description = self._signal_name(signo)
+
+ self._log("SUSPEND", signal_description)
+
+ def show_version(self, is_verbose: int) -> int:
+ sudo.log_info("Python Example IO Plugin version: {}".format(VERSION))
+ if is_verbose:
+ sudo.log_info("Python interpreter version:", sys.version)
+
+ def close(self, exit_status: int, error: int) -> None:
+ """Called when a command execution finished.
+
+ Works the same as close() from C API (see sudo_plugin manual), except
+ that it only gets called if there was a command execution trial (open()
+ returned with sudo.RC.ACCEPT).
+ """
+ if error == 0:
+ self._log("CLOSE", "Command returned {}".format(exit_status))
+ else:
+ error_name = errno.errorcode.get(error, "???")
+ self._log("CLOSE", "Failed to execute, execve returned {} ({})"
+ .format(error, error_name))
+
+ # -- Helper functions --
+
+ def _open_log_file(self, log_path):
+ sudo.log_info("Example sudo python plugin will log to", log_path)
+ self._log_file = open(log_path, "a")
+
+ def _log(self, type, message):
+ print(type, message, file=self._log_file)
+ return sudo.RC.ACCEPT
+
+ if hasattr(signal, "Signals"):
+ def _signal_name(cls, signo: int):
+ try:
+ return signal.Signals(signo).name
+ except ValueError:
+ return "signal {}".format(signo)
+ else:
+ def _signal_name(cls, signo: int):
+ for n, v in sorted(signal.__dict__.items()):
+ if v != signo:
+ continue;
+ if n.startswith("SIG") and not n.startswith("SIG_"):
+ return n
+ return "signal {}".format(signo)
diff --git a/plugins/python/example_policy_plugin.py b/plugins/python/example_policy_plugin.py
new file mode 100644
index 0000000..dfb15ca
--- /dev/null
+++ b/plugins/python/example_policy_plugin.py
@@ -0,0 +1,172 @@
+import sudo
+
+import errno
+import sys
+import os
+import pwd
+import grp
+import shutil
+
+
+VERSION = 1.0
+
+
+class SudoPolicyPlugin(sudo.Plugin):
+ """Example sudo policy plugin
+
+ Demonstrates how to use the sudo policy plugin API. All functions are added
+ as an example on their syntax, but note that most of them are optional
+ (except check_policy).
+
+ On detailed description of the functions refer to sudo_plugin manual (man
+ sudo_plugin).
+
+ Most functions can express error or reject through their "int" return value
+ as documented in the manual. The sudo module also has constants for these:
+ sudo.RC.ACCEPT / sudo.RC.OK 1
+ sudo.RC.REJECT 0
+ sudo.RC.ERROR -1
+ sudo.RC.USAGE_ERROR -2
+
+ If the plugin encounters an error, instead of just returning sudo.RC.ERROR
+ result code it can also add a message describing the problem.
+ This can be done by raising the special exception:
+ raise sudo.PluginError("Message")
+ This added message will be used by the audit plugins.
+
+ If the function returns "None" (for example does not call return), it will
+ be considered sudo.RC.OK. If an exception other than sudo.PluginError is
+ raised, its backtrace will be shown to the user and the plugin function
+ returns sudo.RC.ERROR. If that is not acceptable, catch it.
+ """
+
+ _allowed_commands = ("id", "whoami")
+ _safe_password = "12345"
+
+ # -- Plugin API functions --
+
+ def __init__(self, user_env: tuple, settings: tuple,
+ version: str, **kwargs):
+ """The constructor matches the C sudo plugin API open() call
+
+ Other variables you can currently use as arguments are:
+ user_info: tuple
+ plugin_options: tuple
+
+ For their detailed description, see the open() call of the C plugin API
+ in the sudo manual ("man sudo").
+ """
+ if not version.startswith("1."):
+ raise sudo.PluginError(
+ "This plugin plugin is not compatible with python plugin"
+ "API version {}".format(version))
+
+ self.user_env = sudo.options_as_dict(user_env)
+ self.settings = sudo.options_as_dict(settings)
+
+ def check_policy(self, argv: tuple, env_add: tuple):
+ cmd = argv[0]
+ # Example for a simple reject:
+ if not self._is_command_allowed(cmd):
+ sudo.log_error("You are not allowed to run this command!")
+ return sudo.RC.REJECT
+
+ raise sudo.PluginError("You are not allowed to run this command!")
+
+ # The environment the command will be executed with (we allow any here)
+ user_env_out = sudo.options_from_dict(self.user_env) + env_add
+
+ command_info_out = sudo.options_from_dict({
+ "command": self._find_on_path(cmd), # Absolute path of command
+ "runas_uid": self._runas_uid(), # The user id
+ "runas_gid": self._runas_gid(), # The group id
+ })
+
+ return (sudo.RC.ACCEPT, command_info_out, argv, user_env_out)
+
+ def init_session(self, user_pwd: tuple, user_env: tuple):
+ """Perform session setup
+
+ Beware that user_pwd can be None if user is not present in the password
+ database. Otherwise it is a tuple convertible to pwd.struct_passwd.
+ """
+ # conversion example:
+ user_pwd = pwd.struct_passwd(user_pwd) if user_pwd else None
+
+ # This is how you change the user_env:
+ return (sudo.RC.OK, user_env + ("PLUGIN_EXAMPLE_ENV=1",))
+
+ # If you do not want to change user_env, you can just return (or None):
+ # return sudo.RC.OK
+
+ def list(self, argv: tuple, is_verbose: int, user: str):
+ cmd = argv[0] if argv else None
+ as_user_text = "as user '{}'".format(user) if user else ""
+
+ if cmd:
+ allowed_text = "" if self._is_command_allowed(cmd) else "NOT "
+ sudo.log_info("You are {}allowed to execute command '{}'{}"
+ .format(allowed_text, cmd, as_user_text))
+
+ if not cmd or is_verbose:
+ sudo.log_info("Only the following commands are allowed:",
+ ", ".join(self._allowed_commands), as_user_text)
+
+ def validate(self):
+ pass # we have no cache
+
+ def invalidate(self, remove: int):
+ pass # we have no cache
+
+ def show_version(self, is_verbose: int):
+ sudo.log_info("Python Example Policy Plugin "
+ "version: {}".format(VERSION))
+ if is_verbose:
+ sudo.log_info("Python interpreter version:", sys.version)
+
+ def close(self, exit_status: int, error: int) -> None:
+ if error == 0:
+ sudo.log_info("The command returned with exit_status {}".format(
+ exit_status))
+ else:
+ error_name = errno.errorcode.get(error, "???")
+ sudo.log_error(
+ "Failed to execute command, execve syscall returned "
+ "{} ({})".format(error, error_name))
+
+ # -- Helper functions --
+
+ def _is_command_allowed(self, cmd):
+ return os.path.basename(cmd) in self._allowed_commands
+
+ def _find_on_path(self, cmd):
+ if os.path.isabs(cmd):
+ return cmd
+
+ path = self.user_env.get("PATH", "/usr/bin:/bin")
+ absolute_cmd = shutil.which(cmd, path=path)
+ if not absolute_cmd:
+ raise sudo.PluginError("Can not find cmd '{}' on PATH".format(cmd))
+ return absolute_cmd
+
+ def _runas_pwd(self):
+ runas_user = self.settings.get("runas_user") or "root"
+ try:
+ return pwd.getpwnam(runas_user)
+ except KeyError:
+ raise sudo.PluginError("Could not find user "
+ "'{}'".format(runas_user))
+
+ def _runas_uid(self):
+ return self._runas_pwd().pw_uid
+
+ def _runas_gid(self):
+ runas_group = self.settings.get("runas_group")
+ if runas_group is None:
+ return self._runas_pwd().pw_gid
+
+ try:
+ return grp.getgrnam(runas_group).gr_gid
+ except KeyError:
+ raise sudo.PluginError(
+ "Could not find group '{}'".format(runas_group))
diff --git a/plugins/python/lsan_suppr.txt b/plugins/python/lsan_suppr.txt
new file mode 100644
index 0000000..094ffd5
--- /dev/null
+++ b/plugins/python/lsan_suppr.txt
@@ -0,0 +1 @@
+leak:libpython
diff --git a/plugins/python/pyhelpers.c b/plugins/python/pyhelpers.c
new file mode 100644
index 0000000..882b31a
--- /dev/null
+++ b/plugins/python/pyhelpers.c
@@ -0,0 +1,588 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 Robert Manner <robert.manner@oneidentity.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * This is an open source non-commercial project. Dear PVS-Studio, please check it.
+ * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
+ */
+
+#include "pyhelpers.h"
+
+#include <pwd.h>
+#include <signal.h>
+#include "pathnames.h"
+
+static int
+_sudo_printf_default(int msg_type, const char *fmt, ...)
+{
+ FILE *fp = stdout;
+ FILE *ttyfp = NULL;
+ va_list ap;
+ int len;
+
+ if (ISSET(msg_type, SUDO_CONV_PREFER_TTY)) {
+ /* Try writing to /dev/tty first. */
+ ttyfp = fopen(_PATH_TTY, "w");
+ }
+
+ switch (msg_type & 0xff) {
+ case SUDO_CONV_ERROR_MSG:
+ fp = stderr;
+ FALLTHROUGH;
+ case SUDO_CONV_INFO_MSG:
+ va_start(ap, fmt);
+ len = vfprintf(ttyfp ? ttyfp : fp, fmt, ap);
+ va_end(ap);
+ break;
+ default:
+ len = -1;
+ errno = EINVAL;
+ break;
+ }
+
+ if (ttyfp != NULL)
+ fclose(ttyfp);
+
+ return len;
+}
+
+
+struct PythonContext py_ctx = {
+ .sudo_log = &_sudo_printf_default,
+};
+
+
+char *
+py_join_str_list(PyObject *py_str_list, const char *separator)
+{
+ debug_decl(py_join_str_list, PYTHON_DEBUG_INTERNAL);
+
+ char *result = NULL;
+ PyObject *py_separator = NULL;
+ PyObject *py_str = NULL;
+
+ py_separator = PyUnicode_FromString(separator);
+ if (py_separator == NULL)
+ goto cleanup;
+
+ py_str = PyObject_CallMethod(py_separator, "join", "(O)", py_str_list);
+ if (py_str == NULL) {
+ goto cleanup;
+ }
+
+ const char *str = PyUnicode_AsUTF8(py_str);
+ if (str != NULL) {
+ result = strdup(str);
+ }
+
+cleanup:
+ Py_XDECREF(py_str);
+ Py_XDECREF(py_separator);
+
+ debug_return_str(result);
+}
+
+static char *
+py_create_traceback_string(PyObject *py_traceback)
+{
+ debug_decl(py_create_traceback_string, PYTHON_DEBUG_INTERNAL);
+ if (py_traceback == NULL)
+ debug_return_str(strdup(""));
+
+ char* traceback = NULL;
+
+
+ PyObject *py_traceback_module = PyImport_ImportModule("traceback");
+ if (py_traceback_module == NULL) {
+ PyErr_Clear(); // do not care, we just won't show backtrace
+ } else {
+ PyObject *py_traceback_str_list = PyObject_CallMethod(py_traceback_module, "format_tb", "(O)", py_traceback);
+
+ if (py_traceback_str_list != NULL) {
+ traceback = py_join_str_list(py_traceback_str_list, "");
+ Py_DECREF(py_traceback_str_list);
+ }
+
+ Py_CLEAR(py_traceback_module);
+ }
+
+ debug_return_str(traceback ? traceback : strdup(""));
+}
+
+void
+py_log_last_error(const char *context_message)
+{
+ debug_decl(py_log_last_error, PYTHON_DEBUG_INTERNAL);
+ if (!PyErr_Occurred()) {
+ py_sudo_log(SUDO_CONV_ERROR_MSG, "%s\n", context_message);
+ debug_return;
+ }
+
+ PyObject *py_type = NULL, *py_message = NULL, *py_traceback = NULL;
+ PyErr_Fetch(&py_type, &py_message, &py_traceback);
+
+ char *message = py_message ? py_create_string_rep(py_message) : NULL;
+
+ py_sudo_log(SUDO_CONV_ERROR_MSG, "%s%s%s\n",
+ context_message ? context_message : "",
+ context_message && *context_message ? ": " : "",
+ message ? message : "(NULL)");
+ free(message);
+
+ if (py_traceback != NULL) {
+ char *traceback = py_create_traceback_string(py_traceback);
+ py_sudo_log(SUDO_CONV_INFO_MSG, "Traceback:\n%s\n", traceback);
+ free(traceback);
+ }
+
+ Py_XDECREF(py_type);
+ Py_XDECREF(py_message);
+ Py_XDECREF(py_traceback);
+ debug_return;
+}
+
+PyObject *
+py_str_array_to_tuple_with_count(Py_ssize_t count, char * const strings[])
+{
+ debug_decl(py_str_array_to_tuple_with_count, PYTHON_DEBUG_INTERNAL);
+
+ PyObject *py_argv = PyTuple_New(count);
+ if (py_argv == NULL)
+ debug_return_ptr(NULL);
+
+ for (int i = 0; i < count; ++i) {
+ PyObject *py_arg = PyUnicode_FromString(strings[i]);
+ if (py_arg == NULL || PyTuple_SetItem(py_argv, i, py_arg) != 0) {
+ Py_CLEAR(py_argv);
+ break;
+ }
+ }
+
+ debug_return_ptr(py_argv);
+}
+
+PyObject *
+py_str_array_to_tuple(char * const strings[])
+{
+ debug_decl(py_str_array_to_tuple, PYTHON_DEBUG_INTERNAL);
+
+ // find the item count ("strings" ends with NULL terminator):
+ Py_ssize_t count = 0;
+ if (strings != NULL) {
+ while (strings[count] != NULL)
+ ++count;
+ }
+
+ debug_return_ptr(py_str_array_to_tuple_with_count(count, strings));
+}
+
+char **
+py_str_array_from_tuple(PyObject *py_tuple)
+{
+ debug_decl(py_str_array_from_tuple, PYTHON_DEBUG_INTERNAL);
+
+ if (!PyTuple_Check(py_tuple)) {
+ PyErr_Format(PyExc_ValueError, "%s: value error, argument should be a tuple but it is '%s'",
+ __func__, Py_TYPENAME(py_tuple));
+ debug_return_ptr(NULL);
+ }
+
+ Py_ssize_t tuple_size = PyTuple_Size(py_tuple);
+
+ // we need an extra 0 at the end
+ char **result = calloc(tuple_size + 1, sizeof(char *));
+ if (result == NULL) {
+ debug_return_ptr(NULL);
+ }
+
+ for (int i = 0; i < tuple_size; ++i) {
+ PyObject *py_value = PyTuple_GetItem(py_tuple, i);
+ if (py_value == NULL) {
+ str_array_free(&result);
+ debug_return_ptr(NULL);
+ }
+
+ // Note that it can be an "int" or something else as well
+ char *value = py_create_string_rep(py_value);
+ if (value == NULL) {
+ // conversion error is already set
+ str_array_free(&result);
+ debug_return_ptr(NULL);
+ }
+ result[i] = value;
+ }
+
+ debug_return_ptr(result);
+}
+
+PyObject *
+py_tuple_get(PyObject *py_tuple, Py_ssize_t idx, PyTypeObject *expected_type)
+{
+ debug_decl(py_tuple_get, PYTHON_DEBUG_INTERNAL);
+
+ PyObject *py_item = PyTuple_GetItem(py_tuple, idx);
+ if (py_item == NULL) {
+ debug_return_ptr(NULL);
+ }
+
+ if (!PyObject_TypeCheck(py_item, expected_type)) {
+ PyErr_Format(PyExc_ValueError, "Value error: tuple element %d should "
+ "be a '%s' (but it is '%s')",
+ idx, expected_type->tp_name, Py_TYPENAME(py_item));
+ debug_return_ptr(NULL);
+ }
+
+ debug_return_ptr(py_item);
+}
+
+PyObject *
+py_create_version(unsigned int version)
+{
+ debug_decl(py_create_version, PYTHON_DEBUG_INTERNAL);
+ debug_return_ptr(PyUnicode_FromFormat("%d.%d", SUDO_API_VERSION_GET_MAJOR(version),
+ SUDO_API_VERSION_GET_MINOR(version)));
+}
+
+PyObject *
+py_from_passwd(const struct passwd *pwd)
+{
+ debug_decl(py_from_passwd, PYTHON_DEBUG_INTERNAL);
+
+ if (pwd == NULL) {
+ debug_return_ptr_pynone;
+ }
+
+ // Create a tuple similar and convertible to python "struct_passwd" of "pwd" module
+ debug_return_ptr(
+ Py_BuildValue("(zziizzz)", pwd->pw_name, pwd->pw_passwd,
+ pwd->pw_uid, pwd->pw_gid, pwd->pw_gecos,
+ pwd->pw_dir, pwd->pw_shell)
+ );
+}
+
+char *
+py_create_string_rep(PyObject *py_object)
+{
+ debug_decl(py_create_string_rep, PYTHON_DEBUG_INTERNAL);
+ char *result = NULL;
+
+ if (py_object == NULL)
+ debug_return_ptr(NULL);
+
+ PyObject *py_string = PyObject_Str(py_object);
+ if (py_string != NULL) {
+ const char *bytes = PyUnicode_AsUTF8(py_string);
+ if (bytes != NULL) {
+ /*
+ * Convert from old format w/ numeric value to new without it.
+ * Old: (<DEBUG.ERROR: 2>, 'ERROR level debug message')
+ * New: (DEBUG.ERROR, 'ERROR level debug message')
+ */
+ if (bytes[0] == '(' && bytes[1] == '<') {
+ const char *colon = strchr(bytes + 2, ':');
+ if (colon != NULL && colon[1] == ' ') {
+ const char *cp = colon + 2;
+ while (isdigit((unsigned char)*cp))
+ cp++;
+ if (cp[0] == '>' && (cp[1] == ',' || cp[1] == '\0')) {
+ bytes += 2;
+ if (asprintf(&result, "(%.*s%s", (int)(colon - bytes),
+ bytes, cp + 1) == -1) {
+ result = NULL;
+ goto done;
+ }
+ }
+ }
+ }
+ if (result == NULL)
+ result = strdup(bytes);
+ }
+ }
+
+done:
+ Py_XDECREF(py_string);
+ debug_return_ptr(result);
+}
+
+static void
+_py_debug_python_function(const char *class_name, const char *function_name, const char *message,
+ PyObject *py_args, PyObject *py_kwargs, int subsystem_id)
+{
+ debug_decl_vars(_py_debug_python_function, subsystem_id);
+
+ if (sudo_debug_needed(SUDO_DEBUG_DIAG)) {
+ char *args_str = NULL;
+ char *kwargs_str = NULL;
+ if (py_args != NULL) {
+ /* Sort by key for consistent output on Python < 3.6 */
+ PyObject *py_args_sorted = NULL;
+ if (PyDict_Check(py_args)) {
+ py_args_sorted = PyDict_Items(py_args);
+ if (py_args_sorted != NULL) {
+ if (PyList_Sort(py_args_sorted) == 0) {
+ py_args = py_args_sorted;
+ }
+ }
+ }
+ args_str = py_create_string_rep(py_args);
+ if (args_str != NULL && strncmp(args_str, "RC.", 3) == 0) {
+ /* Strip leading RC. to match python 3.10 behavior. */
+ memmove(args_str, args_str + 3, strlen(args_str + 3) + 1);
+ }
+ if (py_args_sorted != NULL)
+ Py_DECREF(py_args_sorted);
+ }
+ if (py_kwargs != NULL) {
+ /* Sort by key for consistent output on Python < 3.6 */
+ PyObject *py_kwargs_sorted = NULL;
+ if (PyDict_Check(py_kwargs)) {
+ py_kwargs_sorted = PyDict_Items(py_kwargs);
+ if (py_kwargs_sorted != NULL) {
+ if (PyList_Sort(py_kwargs_sorted) == 0) {
+ py_kwargs = py_kwargs_sorted;
+ }
+ }
+ }
+ kwargs_str = py_create_string_rep(py_kwargs);
+ if (py_kwargs_sorted != NULL)
+ Py_DECREF(py_kwargs_sorted);
+ }
+
+ sudo_debug_printf(SUDO_DEBUG_DIAG, "%s.%s %s: %s %s\n", class_name,
+ function_name, message, args_str ? args_str : "()",
+ kwargs_str ? kwargs_str : "");
+ free(args_str);
+ free(kwargs_str);
+ }
+}
+
+void
+py_debug_python_call(const char *class_name, const char *function_name,
+ PyObject *py_args, PyObject *py_kwargs, int subsystem_id)
+{
+ debug_decl_vars(py_debug_python_call, subsystem_id);
+
+ if (subsystem_id == PYTHON_DEBUG_C_CALLS && sudo_debug_needed(SUDO_DEBUG_INFO)) {
+ // at this level we also output the callee python script
+ char *callee_func_name = NULL, *callee_file_name = NULL;
+ long callee_line_number = -1;
+
+ if (py_get_current_execution_frame(&callee_file_name, &callee_line_number, &callee_func_name) == SUDO_RC_OK) {
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s @ %s:%ld calls C function:\n",
+ callee_func_name, callee_file_name, callee_line_number);
+ }
+
+ free(callee_func_name);
+ free(callee_file_name);
+ }
+
+ _py_debug_python_function(class_name, function_name, "was called with arguments",
+ py_args, py_kwargs, subsystem_id);
+}
+
+void
+py_debug_python_result(const char *class_name, const char *function_name,
+ PyObject *py_result, int subsystem_id)
+{
+ if (py_result == NULL) {
+ debug_decl_vars(py_debug_python_result, subsystem_id);
+ sudo_debug_printf(SUDO_CONV_ERROR_MSG, "%s.%s call failed\n",
+ class_name, function_name);
+ } else {
+ _py_debug_python_function(class_name, function_name, "returned result",
+ py_result, NULL, subsystem_id);
+ }
+}
+
+void
+str_array_free(char ***array)
+{
+ debug_decl(str_array_free, PYTHON_DEBUG_INTERNAL);
+
+ if (*array == NULL)
+ debug_return;
+
+ for (char **item_ptr = *array; *item_ptr != NULL; ++item_ptr)
+ free(*item_ptr);
+
+ free(*array);
+ *array = NULL;
+
+ debug_return;
+}
+
+int
+py_get_current_execution_frame(char **file_name, long *line_number, char **function_name)
+{
+ *file_name = NULL;
+ *line_number = (long)-1;
+ *function_name = NULL;
+
+ PyObject *py_err_type = NULL, *py_err_value = NULL, *py_err_traceback = NULL;
+ PyErr_Fetch(&py_err_type, &py_err_value, &py_err_traceback);
+
+ PyObject *py_frame = NULL, *py_f_code = NULL,
+ *py_filename = NULL, *py_function_name = NULL;
+
+ PyObject *py_getframe = PySys_GetObject("_getframe");
+ if (py_getframe == NULL)
+ goto cleanup;
+
+ py_frame = PyObject_CallFunction(py_getframe, "i", 0);
+ if (py_frame == NULL)
+ goto cleanup;
+
+ *line_number = py_object_get_optional_attr_number(py_frame, "f_lineno");
+
+ py_f_code = py_object_get_optional_attr(py_frame, "f_code", NULL);
+ if (py_f_code != NULL) {
+ py_filename = py_object_get_optional_attr(py_f_code, "co_filename", NULL);
+ if (py_filename != NULL)
+ *file_name = strdup(PyUnicode_AsUTF8(py_filename));
+
+ py_function_name = py_object_get_optional_attr(py_f_code, "co_name", NULL);
+ if (py_function_name != NULL)
+ *function_name = strdup(PyUnicode_AsUTF8(py_function_name));
+ }
+
+cleanup:
+ Py_CLEAR(py_frame);
+ Py_CLEAR(py_f_code);
+ Py_CLEAR(py_filename);
+ Py_CLEAR(py_function_name);
+
+ // we hide every error happening inside this function
+ PyErr_Restore(py_err_type, py_err_value, py_err_traceback);
+
+ return (*file_name && *function_name && (*line_number >= 0)) ?
+ SUDO_RC_OK : SUDO_RC_ERROR;
+}
+
+void
+py_ctx_reset()
+{
+ memset(&py_ctx, 0, sizeof(py_ctx));
+ py_ctx.sudo_log = &_sudo_printf_default;
+}
+
+int
+py_sudo_conv(int num_msgs, const struct sudo_conv_message msgs[],
+ struct sudo_conv_reply replies[], struct sudo_conv_callback *callback)
+{
+ /* Enable suspend during password entry. */
+ struct sigaction sa, saved_sigtstp;
+ sigemptyset(&sa.sa_mask);
+ sa.sa_flags = SA_RESTART;
+ sa.sa_handler = SIG_DFL;
+ (void) sigaction(SIGTSTP, &sa, &saved_sigtstp);
+
+ int rc = SUDO_RC_ERROR;
+ if (py_ctx.sudo_conv != NULL)
+ rc = py_ctx.sudo_conv((int)num_msgs, msgs, replies, callback);
+
+ /* Restore signal handlers and signal mask. */
+ (void) sigaction(SIGTSTP, &saved_sigtstp, NULL);
+
+ return rc;
+}
+
+PyObject *
+py_object_get_optional_attr(PyObject *py_object, const char *attr, PyObject *py_default)
+{
+ if (PyObject_HasAttrString(py_object, attr)) {
+ return PyObject_GetAttrString(py_object, attr);
+ }
+ Py_XINCREF(py_default); // whatever we return will have its refcount incremented
+ return py_default;
+}
+
+const char *
+py_object_get_optional_attr_string(PyObject *py_object, const char *attr_name)
+{
+ PyObject *py_value = py_object_get_optional_attr(py_object, attr_name, NULL);
+ if (py_value == NULL)
+ return NULL;
+
+ const char *value = PyUnicode_AsUTF8(py_value);
+ Py_CLEAR(py_value); // Note, the object still has reference to the attribute
+ return value;
+}
+
+long long
+py_object_get_optional_attr_number(PyObject *py_object, const char *attr_name)
+{
+ PyObject *py_value = py_object_get_optional_attr(py_object, attr_name, NULL);
+ if (py_value == NULL)
+ return -1;
+
+ long long value = PyLong_AsLongLong(py_value);
+ Py_CLEAR(py_value);
+ return value;
+}
+
+void
+py_object_set_attr_number(PyObject *py_object, const char *attr_name, long long number)
+{
+ PyObject *py_number = PyLong_FromLong(number);
+ if (py_number == NULL)
+ return;
+
+ PyObject_SetAttrString(py_object, attr_name, py_number);
+ Py_CLEAR(py_number);
+}
+
+void
+py_object_set_attr_string(PyObject *py_object, const char *attr_name, const char *value)
+{
+ PyObject *py_value = PyUnicode_FromString(value);
+ if (py_value == NULL)
+ return;
+
+ PyObject_SetAttrString(py_object, attr_name, py_value);
+ Py_CLEAR(py_value);
+}
+
+PyObject *
+py_dict_create_string_int(size_t count, struct key_value_str_int *key_values)
+{
+ debug_decl(py_dict_create_string_int, PYTHON_DEBUG_INTERNAL);
+
+ PyObject *py_value = NULL;
+ PyObject *py_dict = PyDict_New();
+ if (py_dict == NULL)
+ goto cleanup;
+
+ for (size_t i = 0; i < count; ++i) {
+ py_value = PyLong_FromLong(key_values[i].value);
+ if (py_value == NULL)
+ goto cleanup;
+
+ if (PyDict_SetItemString(py_dict, key_values[i].key, py_value) < 0)
+ goto cleanup;
+
+ Py_CLEAR(py_value);
+ }
+
+cleanup:
+ if (PyErr_Occurred()) {
+ Py_CLEAR(py_dict);
+ }
+ Py_CLEAR(py_value);
+
+ debug_return_ptr(py_dict);
+}
diff --git a/plugins/python/pyhelpers.h b/plugins/python/pyhelpers.h
new file mode 100644
index 0000000..5448e1d
--- /dev/null
+++ b/plugins/python/pyhelpers.h
@@ -0,0 +1,104 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 Robert Manner <robert.manner@oneidentity.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef SUDO_PLUGIN_PYHELPERS_H
+#define SUDO_PLUGIN_PYHELPERS_H
+
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+
+#include <config.h>
+#include "sudo_compat.h"
+#include "sudo_plugin.h"
+
+#include "pyhelpers_cpychecker.h"
+
+#include "sudo_python_debug.h"
+
+enum SudoPluginFunctionReturnCode {
+ SUDO_RC_OK = 1,
+ SUDO_RC_ACCEPT = 1,
+ SUDO_RC_REJECT = 0,
+ SUDO_RC_ERROR = -1,
+ SUDO_RC_USAGE_ERROR = -2,
+};
+
+#define INTERPRETER_MAX 32
+
+struct PythonContext
+{
+ sudo_printf_t sudo_log;
+ sudo_conv_t sudo_conv;
+ PyThreadState *py_main_interpreter;
+ size_t interpreter_count;
+ PyThreadState *py_subinterpreters[INTERPRETER_MAX];
+};
+
+extern struct PythonContext py_ctx;
+
+#define Py_TYPENAME(object) (object ? Py_TYPE(object)->tp_name : "NULL")
+
+#define py_sudo_log(...) py_ctx.sudo_log(__VA_ARGS__)
+
+int py_sudo_conv(int num_msgs, const struct sudo_conv_message msgs[],
+ struct sudo_conv_reply replies[], struct sudo_conv_callback *callback);
+
+void py_log_last_error(const char *context_message);
+
+char *py_create_string_rep(PyObject *py_object);
+
+char *py_join_str_list(PyObject *py_str_list, const char *separator);
+
+struct key_value_str_int
+{
+ const char *key;
+ int value;
+};
+
+PyObject *py_dict_create_string_int(size_t count, struct key_value_str_int *key_values);
+
+PyObject *py_from_passwd(const struct passwd *pwd);
+
+PyObject *py_str_array_to_tuple_with_count(Py_ssize_t count, char * const strings[]);
+PyObject *py_str_array_to_tuple(char * const strings[]);
+char **py_str_array_from_tuple(PyObject *py_tuple);
+
+CPYCHECKER_RETURNS_BORROWED_REF
+PyObject *py_tuple_get(PyObject *py_tuple, Py_ssize_t index, PyTypeObject *expected_type);
+
+PyObject *py_object_get_optional_attr(PyObject *py_object, const char *attr, PyObject *py_default);
+long long py_object_get_optional_attr_number(PyObject *py_object, const char *attr_name);
+const char *py_object_get_optional_attr_string(PyObject *py_object, const char *attr_name);
+
+void py_object_set_attr_number(PyObject *py_object, const char *attr_name, long long number);
+void py_object_set_attr_string(PyObject *py_object, const char *attr_name, const char *value);
+
+PyObject *py_create_version(unsigned int version);
+
+void py_debug_python_call(const char *class_name, const char *function_name,
+ PyObject *py_args, PyObject *py_kwargs, int subsystem_id);
+void py_debug_python_result(const char *class_name, const char *function_name,
+ PyObject *py_args, int subsystem_id);
+
+void str_array_free(char ***array);
+
+int py_get_current_execution_frame(char **file_name, long *line_number, char **function_name);
+
+void py_ctx_reset(void);
+
+#endif // SUDO_PLUGIN_PYHELPERS_H
diff --git a/plugins/python/pyhelpers_cpychecker.h b/plugins/python/pyhelpers_cpychecker.h
new file mode 100644
index 0000000..dc048c3
--- /dev/null
+++ b/plugins/python/pyhelpers_cpychecker.h
@@ -0,0 +1,45 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019 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 SUDO_PLUGIN_PYHELPERS_CPYCHECKER_H
+#define SUDO_PLUGIN_PYHELPERS_CPYCHECKER_H
+
+/* Helper macros for cpychecker */
+
+#if defined(WITH_CPYCHECKER_RETURNS_BORROWED_REF_ATTRIBUTE)
+ #define CPYCHECKER_RETURNS_BORROWED_REF \
+ __attribute__((cpychecker_returns_borrowed_ref))
+#else
+ #define CPYCHECKER_RETURNS_BORROWED_REF
+#endif
+
+#ifdef WITH_CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION_ATTRIBUTE
+ #define CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION \
+ __attribute__ ((cpychecker_negative_result_sets_exception))
+#else
+ #define CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
+#endif
+
+#if defined(WITH_CPYCHECKER_STEALS_REFERENCE_TO_ARG_ATTRIBUTE)
+ #define CPYCHECKER_STEALS_REFERENCE_TO_ARG(n) \
+ __attribute__((cpychecker_steals_reference_to_arg(n)))
+#else
+ #define CPYCHECKER_STEALS_REFERENCE_TO_ARG(n)
+#endif
+
+#endif // SUDO_PLUGIN_PYHELPERS_CPYCHECKER_H
diff --git a/plugins/python/python_baseplugin.c b/plugins/python/python_baseplugin.c
new file mode 100644
index 0000000..6060169
--- /dev/null
+++ b/plugins/python/python_baseplugin.c
@@ -0,0 +1,87 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 Robert Manner <robert.manner@oneidentity.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * This is an open source non-commercial project. Dear PVS-Studio, please check it.
+ * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
+ */
+
+#include "sudo_python_module.h"
+
+PyTypeObject *sudo_type_Plugin = NULL;
+
+static PyObject *
+_sudo_Plugin__Init(PyObject *py_self, PyObject *py_args, PyObject *py_kwargs)
+{
+ debug_decl(_sudo_Plugin__Init, PYTHON_DEBUG_C_CALLS);
+
+ py_debug_python_call("Plugin", "__init__", py_args, NULL, PYTHON_DEBUG_C_CALLS);
+
+ if (!PyArg_UnpackTuple(py_args, "sudo.Plugin.__init__", 1, 1, &py_self))
+ goto cleanup;
+
+ Py_ssize_t pos = 0;
+ PyObject *py_key = NULL, *py_value = NULL; // -> borrowed references
+
+ while (PyDict_Next(py_kwargs, &pos, &py_key, &py_value)) {
+ if (PyObject_SetAttr(py_self, py_key, py_value) != 0)
+ goto cleanup;
+ }
+
+cleanup:
+ if (PyErr_Occurred())
+ debug_return_ptr(NULL);
+
+ debug_return_ptr_pynone;
+}
+
+
+static PyMethodDef _sudo_Plugin_class_methods[] = {
+ {"__init__", (PyCFunction)_sudo_Plugin__Init,
+ METH_VARARGS | METH_KEYWORDS,
+ "Base sudo plugin constructor"},
+ {NULL, NULL, 0, NULL}
+};
+
+
+int
+sudo_module_register_baseplugin(PyObject *py_module)
+{
+ debug_decl(sudo_module_register_baseplugin, PYTHON_DEBUG_INTERNAL);
+ int rc = SUDO_RC_ERROR;
+ PyObject *py_class = NULL;
+
+ py_class = sudo_module_create_class("sudo.Plugin", _sudo_Plugin_class_methods, NULL);
+ if (py_class == NULL)
+ goto cleanup;
+
+ if (PyModule_AddObject(py_module, "Plugin", py_class) < 0) {
+ goto cleanup;
+ }
+
+ Py_INCREF(py_class);
+ rc = SUDO_RC_OK;
+
+ Py_CLEAR(sudo_type_Plugin);
+ sudo_type_Plugin = (PyTypeObject *)py_class;
+ Py_INCREF(sudo_type_Plugin);
+
+cleanup:
+ Py_CLEAR(py_class);
+ debug_return_int(rc);
+}
diff --git a/plugins/python/python_convmessage.c b/plugins/python/python_convmessage.c
new file mode 100644
index 0000000..7b2aa6d
--- /dev/null
+++ b/plugins/python/python_convmessage.c
@@ -0,0 +1,154 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 Robert Manner <robert.manner@oneidentity.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * This is an open source non-commercial project. Dear PVS-Studio, please check it.
+ * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
+ */
+
+#include "sudo_python_module.h"
+
+PyTypeObject *sudo_type_ConvMessage;
+
+static PyObject *
+_sudo_ConvMessage__Init(PyObject *py_self, PyObject *py_args, PyObject *py_kwargs)
+{
+ debug_decl(_sudo_ConvMessage__Init, PYTHON_DEBUG_C_CALLS);
+
+ py_debug_python_call("ConvMessage", "__init__", py_args, py_kwargs, PYTHON_DEBUG_C_CALLS);
+
+ PyObject *py_empty = PyTuple_New(0);
+
+ struct sudo_conv_message conv_message = { 0, 0, NULL };
+
+ static const char *keywords[] = { "self", "msg_type", "msg", "timeout", NULL };
+ if (!PyArg_ParseTupleAndKeywords(py_args ? py_args : py_empty, py_kwargs, "Ois|i:sudo.ConvMessage", (char **)keywords,
+ &py_self, &(conv_message.msg_type), &(conv_message.msg),
+ &(conv_message.timeout)))
+ goto cleanup;
+
+ sudo_debug_printf(SUDO_DEBUG_TRACE, "Parsed arguments: self='%p' msg_type='%d' timeout='%d' msg='%s'",
+ (void *)py_self, conv_message.msg_type, conv_message.timeout, conv_message.msg);
+
+ py_object_set_attr_number(py_self, "msg_type", conv_message.msg_type);
+ if (PyErr_Occurred())
+ goto cleanup;
+
+ py_object_set_attr_number(py_self, "timeout", conv_message.timeout);
+ if (PyErr_Occurred()) // -V547
+ goto cleanup;
+
+ py_object_set_attr_string(py_self, "msg", conv_message.msg);
+ if (PyErr_Occurred()) // -V547
+ goto cleanup;
+
+cleanup:
+ Py_CLEAR(py_empty);
+
+ if (PyErr_Occurred())
+ debug_return_ptr(NULL);
+
+ debug_return_ptr_pynone;
+}
+
+
+static PyMethodDef _sudo_ConvMessage_class_methods[] =
+{
+ {"__init__", (PyCFunction)_sudo_ConvMessage__Init,
+ METH_VARARGS | METH_KEYWORDS,
+ "Conversation message (same as C type sudo_conv_message)"},
+ {NULL, NULL, 0, NULL}
+};
+
+
+int
+sudo_module_register_conv_message(PyObject *py_module)
+{
+ debug_decl(_sudo_module_register_conv_message, PYTHON_DEBUG_INTERNAL);
+ int rc = SUDO_RC_ERROR;
+ PyObject *py_class = NULL;
+
+ py_class = sudo_module_create_class("sudo.ConvMessage", _sudo_ConvMessage_class_methods, NULL);
+ if (py_class == NULL)
+ goto cleanup;
+
+ if (PyModule_AddObject(py_module, "ConvMessage", py_class) < 0) {
+ goto cleanup;
+ }
+
+ Py_INCREF(py_class);
+ rc = SUDO_RC_OK;
+
+ Py_CLEAR(sudo_type_ConvMessage);
+ sudo_type_ConvMessage = (PyTypeObject *)py_class;
+ Py_INCREF(sudo_type_ConvMessage);
+
+cleanup:
+ Py_CLEAR(py_class);
+ debug_return_int(rc);
+}
+
+int
+sudo_module_ConvMessage_to_c(PyObject *py_conv_message, struct sudo_conv_message *conv_message)
+{
+ debug_decl(sudo_module_ConvMessage_to_c, PYTHON_DEBUG_C_CALLS);
+
+ conv_message->msg_type = (int)py_object_get_optional_attr_number(py_conv_message, "msg_type");
+ if (PyErr_Occurred())
+ debug_return_int(SUDO_RC_ERROR);
+
+ conv_message->timeout = (int)py_object_get_optional_attr_number(py_conv_message, "timeout");
+ if (PyErr_Occurred()) // -V547
+ debug_return_int(SUDO_RC_ERROR);
+
+ conv_message->msg = py_object_get_optional_attr_string(py_conv_message, "msg");
+ if (PyErr_Occurred()) // -V547
+ debug_return_int(SUDO_RC_ERROR);
+
+ debug_return_int(SUDO_RC_OK);
+}
+
+int
+sudo_module_ConvMessages_to_c(PyObject *py_tuple, Py_ssize_t *num_msgs, struct sudo_conv_message **msgs)
+{
+ debug_decl(sudo_module_ConvMessages_to_c, PYTHON_DEBUG_C_CALLS);
+
+ *num_msgs = PyTuple_Size(py_tuple);
+ *msgs = NULL;
+
+ if (*num_msgs <= 0) {
+ *num_msgs = 0;
+ PyErr_Format(sudo_exc_SudoException, "Expected at least one ConvMessage");
+ debug_return_int(SUDO_RC_ERROR);
+ }
+
+ *msgs = calloc(*num_msgs, sizeof(struct sudo_conv_message));
+ if (*msgs == NULL) {
+ debug_return_int(SUDO_RC_ERROR);
+ }
+
+ for (Py_ssize_t i = 0; i < *num_msgs; ++i) {
+ PyObject *py_msg = py_tuple_get(py_tuple, i, sudo_type_ConvMessage);
+ if (py_msg == NULL || sudo_module_ConvMessage_to_c(py_msg, &(*msgs)[i]) < 0) {
+ debug_return_int(SUDO_RC_ERROR);
+ }
+ }
+
+ debug_return_int(SUDO_RC_OK);
+}
+
diff --git a/plugins/python/python_loghandler.c b/plugins/python/python_loghandler.c
new file mode 100644
index 0000000..2541e68
--- /dev/null
+++ b/plugins/python/python_loghandler.c
@@ -0,0 +1,190 @@
+/*
+ * 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 "sudo_python_module.h"
+
+PyObject *sudo_type_LogHandler;
+
+
+static void
+_debug_plugin(int log_level, const char *log_message)
+{
+ debug_decl_vars(python_sudo_debug, PYTHON_DEBUG_PLUGIN);
+
+ if (sudo_debug_needed(SUDO_DEBUG_INFO)) {
+ // at trace level we output the position for the python log as well
+ char *func_name = NULL, *file_name = NULL;
+ long line_number = -1;
+
+ if (py_get_current_execution_frame(&file_name, &line_number, &func_name) == SUDO_RC_OK) {
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s @ %s:%ld debugs:\n",
+ func_name, file_name, line_number);
+ }
+
+ free(func_name);
+ free(file_name);
+ }
+
+ sudo_debug_printf(log_level, "%s\n", log_message);
+}
+
+PyObject *
+python_sudo_debug(PyObject *Py_UNUSED(py_self), PyObject *py_args)
+{
+ debug_decl(python_sudo_debug, PYTHON_DEBUG_C_CALLS);
+ py_debug_python_call("sudo", "debug", py_args, NULL, PYTHON_DEBUG_C_CALLS);
+
+ int log_level = SUDO_DEBUG_DEBUG;
+ const char *log_message = NULL;
+ if (!PyArg_ParseTuple(py_args, "is:sudo.debug", &log_level, &log_message)) {
+ debug_return_ptr(NULL);
+ }
+
+ _debug_plugin(log_level, log_message);
+
+ debug_return_ptr_pynone;
+}
+
+static int
+_sudo_log_level_from_python(long level)
+{
+ if (level >= 50)
+ return SUDO_DEBUG_CRIT;
+ if (level >= 40)
+ return SUDO_DEBUG_ERROR;
+ if (level >= 30)
+ return SUDO_DEBUG_WARN;
+ if (level >= 20)
+ return SUDO_DEBUG_INFO;
+
+ return SUDO_DEBUG_TRACE;
+}
+
+static PyObject *
+_sudo_LogHandler__emit(PyObject *py_self, PyObject *py_args)
+{
+ debug_decl(_sudo_LogHandler__emit, PYTHON_DEBUG_C_CALLS);
+
+ PyObject *py_record = NULL; // borrowed
+ PyObject *py_message = NULL;
+
+ py_debug_python_call("LogHandler", "emit", py_args, NULL, PYTHON_DEBUG_C_CALLS);
+
+ if (!PyArg_UnpackTuple(py_args, "sudo.LogHandler.emit", 2, 2, &py_self, &py_record))
+ goto cleanup;
+
+ long python_loglevel = py_object_get_optional_attr_number(py_record, "levelno");
+ if (PyErr_Occurred()) {
+ PyErr_Format(sudo_exc_SudoException, "sudo.LogHandler: Failed to determine log level");
+ goto cleanup;
+ }
+
+ int sudo_loglevel = _sudo_log_level_from_python(python_loglevel);
+
+ py_message = PyObject_CallMethod(py_self, "format", "O", py_record);
+ if (py_message == NULL)
+ goto cleanup;
+
+ _debug_plugin(sudo_loglevel, PyUnicode_AsUTF8(py_message));
+
+cleanup:
+ Py_CLEAR(py_message);
+ if (PyErr_Occurred()) {
+ debug_return_ptr(NULL);
+ }
+
+ debug_return_ptr_pynone;
+}
+
+/* The sudo.LogHandler class can be used to make the default python logger
+ * use sudo's built in log system. */
+static PyMethodDef _sudo_LogHandler_class_methods[] =
+{
+ {"emit", _sudo_LogHandler__emit, METH_VARARGS, ""},
+ {NULL, NULL, 0, NULL}
+};
+
+// This function registers sudo.LogHandler class
+int
+sudo_module_register_loghandler(PyObject *py_module)
+{
+ debug_decl(sudo_module_register_loghandler, PYTHON_DEBUG_INTERNAL);
+
+ PyObject *py_logging_module = NULL, *py_streamhandler = NULL;
+
+ py_logging_module = PyImport_ImportModule("logging");
+ if (py_logging_module == NULL)
+ goto cleanup;
+
+ py_streamhandler = PyObject_GetAttrString(py_logging_module, "StreamHandler");
+ if (py_streamhandler == NULL)
+ goto cleanup;
+
+ sudo_type_LogHandler = sudo_module_create_class("sudo.LogHandler",
+ _sudo_LogHandler_class_methods, py_streamhandler);
+ if (sudo_type_LogHandler == NULL)
+ goto cleanup;
+
+ if (PyModule_AddObject(py_module, "LogHandler", sudo_type_LogHandler) < 0)
+ goto cleanup;
+
+ Py_INCREF(sudo_type_LogHandler);
+
+cleanup:
+ Py_CLEAR(py_streamhandler);
+ Py_CLEAR(py_logging_module);
+ debug_return_int(PyErr_Occurred() ? SUDO_RC_ERROR : SUDO_RC_OK);
+}
+
+// This sets sudo.LogHandler as the default log handler:
+// logging.getLogger().addHandler(sudo.LogHandler())
+int
+sudo_module_set_default_loghandler(void)
+{
+ debug_decl(sudo_module_set_default_loghandler, PYTHON_DEBUG_INTERNAL);
+
+ PyObject *py_loghandler = NULL, *py_logging_module = NULL,
+ *py_logger = NULL, *py_result = NULL;
+
+ py_loghandler = PyObject_CallObject(sudo_type_LogHandler, NULL);
+ if (py_loghandler == NULL)
+ goto cleanup;
+
+ py_logging_module = PyImport_ImportModule("logging");
+ if (py_logging_module == NULL)
+ goto cleanup;
+
+ py_logger = PyObject_CallMethod(py_logging_module, "getLogger", NULL);
+ if (py_logger == NULL)
+ goto cleanup;
+
+ py_result = PyObject_CallMethod(py_logger, "addHandler", "O", py_loghandler);
+
+cleanup:
+ Py_CLEAR(py_result);
+ Py_CLEAR(py_logger);
+ Py_CLEAR(py_logging_module);
+ Py_CLEAR(py_loghandler);
+
+ debug_return_int(PyErr_Occurred() ? SUDO_RC_ERROR : SUDO_RC_OK);
+}
diff --git a/plugins/python/python_plugin.exp b/plugins/python/python_plugin.exp
new file mode 100644
index 0000000..1261a58
--- /dev/null
+++ b/plugins/python/python_plugin.exp
@@ -0,0 +1,8 @@
+group_plugin
+python_approval
+python_approval_clone
+python_audit
+python_audit_clone
+python_io
+python_io_clone
+python_policy
diff --git a/plugins/python/python_plugin_approval.c b/plugins/python/python_plugin_approval.c
new file mode 100644
index 0000000..b9c746e
--- /dev/null
+++ b/plugins/python/python_plugin_approval.c
@@ -0,0 +1,196 @@
+/*
+ * 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 "python_plugin_common.h"
+
+struct ApprovalPluginContext
+{
+ struct PluginContext base_ctx;
+ struct approval_plugin *plugin;
+};
+
+#define BASE_CTX(approval_ctx) (&(approval_ctx->base_ctx))
+
+#define PY_APPROVAL_PLUGIN_VERSION SUDO_API_MKVERSION(1, 0)
+
+#define CALLBACK_PLUGINFUNC(func_name) approval_ctx->plugin->func_name
+
+// This also verifies compile time that the name matches the sudo plugin API.
+#define CALLBACK_PYNAME(func_name) ((void)CALLBACK_PLUGINFUNC(func_name), #func_name)
+
+sudo_dso_public struct approval_plugin *python_approval_clone(void);
+
+static int
+python_plugin_approval_open(struct ApprovalPluginContext *approval_ctx,
+ unsigned int version, sudo_conv_t conversation, sudo_printf_t sudo_printf,
+ char * const settings[], char * const user_info[], int submit_optind,
+ char * const submit_argv[], char * const submit_envp[],
+ char * const plugin_options[], const char **errstr)
+{
+ debug_decl(python_plugin_approval_open, PYTHON_DEBUG_CALLBACKS);
+ (void) version;
+
+ int rc = python_plugin_register_logging(conversation, sudo_printf, settings);
+ if (rc != SUDO_RC_OK) {
+ debug_return_int(rc);
+ }
+
+ struct PluginContext *plugin_ctx = BASE_CTX(approval_ctx);
+
+ rc = python_plugin_init(plugin_ctx, plugin_options, version);
+ if (rc != SUDO_RC_OK) {
+ debug_return_int(rc);
+ }
+
+ PyObject *py_kwargs = NULL, *py_submit_optind = NULL,
+ *py_submit_argv = NULL;
+
+ if ((py_kwargs = python_plugin_construct_args(version, settings, user_info,
+ submit_envp, plugin_options)) == NULL ||
+ (py_submit_optind = PyLong_FromLong(submit_optind)) == NULL ||
+ (py_submit_argv = py_str_array_to_tuple(submit_argv)) == NULL)
+ {
+ py_log_last_error("Failed to construct plugin instance");
+ rc = SUDO_RC_ERROR;
+ } else {
+ PyDict_SetItemString(py_kwargs, "submit_optind", py_submit_optind);
+ PyDict_SetItemString(py_kwargs, "submit_argv", py_submit_argv);
+
+ rc = python_plugin_construct_custom(plugin_ctx, py_kwargs);
+ CALLBACK_SET_ERROR(plugin_ctx, errstr);
+ }
+
+ Py_CLEAR(py_kwargs);
+ Py_CLEAR(py_submit_argv);
+ Py_CLEAR(py_submit_optind);
+
+ if (rc != SUDO_RC_OK) {
+ debug_return_int(rc);
+ }
+
+ debug_return_int(rc);
+}
+
+static void
+python_plugin_approval_close(struct ApprovalPluginContext *approval_ctx)
+{
+ debug_decl(python_plugin_approval_close, PYTHON_DEBUG_CALLBACKS);
+
+ struct PluginContext *plugin_ctx = BASE_CTX(approval_ctx);
+ PyThreadState_Swap(plugin_ctx->py_interpreter);
+ python_plugin_deinit(plugin_ctx);
+
+ debug_return;
+}
+
+static int
+python_plugin_approval_check(struct ApprovalPluginContext *approval_ctx,
+ char * const command_info[], char * const run_argv[],
+ char * const run_envp[], const char **errstr)
+{
+ debug_decl(python_plugin_approval_check, PYTHON_DEBUG_CALLBACKS);
+
+ struct PluginContext *plugin_ctx = BASE_CTX(approval_ctx);
+
+ PyObject *py_command_info = NULL, *py_run_argv = NULL, *py_run_envp = NULL,
+ *py_args = NULL;
+
+ int rc = SUDO_RC_ERROR;
+ if ((py_command_info = py_str_array_to_tuple(command_info)) != NULL &&
+ (py_run_argv = py_str_array_to_tuple(run_argv)) != NULL &&
+ (py_run_envp = py_str_array_to_tuple(run_envp)) != NULL)
+ {
+ py_args = Py_BuildValue("(OOO)", py_command_info, py_run_argv, py_run_envp);
+ }
+
+ // Note, py_args gets cleared by api_rc_call
+ rc = python_plugin_api_rc_call(plugin_ctx, CALLBACK_PYNAME(check), py_args);
+ CALLBACK_SET_ERROR(plugin_ctx, errstr);
+
+ Py_CLEAR(py_command_info);
+ Py_CLEAR(py_run_argv);
+ Py_CLEAR(py_run_envp);
+
+ debug_return_int(rc);
+}
+
+static int
+python_plugin_approval_show_version(struct ApprovalPluginContext *approval_ctx, int verbose)
+{
+ debug_decl(python_plugin_approval_show_version, PYTHON_DEBUG_CALLBACKS);
+
+ struct PluginContext *plugin_ctx = BASE_CTX(approval_ctx);
+ PyThreadState_Swap(plugin_ctx->py_interpreter);
+
+ debug_return_int(python_plugin_show_version(plugin_ctx,
+ CALLBACK_PYNAME(show_version), verbose, PY_APPROVAL_PLUGIN_VERSION, "approval"));
+}
+
+sudo_dso_public struct approval_plugin python_approval;
+
+// generate symbols for loading multiple approval plugins:
+#define APPROVAL_SYMBOL_NAME(symbol) symbol
+#include "python_plugin_approval_multi.inc"
+#define APPROVAL_SYMBOL_NAME(symbol) symbol##1
+#include "python_plugin_approval_multi.inc"
+#define APPROVAL_SYMBOL_NAME(symbol) symbol##2
+#include "python_plugin_approval_multi.inc"
+#define APPROVAL_SYMBOL_NAME(symbol) symbol##3
+#include "python_plugin_approval_multi.inc"
+#define APPROVAL_SYMBOL_NAME(symbol) symbol##4
+#include "python_plugin_approval_multi.inc"
+#define APPROVAL_SYMBOL_NAME(symbol) symbol##5
+#include "python_plugin_approval_multi.inc"
+#define APPROVAL_SYMBOL_NAME(symbol) symbol##6
+#include "python_plugin_approval_multi.inc"
+#define APPROVAL_SYMBOL_NAME(symbol) symbol##7
+#include "python_plugin_approval_multi.inc"
+
+static struct approval_plugin *extra_approval_plugins[] = {
+ &python_approval1,
+ &python_approval2,
+ &python_approval3,
+ &python_approval4,
+ &python_approval5,
+ &python_approval6,
+ &python_approval7
+};
+
+struct approval_plugin *
+python_approval_clone(void)
+{
+ static size_t counter = 0;
+ struct approval_plugin *next_plugin = NULL;
+
+ size_t max = sizeof(extra_approval_plugins) / sizeof(*extra_approval_plugins);
+ if (counter < max) {
+ next_plugin = extra_approval_plugins[counter];
+ ++counter;
+ } else if (counter == max) {
+ ++counter;
+ py_sudo_log(SUDO_CONV_ERROR_MSG,
+ "sudo: loading more than %d sudo python approval plugins is not supported\n", counter);
+ }
+
+ return next_plugin;
+}
diff --git a/plugins/python/python_plugin_approval_multi.inc b/plugins/python/python_plugin_approval_multi.inc
new file mode 100644
index 0000000..d5b15ff
--- /dev/null
+++ b/plugins/python/python_plugin_approval_multi.inc
@@ -0,0 +1,57 @@
+/* The purpose of this file is to generate a approval_plugin symbols,
+ * with an I/O plugin context which is unique to it and its functions.
+ * The callbacks inside are just wrappers around the real functions in python_plugin_approval.c,
+ * their only purpose is to add the unique context to each separate approval_plugin call.
+ */
+
+#define PLUGIN_CTX APPROVAL_SYMBOL_NAME(plugin_ctx)
+#define CALLBACK_CFUNC(func_name) APPROVAL_SYMBOL_NAME(_python_plugin_approval_ ## func_name)
+
+extern struct approval_plugin APPROVAL_SYMBOL_NAME(python_approval);
+static struct ApprovalPluginContext PLUGIN_CTX = { { NULL }, &APPROVAL_SYMBOL_NAME(python_approval) };
+
+
+static int
+CALLBACK_CFUNC(open)(unsigned int version, sudo_conv_t conversation,
+ sudo_printf_t sudo_printf, char * const settings[],
+ char * const user_info[], int submit_optind,
+ char * const submit_argv[], char * const submit_envp[],
+ char * const plugin_options[], const char **errstr)
+{
+ return python_plugin_approval_open(&PLUGIN_CTX, version, conversation,
+ sudo_printf, settings, user_info, submit_optind, submit_argv,
+ submit_envp, plugin_options, errstr);
+}
+
+static void
+CALLBACK_CFUNC(close)(void)
+{
+ python_plugin_approval_close(&PLUGIN_CTX);
+}
+
+static int
+CALLBACK_CFUNC(check)(char * const command_info[], char * const run_argv[],
+ char * const run_envp[], const char **errstr)
+{
+ return python_plugin_approval_check(&PLUGIN_CTX, command_info, run_argv,
+ run_envp, errstr);
+}
+
+static int
+CALLBACK_CFUNC(show_version)(int verbose)
+{
+ return python_plugin_approval_show_version(&PLUGIN_CTX, verbose);
+}
+
+struct approval_plugin APPROVAL_SYMBOL_NAME(python_approval) = {
+ SUDO_APPROVAL_PLUGIN,
+ SUDO_API_VERSION,
+ CALLBACK_CFUNC(open),
+ CALLBACK_CFUNC(close),
+ CALLBACK_CFUNC(check),
+ CALLBACK_CFUNC(show_version)
+};
+
+#undef PLUGIN_CTX
+#undef CALLBACK_CFUNC
+#undef APPROVAL_SYMBOL_NAME
diff --git a/plugins/python/python_plugin_audit.c b/plugins/python/python_plugin_audit.c
new file mode 100644
index 0000000..58d59f7
--- /dev/null
+++ b/plugins/python/python_plugin_audit.c
@@ -0,0 +1,281 @@
+/*
+ * 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 "python_plugin_common.h"
+
+struct AuditPluginContext
+{
+ struct PluginContext base_ctx;
+ struct audit_plugin *plugin;
+};
+
+#define BASE_CTX(audit_ctx) (&(audit_ctx->base_ctx))
+
+#define PY_AUDIT_PLUGIN_VERSION SUDO_API_MKVERSION(1, 0)
+
+#define CALLBACK_PLUGINFUNC(func_name) audit_ctx->plugin->func_name
+
+// This also verifies compile time that the name matches the sudo plugin API.
+#define CALLBACK_PYNAME(func_name) ((void)CALLBACK_PLUGINFUNC(func_name), #func_name)
+
+#define MARK_CALLBACK_OPTIONAL(function_name) \
+ do { \
+ python_plugin_mark_callback_optional(plugin_ctx, CALLBACK_PYNAME(function_name), \
+ (void **)&CALLBACK_PLUGINFUNC(function_name)); \
+ } while(0)
+
+sudo_dso_public struct audit_plugin *python_audit_clone(void);
+
+static int
+_call_plugin_open(struct AuditPluginContext *audit_ctx, int submit_optind, char * const submit_argv[])
+{
+ debug_decl(_call_plugin_open, PYTHON_DEBUG_CALLBACKS);
+
+ struct PluginContext *plugin_ctx = BASE_CTX(audit_ctx);
+ if (!PyObject_HasAttrString(plugin_ctx->py_instance, CALLBACK_PYNAME(open))) {
+ debug_return_int(SUDO_RC_OK);
+ }
+
+ int rc = SUDO_RC_ERROR;
+ PyObject *py_submit_argv = py_str_array_to_tuple(submit_argv);
+
+ if (py_submit_argv != NULL) {
+ rc = python_plugin_api_rc_call(plugin_ctx, CALLBACK_PYNAME(open),
+ Py_BuildValue("(iO)", submit_optind, py_submit_argv));
+ }
+
+ Py_XDECREF(py_submit_argv);
+ debug_return_int(rc);
+}
+
+static int
+python_plugin_audit_open(struct AuditPluginContext *audit_ctx,
+ unsigned int version, sudo_conv_t conversation,
+ sudo_printf_t sudo_printf, char * const settings[],
+ char * const user_info[], int submit_optind,
+ char * const submit_argv[], char * const submit_envp[],
+ char * const plugin_options[], const char **errstr)
+{
+ debug_decl(python_plugin_audit_open, PYTHON_DEBUG_CALLBACKS);
+ (void) version;
+
+ int rc = python_plugin_register_logging(conversation, sudo_printf, settings);
+ if (rc != SUDO_RC_OK) {
+ debug_return_int(rc);
+ }
+
+ struct PluginContext *plugin_ctx = BASE_CTX(audit_ctx);
+
+ rc = python_plugin_init(plugin_ctx, plugin_options, version);
+ if (rc != SUDO_RC_OK) {
+ debug_return_int(rc);
+ }
+
+ rc = python_plugin_construct(plugin_ctx, PY_AUDIT_PLUGIN_VERSION, settings,
+ user_info, submit_envp, plugin_options);
+ CALLBACK_SET_ERROR(plugin_ctx, errstr);
+ if (rc != SUDO_RC_OK) {
+ debug_return_int(rc);
+ }
+
+ // skip plugin callbacks which are not mandatory
+ MARK_CALLBACK_OPTIONAL(accept);
+ MARK_CALLBACK_OPTIONAL(reject);
+ MARK_CALLBACK_OPTIONAL(error);
+
+ plugin_ctx->call_close = 1;
+ rc = _call_plugin_open(audit_ctx, submit_optind, submit_argv);
+ CALLBACK_SET_ERROR(plugin_ctx, errstr);
+
+ if (PyErr_Occurred()) {
+ py_log_last_error("Error during calling audit open");
+ }
+
+ debug_return_int(rc);
+}
+
+static void
+python_plugin_audit_close(struct AuditPluginContext *audit_ctx, int status_type, int status)
+{
+ debug_decl(python_plugin_audit_close, PYTHON_DEBUG_CALLBACKS);
+
+ python_plugin_close(BASE_CTX(audit_ctx), CALLBACK_PYNAME(close),
+ Py_BuildValue("(ii)", status_type, status));
+
+ debug_return;
+}
+
+static int
+python_plugin_audit_accept(struct AuditPluginContext *audit_ctx,
+ const char *plugin_name, unsigned int plugin_type,
+ char * const command_info[], char * const run_argv[],
+ char * const run_envp[], const char **errstr)
+{
+ debug_decl(python_plugin_audit_accept, PYTHON_DEBUG_CALLBACKS);
+
+ struct PluginContext *plugin_ctx = BASE_CTX(audit_ctx);
+ PyThreadState_Swap(plugin_ctx->py_interpreter);
+
+ PyObject *py_command_info = NULL, *py_run_argv = NULL, *py_run_envp = NULL;
+ int rc = SUDO_RC_ERROR;
+
+ py_run_argv = py_str_array_to_tuple(run_argv);
+ if (py_run_argv == NULL)
+ goto cleanup;
+
+ py_command_info = py_str_array_to_tuple(command_info);
+ if (py_command_info == NULL)
+ goto cleanup;
+
+ py_run_envp = py_str_array_to_tuple(run_envp);
+ if (py_run_envp == NULL)
+ goto cleanup;
+
+ PyObject *py_args = Py_BuildValue("(ziOOO)", plugin_name, plugin_type, py_command_info, py_run_argv, py_run_envp);
+ rc = python_plugin_api_rc_call(plugin_ctx, CALLBACK_PYNAME(accept), py_args);
+ CALLBACK_SET_ERROR(plugin_ctx, errstr);
+
+cleanup:
+ Py_CLEAR(py_command_info);
+ Py_CLEAR(py_run_argv);
+ Py_CLEAR(py_run_envp);
+
+ debug_return_int(rc);
+}
+
+static int
+python_plugin_audit_reject(struct AuditPluginContext *audit_ctx,
+ const char *plugin_name, unsigned int plugin_type,
+ const char *audit_msg, char * const command_info[], const char **errstr)
+{
+ debug_decl(python_plugin_audit_reject, PYTHON_DEBUG_CALLBACKS);
+
+ struct PluginContext *plugin_ctx = BASE_CTX(audit_ctx);
+ PyThreadState_Swap(plugin_ctx->py_interpreter);
+
+ PyObject *py_command_info = NULL;
+ int rc = SUDO_RC_ERROR;
+
+ py_command_info = py_str_array_to_tuple(command_info);
+ if (PyErr_Occurred())
+ goto cleanup;
+
+ PyObject *py_args = Py_BuildValue("(zizO)", plugin_name, plugin_type, audit_msg, py_command_info);
+ rc = python_plugin_api_rc_call(plugin_ctx, CALLBACK_PYNAME(reject), py_args);
+
+ CALLBACK_SET_ERROR(plugin_ctx, errstr);
+
+cleanup:
+ Py_CLEAR(py_command_info);
+ if (PyErr_Occurred())
+ py_log_last_error("Error during calling audit reject");
+
+ debug_return_int(rc);
+}
+
+static int
+python_plugin_audit_error(struct AuditPluginContext *audit_ctx,
+ const char *plugin_name, unsigned int plugin_type,
+ const char *audit_msg, char * const command_info[], const char **errstr)
+{
+ debug_decl(python_plugin_audit_error, PYTHON_DEBUG_CALLBACKS);
+
+ struct PluginContext *plugin_ctx = BASE_CTX(audit_ctx);
+ PyThreadState_Swap(plugin_ctx->py_interpreter);
+
+ PyObject *py_command_info = NULL;
+ int rc = SUDO_RC_ERROR;
+
+ py_command_info = py_str_array_to_tuple(command_info);
+ if (PyErr_Occurred())
+ goto cleanup;
+
+ PyObject *py_args = Py_BuildValue("(zizO)", plugin_name, plugin_type, audit_msg, py_command_info);
+ rc = python_plugin_api_rc_call(plugin_ctx, CALLBACK_PYNAME(error), py_args);
+ CALLBACK_SET_ERROR(plugin_ctx, errstr);
+
+cleanup:
+ Py_CLEAR(py_command_info);
+
+ debug_return_int(rc);
+}
+
+static int
+python_plugin_audit_show_version(struct AuditPluginContext *audit_ctx, int verbose)
+{
+ debug_decl(python_plugin_audit_show_version, PYTHON_DEBUG_CALLBACKS);
+
+ struct PluginContext *plugin_ctx = BASE_CTX(audit_ctx);
+ PyThreadState_Swap(plugin_ctx->py_interpreter);
+
+ debug_return_int(python_plugin_show_version(plugin_ctx,
+ CALLBACK_PYNAME(show_version), verbose, PY_AUDIT_PLUGIN_VERSION, "audit"));
+}
+
+sudo_dso_public struct audit_plugin python_audit;
+
+// generate symbols for loading multiple audit plugins:
+#define AUDIT_SYMBOL_NAME(symbol) symbol
+#include "python_plugin_audit_multi.inc"
+#define AUDIT_SYMBOL_NAME(symbol) symbol##1
+#include "python_plugin_audit_multi.inc"
+#define AUDIT_SYMBOL_NAME(symbol) symbol##2
+#include "python_plugin_audit_multi.inc"
+#define AUDIT_SYMBOL_NAME(symbol) symbol##3
+#include "python_plugin_audit_multi.inc"
+#define AUDIT_SYMBOL_NAME(symbol) symbol##4
+#include "python_plugin_audit_multi.inc"
+#define AUDIT_SYMBOL_NAME(symbol) symbol##5
+#include "python_plugin_audit_multi.inc"
+#define AUDIT_SYMBOL_NAME(symbol) symbol##6
+#include "python_plugin_audit_multi.inc"
+#define AUDIT_SYMBOL_NAME(symbol) symbol##7
+#include "python_plugin_audit_multi.inc"
+
+static struct audit_plugin *extra_audit_plugins[] = {
+ &python_audit1,
+ &python_audit2,
+ &python_audit3,
+ &python_audit4,
+ &python_audit5,
+ &python_audit6,
+ &python_audit7
+};
+
+struct audit_plugin *
+python_audit_clone(void)
+{
+ static size_t counter = 0;
+ struct audit_plugin *next_plugin = NULL;
+
+ size_t max = sizeof(extra_audit_plugins) / sizeof(*extra_audit_plugins);
+ if (counter < max) {
+ next_plugin = extra_audit_plugins[counter];
+ ++counter;
+ } else if (counter == max) {
+ ++counter;
+ py_sudo_log(SUDO_CONV_ERROR_MSG, "sudo: loading more than %d sudo python audit plugins is not supported\n", counter);
+ }
+
+ return next_plugin;
+}
diff --git a/plugins/python/python_plugin_audit_multi.inc b/plugins/python/python_plugin_audit_multi.inc
new file mode 100644
index 0000000..015b4d1
--- /dev/null
+++ b/plugins/python/python_plugin_audit_multi.inc
@@ -0,0 +1,78 @@
+/* The purpose of this file is to generate a audit_plugin symbols,
+ * with an I/O plugin context which is unique to it and its functions.
+ * The callbacks inside are just wrappers around the real functions in python_plugin_audit.c,
+ * their only purpose is to add the unique context to each separate audit_plugin call.
+ */
+
+#define PLUGIN_CTX AUDIT_SYMBOL_NAME(plugin_ctx)
+#define CALLBACK_CFUNC(func_name) AUDIT_SYMBOL_NAME(_python_plugin_audit_ ## func_name)
+
+extern struct audit_plugin AUDIT_SYMBOL_NAME(python_audit);
+static struct AuditPluginContext PLUGIN_CTX = { { NULL }, &AUDIT_SYMBOL_NAME(python_audit) };
+
+
+static int
+CALLBACK_CFUNC(open)(unsigned int version, sudo_conv_t conversation,
+ sudo_printf_t sudo_printf, char * const settings[],
+ char * const user_info[], int submit_optind,
+ char * const submit_argv[], char * const submit_envp[],
+ char * const plugin_options[], const char **errstr)
+{
+ return python_plugin_audit_open(&PLUGIN_CTX, version, conversation, sudo_printf,
+ settings, user_info, submit_optind, submit_argv, submit_envp,
+ plugin_options, errstr);
+}
+
+static void
+CALLBACK_CFUNC(close)(int status_type, int status)
+{
+ python_plugin_audit_close(&PLUGIN_CTX, status_type, status);
+}
+
+static int
+CALLBACK_CFUNC(accept)(const char *plugin_name, unsigned int plugin_type,
+ char * const command_info[], char * const run_argv[],
+ char * const run_envp[], const char **errstr)
+{
+ return python_plugin_audit_accept(&PLUGIN_CTX, plugin_name, plugin_type,
+ command_info, run_argv, run_envp, errstr);
+}
+
+static int
+CALLBACK_CFUNC(reject)(const char *plugin_name, unsigned int plugin_type,
+ const char *audit_msg, char * const command_info[], const char **errstr)
+{
+ return python_plugin_audit_reject(&PLUGIN_CTX, plugin_name, plugin_type,
+ audit_msg, command_info, errstr);
+}
+
+static int
+CALLBACK_CFUNC(error)(const char *plugin_name, unsigned int plugin_type,
+ const char *audit_msg, char * const command_info[], const char **errstr)
+{
+ return python_plugin_audit_error(&PLUGIN_CTX, plugin_name, plugin_type,
+ audit_msg, command_info, errstr);
+}
+
+static int
+CALLBACK_CFUNC(show_version)(int verbose)
+{
+ return python_plugin_audit_show_version(&PLUGIN_CTX, verbose);
+}
+
+struct audit_plugin AUDIT_SYMBOL_NAME(python_audit) = {
+ SUDO_AUDIT_PLUGIN,
+ SUDO_API_VERSION,
+ CALLBACK_CFUNC(open),
+ CALLBACK_CFUNC(close),
+ CALLBACK_CFUNC(accept),
+ CALLBACK_CFUNC(reject),
+ CALLBACK_CFUNC(error),
+ CALLBACK_CFUNC(show_version),
+ NULL, /* register_hooks */
+ NULL /* deregister_hooks */
+};
+
+#undef PLUGIN_CTX
+#undef CALLBACK_CFUNC
+#undef AUDIT_SYMBOL_NAME
diff --git a/plugins/python/python_plugin_common.c b/plugins/python/python_plugin_common.c
new file mode 100644
index 0000000..0c174cb
--- /dev/null
+++ b/plugins/python/python_plugin_common.c
@@ -0,0 +1,755 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 Robert Manner <robert.manner@oneidentity.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * This is an open source non-commercial project. Dear PVS-Studio, please check it.
+ * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
+ */
+
+#include "python_plugin_common.h"
+#include "sudo_python_module.h"
+
+#include "sudo_queue.h"
+#include "sudo_conf.h"
+
+#include <limits.h>
+#include <string.h>
+
+static struct _inittab * python_inittab_copy = NULL;
+static size_t python_inittab_copy_len = 0;
+
+#ifndef PLUGIN_DIR
+#define PLUGIN_DIR ""
+#endif
+
+/* Py_FinalizeEx is new in version 3.6 */
+#if PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION < 6
+# define Py_FinalizeEx() (Py_Finalize(), 0)
+#endif
+
+static const char *
+_lookup_value(char * const keyvalues[], const char *key)
+{
+ debug_decl(_lookup_value, PYTHON_DEBUG_INTERNAL);
+ if (keyvalues == NULL)
+ debug_return_const_str(NULL);
+
+ size_t keylen = strlen(key);
+ for (; *keyvalues != NULL; ++keyvalues) {
+ const char *keyvalue = *keyvalues;
+ if (strncmp(keyvalue, key, keylen) == 0 && keyvalue[keylen] == '=')
+ debug_return_const_str(keyvalue + keylen + 1);
+ }
+ debug_return_const_str(NULL);
+}
+
+CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
+static int
+_append_python_path(const char *module_dir)
+{
+ debug_decl(_append_python_path, PYTHON_DEBUG_PLUGIN_LOAD);
+ int rc = -1;
+ PyObject *py_sys_path = PySys_GetObject("path");
+ if (py_sys_path == NULL) {
+ PyErr_Format(sudo_exc_SudoException, "Failed to get python 'path'");
+ debug_return_int(rc);
+ }
+
+ sudo_debug_printf(SUDO_DEBUG_DIAG, "Extending python 'path' with '%s'\n", module_dir);
+
+ PyObject *py_module_dir = PyUnicode_FromString(module_dir);
+ if (py_module_dir == NULL || PyList_Append(py_sys_path, py_module_dir) != 0) {
+ Py_XDECREF(py_module_dir);
+ debug_return_int(rc);
+ }
+ Py_XDECREF(py_module_dir);
+
+ if (sudo_debug_needed(SUDO_DEBUG_INFO)) {
+ char *path = py_join_str_list(py_sys_path, ":");
+ sudo_debug_printf(SUDO_DEBUG_INFO, "Python path became: %s\n", path);
+ free(path);
+ }
+
+ rc = 0;
+ debug_return_int(rc);
+}
+
+static PyObject *
+_import_module(const char *path)
+{
+ PyObject *module;
+ debug_decl(_import_module, PYTHON_DEBUG_PLUGIN_LOAD);
+
+ sudo_debug_printf(SUDO_DEBUG_DIAG, "importing module: %s\n", path);
+
+ char path_copy[PATH_MAX];
+ if (strlcpy(path_copy, path, sizeof(path_copy)) >= sizeof(path_copy))
+ debug_return_ptr(NULL);
+
+ const char *module_dir = path_copy;
+ char *module_name = strrchr(path_copy, '/');
+ if (module_name == NULL) {
+ module_name = path_copy;
+ module_dir = "";
+ } else {
+ *module_name++ = '\0';
+ }
+
+ size_t len = strlen(module_name);
+ if (len >= 3 && strcmp(".py", module_name + len - 3) == 0)
+ module_name[len - 3] = '\0';
+
+ sudo_debug_printf(SUDO_DEBUG_INFO, "module_name: '%s', module_dir: '%s'\n", module_name, module_dir);
+
+ if (_append_python_path(module_dir) < 0)
+ debug_return_ptr(NULL);
+
+ module = PyImport_ImportModule(module_name);
+ if (module != NULL) {
+ PyObject *py_loaded_path = PyObject_GetAttrString(module, "__file__");
+ if (py_loaded_path != NULL) {
+ const char *loaded_path = PyUnicode_AsUTF8(py_loaded_path);
+ /* If path is a directory, loaded_path may be a file inside it. */
+ if (strncmp(loaded_path, path, strlen(path)) != 0) {
+ PyErr_Format(PyExc_Exception,
+ "module name conflict, tried to load %s, got %s",
+ path, loaded_path);
+ Py_CLEAR(module);
+ }
+ Py_DECREF(py_loaded_path);
+ }
+ }
+ debug_return_ptr(module);
+}
+
+static PyThreadState *
+_python_plugin_new_interpreter(void)
+{
+ debug_decl(_python_plugin_new_interpreter, PYTHON_DEBUG_INTERNAL);
+ if (py_ctx.interpreter_count >= INTERPRETER_MAX) {
+ PyErr_Format(PyExc_Exception, "Too many interpreters");
+ debug_return_ptr(NULL);
+ }
+
+ PyThreadState *py_interpreter = Py_NewInterpreter();
+ if (py_interpreter != NULL) {
+ py_ctx.py_subinterpreters[py_ctx.interpreter_count] = py_interpreter;
+ ++py_ctx.interpreter_count;
+ }
+
+ debug_return_ptr(py_interpreter);
+}
+
+static int
+_save_inittab(void)
+{
+ debug_decl(_save_inittab, PYTHON_DEBUG_INTERNAL);
+ free(python_inittab_copy); // just to be sure (it is always NULL)
+
+ for (python_inittab_copy_len = 0;
+ PyImport_Inittab[python_inittab_copy_len].name != NULL;
+ ++python_inittab_copy_len) {
+ }
+ ++python_inittab_copy_len; // for the null mark
+
+ python_inittab_copy = malloc(sizeof(struct _inittab) * python_inittab_copy_len);
+ if (python_inittab_copy == NULL) {
+ debug_return_int(SUDO_RC_ERROR);
+ }
+
+ memcpy(python_inittab_copy, PyImport_Inittab, python_inittab_copy_len * sizeof(struct _inittab));
+ debug_return_int(SUDO_RC_OK);
+}
+
+static void
+_restore_inittab(void)
+{
+ debug_decl(_restore_inittab, PYTHON_DEBUG_INTERNAL);
+
+ if (python_inittab_copy != NULL)
+ memcpy(PyImport_Inittab, python_inittab_copy, python_inittab_copy_len * sizeof(struct _inittab));
+
+ free(python_inittab_copy);
+ python_inittab_copy = NULL;
+ python_inittab_copy_len = 0;
+ debug_return;
+}
+
+static void
+python_plugin_handle_plugin_error_exception(PyObject **py_result, struct PluginContext *plugin_ctx)
+{
+ debug_decl(python_plugin_handle_plugin_error_exception, PYTHON_DEBUG_INTERNAL);
+
+ free(plugin_ctx->callback_error);
+ plugin_ctx->callback_error = NULL;
+
+ if (PyErr_Occurred()) {
+ int rc = SUDO_RC_ERROR;
+ if (PyErr_ExceptionMatches(sudo_exc_PluginReject)) {
+ rc = SUDO_RC_REJECT;
+ } else if (!PyErr_ExceptionMatches(sudo_exc_PluginError)) {
+ debug_return;
+ }
+
+ if (py_result != NULL) {
+ Py_CLEAR(*py_result);
+ *py_result = PyLong_FromLong(rc);
+ }
+
+ PyObject *py_type = NULL, *py_message = NULL, *py_traceback = NULL;
+ PyErr_Fetch(&py_type, &py_message, &py_traceback);
+
+ char *message = py_message ? py_create_string_rep(py_message) : NULL;
+ sudo_debug_printf(SUDO_DEBUG_INFO, "received sudo.PluginError exception with message '%s'",
+ message == NULL ? "(null)" : message);
+
+ plugin_ctx->callback_error = message;
+
+ Py_CLEAR(py_type);
+ Py_CLEAR(py_message);
+ Py_CLEAR(py_traceback);
+ }
+
+ debug_return;
+}
+
+int
+python_plugin_construct_custom(struct PluginContext *plugin_ctx, PyObject *py_kwargs)
+{
+ debug_decl(python_plugin_construct_custom, PYTHON_DEBUG_PLUGIN_LOAD);
+ int rc = SUDO_RC_ERROR;
+ PyObject *py_args = PyTuple_New(0);
+
+ if (py_args == NULL)
+ goto cleanup;
+
+ py_debug_python_call(python_plugin_name(plugin_ctx), "__init__",
+ py_args, py_kwargs, PYTHON_DEBUG_PY_CALLS);
+
+ plugin_ctx->py_instance = PyObject_Call(plugin_ctx->py_class, py_args, py_kwargs);
+ python_plugin_handle_plugin_error_exception(NULL, plugin_ctx);
+
+ py_debug_python_result(python_plugin_name(plugin_ctx), "__init__",
+ plugin_ctx->py_instance, PYTHON_DEBUG_PY_CALLS);
+
+ if (plugin_ctx->py_instance)
+ rc = SUDO_RC_OK;
+
+cleanup:
+ if (PyErr_Occurred()) {
+ py_log_last_error("Failed to construct plugin instance");
+ Py_CLEAR(plugin_ctx->py_instance);
+ rc = SUDO_RC_ERROR;
+ }
+
+ Py_XDECREF(py_args);
+ debug_return_int(rc);
+}
+
+PyObject *
+python_plugin_construct_args(unsigned int version,
+ char *const settings[], char *const user_info[],
+ char *const user_env[], char *const plugin_options[])
+{
+ PyObject *py_settings = NULL;
+ PyObject *py_user_info = NULL;
+ PyObject *py_user_env = NULL;
+ PyObject *py_plugin_options = NULL;
+ PyObject *py_version = NULL;
+ PyObject *py_kwargs = NULL;
+
+ if ((py_settings = py_str_array_to_tuple(settings)) == NULL ||
+ (py_user_info = py_str_array_to_tuple(user_info)) == NULL ||
+ (py_user_env = py_str_array_to_tuple(user_env)) == NULL ||
+ (py_plugin_options = py_str_array_to_tuple(plugin_options)) == NULL ||
+ (py_version = py_create_version(version)) == NULL ||
+ (py_kwargs = PyDict_New()) == NULL ||
+ PyDict_SetItemString(py_kwargs, "version", py_version) != 0 ||
+ PyDict_SetItemString(py_kwargs, "settings", py_settings) != 0 ||
+ PyDict_SetItemString(py_kwargs, "user_env", py_user_env) != 0 ||
+ PyDict_SetItemString(py_kwargs, "user_info", py_user_info) != 0 ||
+ PyDict_SetItemString(py_kwargs, "plugin_options", py_plugin_options) != 0)
+ {
+ Py_CLEAR(py_kwargs);
+ }
+
+ Py_CLEAR(py_settings);
+ Py_CLEAR(py_user_info);
+ Py_CLEAR(py_user_env);
+ Py_CLEAR(py_plugin_options);
+ Py_CLEAR(py_version);
+ return py_kwargs;
+}
+
+int
+python_plugin_construct(struct PluginContext *plugin_ctx, unsigned int version,
+ char *const settings[], char *const user_info[],
+ char *const user_env[], char *const plugin_options[])
+{
+ debug_decl(python_plugin_construct, PYTHON_DEBUG_PLUGIN_LOAD);
+
+ int rc = SUDO_RC_ERROR;
+ PyObject *py_kwargs = python_plugin_construct_args(
+ version, settings, user_info, user_env, plugin_options);
+
+ if (py_kwargs == NULL) {
+ py_log_last_error("Failed to construct plugin instance");
+ } else {
+ rc = python_plugin_construct_custom(plugin_ctx, py_kwargs);
+ }
+
+ Py_CLEAR(py_kwargs);
+
+ debug_return_int(rc);
+}
+
+int
+python_plugin_register_logging(sudo_conv_t conversation,
+ sudo_printf_t sudo_printf,
+ char * const settings[])
+{
+ debug_decl(python_plugin_register_logging, PYTHON_DEBUG_INTERNAL);
+
+ int rc = SUDO_RC_ERROR;
+ if (conversation != NULL)
+ py_ctx.sudo_conv = conversation;
+
+ if (sudo_printf)
+ py_ctx.sudo_log = sudo_printf;
+
+ struct sudo_conf_debug_file_list debug_files = TAILQ_HEAD_INITIALIZER(debug_files);
+ struct sudo_conf_debug_file_list *debug_files_ptr = &debug_files;
+
+ const char *plugin_path = _lookup_value(settings, "plugin_path");
+ if (plugin_path == NULL)
+ plugin_path = "python_plugin.so";
+
+ const char *debug_flags = _lookup_value(settings, "debug_flags");
+
+ if (debug_flags == NULL) { // the group plugin does not have this information, so try to look it up
+ debug_files_ptr = sudo_conf_debug_files(plugin_path);
+ } else {
+ if (!python_debug_parse_flags(&debug_files, debug_flags))
+ goto cleanup;
+ }
+
+ if (debug_files_ptr != NULL) {
+ if (!python_debug_register(plugin_path, debug_files_ptr))
+ goto cleanup;
+ }
+
+ rc = SUDO_RC_OK;
+
+cleanup:
+ debug_return_int(rc);
+}
+
+CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
+static int
+_python_plugin_register_plugin_in_py_ctx(void)
+{
+ debug_decl(_python_plugin_register_plugin_in_py_ctx, PYTHON_DEBUG_PLUGIN_LOAD);
+
+ if (!Py_IsInitialized()) {
+ // Disable environment variables effecting the python interpreter
+ // This is important since we are running code here as root, the
+ // user should not be able to alter what is running any how.
+ Py_IgnoreEnvironmentFlag = 1;
+ Py_IsolatedFlag = 1;
+ Py_NoUserSiteDirectory = 1;
+
+ if (_save_inittab() != SUDO_RC_OK)
+ debug_return_int(SUDO_RC_ERROR);
+
+ PyImport_AppendInittab("sudo", sudo_module_init);
+ Py_InitializeEx(0);
+ py_ctx.py_main_interpreter = PyThreadState_Get();
+
+ // This ensures we import "sudo" module in the main interpreter,
+ // each subinterpreter will have a shallow copy.
+ // (This makes the C sudo module able to eg. import other modules.)
+ PyObject *py_sudo = NULL;
+ if ((py_sudo = PyImport_ImportModule("sudo")) == NULL) {
+ debug_return_int(SUDO_RC_ERROR);
+ }
+ Py_CLEAR(py_sudo);
+ } else {
+ PyThreadState_Swap(py_ctx.py_main_interpreter);
+ }
+
+ debug_return_int(SUDO_RC_OK);
+}
+
+static int
+_python_plugin_set_path(struct PluginContext *plugin_ctx, const char *path)
+{
+ if (path == NULL) {
+ py_sudo_log(SUDO_CONV_ERROR_MSG, "No python module path is specified. "
+ "Use 'ModulePath' plugin config option in 'sudo.conf'\n");
+ return SUDO_RC_ERROR;
+ }
+
+ if (*path == '/') { // absolute path
+ plugin_ctx->plugin_path = strdup(path);
+ } else {
+ if (asprintf(&plugin_ctx->plugin_path, PLUGIN_DIR "/python/%s", path) < 0)
+ plugin_ctx->plugin_path = NULL;
+ }
+
+ if (plugin_ctx->plugin_path == NULL) {
+ py_sudo_log(SUDO_CONV_ERROR_MSG, "Failed to allocate memory");
+ return SUDO_RC_ERROR;
+ }
+
+ return SUDO_RC_OK;
+}
+
+/* Returns the list of sudo.Plugins in a module */
+static PyObject *
+_python_plugin_class_list(PyObject *py_module) {
+ PyObject *py_module_dict = PyModule_GetDict(py_module); // Note: borrowed
+ PyObject *key, *value; // Note: borrowed
+ Py_ssize_t pos = 0;
+ PyObject *py_plugin_list = PyList_New(0);
+
+ while (PyDict_Next(py_module_dict, &pos, &key, &value)) {
+ if (PyObject_IsSubclass(value, (PyObject *)sudo_type_Plugin) == 1) {
+ if (PyList_Append(py_plugin_list, key) != 0)
+ goto cleanup;
+ } else {
+ PyErr_Clear();
+ }
+ }
+
+cleanup:
+ if (PyErr_Occurred()) {
+ Py_CLEAR(py_plugin_list);
+ }
+ return py_plugin_list;
+}
+
+/* Gets a sudo.Plugin class from the specified module. The argument "plugin_class"
+ * can be NULL in which case it loads the one and only "sudo.Plugin" present
+ * in the module (if so), or displays helpful error message. */
+static PyObject *
+_python_plugin_get_class(const char *plugin_path, PyObject *py_module, const char *plugin_class)
+{
+ debug_decl(python_plugin_init, PYTHON_DEBUG_PLUGIN_LOAD);
+ PyObject *py_plugin_list = NULL, *py_class = NULL;
+
+ if (plugin_class == NULL) {
+ py_plugin_list = _python_plugin_class_list(py_module);
+ if (py_plugin_list == NULL) {
+ goto cleanup;
+ }
+
+ if (PyList_Size(py_plugin_list) == 1) {
+ PyObject *py_plugin_name = PyList_GetItem(py_plugin_list, 0); // Note: borrowed
+ plugin_class = PyUnicode_AsUTF8(py_plugin_name);
+ }
+ }
+
+ if (plugin_class == NULL) {
+ py_sudo_log(SUDO_CONV_ERROR_MSG, "No plugin class is specified for python module '%s'. "
+ "Use 'ClassName' configuration option in 'sudo.conf'\n", plugin_path);
+ if (py_plugin_list != NULL) {
+ /* Sorting the plugin list makes regress test output consistent. */
+ PyObject *py_obj = PyObject_CallMethod(py_plugin_list, "sort", "");
+ Py_CLEAR(py_obj);
+ char *possible_plugins = py_join_str_list(py_plugin_list, ", ");
+ if (possible_plugins != NULL) {
+ py_sudo_log(SUDO_CONV_ERROR_MSG, "Possible plugins: %s\n", possible_plugins);
+ free(possible_plugins);
+ }
+ }
+ goto cleanup;
+ }
+
+ sudo_debug_printf(SUDO_DEBUG_DEBUG, "Using plugin class '%s'", plugin_class);
+ py_class = PyObject_GetAttrString(py_module, plugin_class);
+ if (py_class == NULL) {
+ py_sudo_log(SUDO_CONV_ERROR_MSG, "Failed to find plugin class '%s'\n", plugin_class);
+ PyErr_Clear();
+ goto cleanup;
+ }
+
+ if (!PyObject_IsSubclass(py_class, (PyObject *)sudo_type_Plugin)) {
+ py_sudo_log(SUDO_CONV_ERROR_MSG, "Plugin class '%s' does not inherit from 'sudo.Plugin'\n", plugin_class);
+ Py_CLEAR(py_class);
+ goto cleanup;
+ }
+
+cleanup:
+ Py_CLEAR(py_plugin_list);
+ debug_return_ptr(py_class);
+}
+
+int
+python_plugin_init(struct PluginContext *plugin_ctx, char * const plugin_options[],
+ unsigned int version)
+{
+ debug_decl(python_plugin_init, PYTHON_DEBUG_PLUGIN_LOAD);
+
+ int rc = SUDO_RC_ERROR;
+
+ if (_python_plugin_register_plugin_in_py_ctx() != SUDO_RC_OK)
+ goto cleanup;
+
+ plugin_ctx->sudo_api_version = version;
+
+ plugin_ctx->py_interpreter = _python_plugin_new_interpreter();
+ if (plugin_ctx->py_interpreter == NULL) {
+ goto cleanup;
+ }
+ PyThreadState_Swap(plugin_ctx->py_interpreter);
+
+ if (sudo_module_set_default_loghandler() < 0)
+ goto cleanup;
+
+ if (_python_plugin_set_path(plugin_ctx, _lookup_value(plugin_options, "ModulePath")) != SUDO_RC_OK) {
+ goto cleanup;
+ }
+
+ sudo_debug_printf(SUDO_DEBUG_DEBUG, "Loading python module from path '%s'", plugin_ctx->plugin_path);
+ plugin_ctx->py_module = _import_module(plugin_ctx->plugin_path);
+ if (plugin_ctx->py_module == NULL) {
+ goto cleanup;
+ }
+
+ plugin_ctx->py_class = _python_plugin_get_class(plugin_ctx->plugin_path, plugin_ctx->py_module,
+ _lookup_value(plugin_options, "ClassName"));
+ if (plugin_ctx->py_class == NULL) {
+ goto cleanup;
+ }
+
+ rc = SUDO_RC_OK;
+
+cleanup:
+ if (plugin_ctx->py_class == NULL) {
+ py_log_last_error("Failed during loading plugin class");
+ rc = SUDO_RC_ERROR;
+ }
+
+ debug_return_int(rc);
+}
+
+void
+python_plugin_deinit(struct PluginContext *plugin_ctx)
+{
+ debug_decl(python_plugin_deinit, PYTHON_DEBUG_PLUGIN_LOAD);
+ sudo_debug_printf(SUDO_DEBUG_DIAG, "Deinit was called for a python plugin\n");
+
+ Py_CLEAR(plugin_ctx->py_instance);
+ Py_CLEAR(plugin_ctx->py_class);
+ Py_CLEAR(plugin_ctx->py_module);
+
+ // Note: we are preserving the interpreters here until the unlink because
+ // of bugs like (strptime does not work after python interpreter reinit):
+ // https://bugs.python.org/issue27400
+ // These potentially effect a lot more python functions, simply because
+ // it is a rare tested scenario.
+
+ free(plugin_ctx->callback_error);
+ free(plugin_ctx->plugin_path);
+ memset(plugin_ctx, 0, sizeof(*plugin_ctx));
+
+ python_debug_deregister();
+ debug_return;
+}
+
+PyObject *
+python_plugin_api_call(struct PluginContext *plugin_ctx, const char *func_name, PyObject *py_args)
+{
+ debug_decl(python_plugin_api_call, PYTHON_DEBUG_PY_CALLS);
+
+ // Note: call fails if py_args is an empty tuple. Passing no arguments works passing NULL
+ // instead. So having such must be handled as valid. (See policy_plugin.validate())
+ if (py_args == NULL && PyErr_Occurred()) {
+ py_sudo_log(SUDO_CONV_ERROR_MSG, "Failed to build arguments for python plugin API call '%s'\n", func_name);
+ py_log_last_error(NULL);
+ debug_return_ptr(NULL);
+ }
+
+ PyObject *py_callable = NULL;
+ py_callable = PyObject_GetAttrString(plugin_ctx->py_instance, func_name);
+
+ if (py_callable == NULL) {
+ Py_CLEAR(py_args);
+ debug_return_ptr(NULL);
+ }
+
+ py_debug_python_call(python_plugin_name(plugin_ctx), func_name,
+ py_args, NULL, PYTHON_DEBUG_PY_CALLS);
+
+ PyObject *py_result = PyObject_CallObject(py_callable, py_args);
+ Py_CLEAR(py_args);
+ Py_CLEAR(py_callable);
+
+ py_debug_python_result(python_plugin_name(plugin_ctx), func_name,
+ py_result, PYTHON_DEBUG_PY_CALLS);
+
+ python_plugin_handle_plugin_error_exception(&py_result, plugin_ctx);
+
+ if (PyErr_Occurred()) {
+ py_log_last_error(NULL);
+ }
+
+ debug_return_ptr(py_result);
+}
+
+int
+python_plugin_rc_to_int(PyObject *py_result)
+{
+ debug_decl(python_plugin_rc_to_int, PYTHON_DEBUG_PY_CALLS);
+ if (py_result == NULL)
+ debug_return_int(SUDO_RC_ERROR);
+
+ if (py_result == Py_None)
+ debug_return_int(SUDO_RC_OK);
+
+ debug_return_int((int)PyLong_AsLong(py_result));
+}
+
+int
+python_plugin_api_rc_call(struct PluginContext *plugin_ctx, const char *func_name, PyObject *py_args)
+{
+ debug_decl(python_plugin_api_rc_call, PYTHON_DEBUG_PY_CALLS);
+
+ PyObject *py_result = python_plugin_api_call(plugin_ctx, func_name, py_args);
+ int rc = python_plugin_rc_to_int(py_result);
+ Py_XDECREF(py_result);
+ debug_return_int(rc);
+}
+
+int
+python_plugin_show_version(struct PluginContext *plugin_ctx, const char *python_callback_name,
+ int is_verbose, unsigned int plugin_api_version, const char *plugin_api_name)
+{
+ debug_decl(python_plugin_show_version, PYTHON_DEBUG_CALLBACKS);
+
+ if (is_verbose) {
+ py_sudo_log(SUDO_CONV_INFO_MSG, "Python %s plugin (API %d.%d): %s (loaded from '%s')\n",
+ plugin_api_name,
+ SUDO_API_VERSION_GET_MAJOR(plugin_api_version),
+ SUDO_API_VERSION_GET_MINOR(plugin_api_version),
+ python_plugin_name(plugin_ctx),
+ plugin_ctx->plugin_path);
+ }
+
+ int rc = SUDO_RC_OK;
+ if (PyObject_HasAttrString(plugin_ctx->py_instance, python_callback_name)) {
+ rc = python_plugin_api_rc_call(plugin_ctx, python_callback_name,
+ Py_BuildValue("(i)", is_verbose));
+ }
+
+ debug_return_int(rc);
+}
+
+void
+python_plugin_close(struct PluginContext *plugin_ctx, const char *callback_name,
+ PyObject *py_args)
+{
+ debug_decl(python_plugin_close, PYTHON_DEBUG_CALLBACKS);
+
+ PyThreadState_Swap(plugin_ctx->py_interpreter);
+
+ // Note, this should handle the case when init has failed
+ if (plugin_ctx->py_instance != NULL) {
+ if (!plugin_ctx->call_close) {
+ sudo_debug_printf(SUDO_DEBUG_INFO, "Skipping close call, because there was no command run\n");
+
+ } else if (!PyObject_HasAttrString(plugin_ctx->py_instance, callback_name)) {
+ sudo_debug_printf(SUDO_DEBUG_INFO, "Python plugin function 'close' is skipped (not present)\n");
+ } else {
+ PyObject *py_result = python_plugin_api_call(plugin_ctx, callback_name, py_args);
+ py_args = NULL; // api call already freed it
+ Py_XDECREF(py_result);
+ }
+ }
+
+ Py_CLEAR(py_args);
+
+ if (PyErr_Occurred()) {
+ py_log_last_error(NULL);
+ }
+
+ python_plugin_deinit(plugin_ctx);
+
+ debug_return;
+}
+
+void
+python_plugin_mark_callback_optional(struct PluginContext *plugin_ctx,
+ const char *function_name, void **function)
+{
+ if (!PyObject_HasAttrString(plugin_ctx->py_instance, function_name)) {
+ debug_decl_vars(python_plugin_mark_callback_optional, PYTHON_DEBUG_PY_CALLS);
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s function '%s' is not implemented\n",
+ Py_TYPENAME(plugin_ctx->py_instance), function_name);
+ *function = NULL;
+ }
+}
+
+const char *
+python_plugin_name(struct PluginContext *plugin_ctx)
+{
+ debug_decl(python_plugin_name, PYTHON_DEBUG_INTERNAL);
+
+ const char *name = "(NULL)";
+
+ if (plugin_ctx == NULL || !PyType_Check(plugin_ctx->py_class))
+ debug_return_const_str(name);
+
+ debug_return_const_str(((PyTypeObject *)(plugin_ctx->py_class))->tp_name);
+}
+
+void python_plugin_unlink(void) __attribute__((destructor));
+
+// this gets run only when sudo unlinks the python_plugin.so
+void
+python_plugin_unlink(void)
+{
+ debug_decl(python_plugin_unlink, PYTHON_DEBUG_INTERNAL);
+ if (py_ctx.py_main_interpreter == NULL)
+ return;
+
+ if (Py_IsInitialized()) {
+ sudo_debug_printf(SUDO_DEBUG_NOTICE, "Closing: deinit python %zu subinterpreters\n",
+ py_ctx.interpreter_count);
+ while (py_ctx.interpreter_count != 0) {
+ PyThreadState *py_interpreter =
+ py_ctx.py_subinterpreters[--py_ctx.interpreter_count];
+ PyThreadState_Swap(py_interpreter);
+ Py_EndInterpreter(py_interpreter);
+ }
+
+ sudo_debug_printf(SUDO_DEBUG_NOTICE, "Closing: deinit main interpreter\n");
+
+ // we need to call finalize from the main interpreter
+ PyThreadState_Swap(py_ctx.py_main_interpreter);
+
+ if (Py_FinalizeEx() != 0) {
+ sudo_debug_printf(SUDO_DEBUG_WARN, "Closing: failed to deinit python interpreter\n");
+ }
+
+ // Restore inittab so "sudo" module does not remain there (as garbage)
+ _restore_inittab();
+ }
+ py_ctx_reset();
+ debug_return;
+}
diff --git a/plugins/python/python_plugin_common.h b/plugins/python/python_plugin_common.h
new file mode 100644
index 0000000..c0fdce6
--- /dev/null
+++ b/plugins/python/python_plugin_common.h
@@ -0,0 +1,85 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 Robert Manner <robert.manner@oneidentity.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef SUDO_PYTHON_PLUGIN_COMMON_H
+#define SUDO_PYTHON_PLUGIN_COMMON_H
+
+#include "pyhelpers.h"
+
+struct PluginContext {
+ PyThreadState *py_interpreter;
+ PyObject *py_module;
+ PyObject *py_class;
+ PyObject *py_instance;
+ int call_close;
+ unsigned int sudo_api_version;
+ char *plugin_path;
+
+ // We use this to let the error string live until sudo and the audit plugins
+ // are using it.
+ char *callback_error;
+};
+
+int python_plugin_register_logging(sudo_conv_t conversation, sudo_printf_t sudo_printf, char * const settings[]);
+
+int python_plugin_init(struct PluginContext *plugin_ctx, char * const plugin_options[], unsigned int version);
+
+int python_plugin_construct_custom(struct PluginContext *plugin_ctx, PyObject *py_kwargs);
+
+PyObject *python_plugin_construct_args(unsigned int version, char *const settings[],
+ char *const user_info[], char *const user_env[], char *const plugin_options[]);
+
+int python_plugin_construct(struct PluginContext *plugin_ctx, unsigned int version,
+ char *const settings[], char *const user_info[],
+ char *const user_env[], char *const plugin_options[]);
+
+void python_plugin_deinit(struct PluginContext *plugin_ctx);
+
+int python_plugin_show_version(struct PluginContext *plugin_ctx,
+ const char *python_callback_name, int isVerbose, unsigned int plugin_api_version, const char *plugin_api_name);
+
+CPYCHECKER_STEALS_REFERENCE_TO_ARG(3)
+void python_plugin_close(struct PluginContext *plugin_ctx, const char *callback_name,
+ PyObject *py_args);
+
+CPYCHECKER_STEALS_REFERENCE_TO_ARG(3)
+PyObject *python_plugin_api_call(struct PluginContext *plugin_ctx,
+ const char *func_name, PyObject *py_args);
+
+CPYCHECKER_STEALS_REFERENCE_TO_ARG(3)
+int python_plugin_api_rc_call(struct PluginContext *plugin_ctx,
+ const char *func_name, PyObject *py_args);
+
+int python_plugin_rc_to_int(PyObject *py_result);
+
+void python_plugin_mark_callback_optional(struct PluginContext *plugin_ctx,
+ const char *function_name, void **function);
+
+const char *python_plugin_name(struct PluginContext *plugin_ctx);
+
+// sets the callback error stored in plugin_ctx into "errstr" but only if API
+// version is enough and "errstr" is valid
+#define CALLBACK_SET_ERROR(plugin_ctx, errstr) \
+ do { \
+ if ((plugin_ctx)->sudo_api_version >= SUDO_API_MKVERSION(1, 15)) { \
+ if (errstr != NULL) \
+ *errstr = (plugin_ctx)->callback_error; \
+ } \
+ } while(0)
+
+#endif // SUDO_PYTHON_PLUGIN_COMMON_H
diff --git a/plugins/python/python_plugin_group.c b/plugins/python/python_plugin_group.c
new file mode 100644
index 0000000..b1514c7
--- /dev/null
+++ b/plugins/python/python_plugin_group.c
@@ -0,0 +1,114 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 Robert Manner <robert.manner@oneidentity.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * This is an open source non-commercial project. Dear PVS-Studio, please check it.
+ * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
+ */
+
+#include "python_plugin_common.h"
+
+static struct PluginContext plugin_ctx;
+
+extern struct sudoers_group_plugin group_plugin;
+
+#define PY_GROUP_PLUGIN_VERSION SUDO_API_MKVERSION(1, 0)
+
+#define CALLBACK_PLUGINFUNC(func_name) group_plugin.func_name
+#define CALLBACK_CFUNC(func_name) python_plugin_group_ ## func_name
+
+// This also verifies compile time that the name matches the sudo plugin API.
+#define CALLBACK_PYNAME(func_name) ((void)CALLBACK_PLUGINFUNC(func_name), #func_name)
+
+
+static int
+python_plugin_group_init(int version, sudo_printf_t sudo_printf, char *const plugin_options[])
+{
+ debug_decl(python_plugin_group_init, PYTHON_DEBUG_CALLBACKS);
+
+ if (version < SUDO_API_MKVERSION(1, 0)) {
+ sudo_printf(SUDO_CONV_ERROR_MSG,
+ "Error: Python group plugin requires at least plugin API version 1.0\n");
+ debug_return_int(SUDO_RC_ERROR);
+ }
+
+ int rc = SUDO_RC_ERROR;
+
+ rc = python_plugin_register_logging(NULL, sudo_printf, NULL);
+ if (rc != SUDO_RC_OK)
+ debug_return_int(rc);
+
+ rc = python_plugin_init(&plugin_ctx, plugin_options, (unsigned int)version);
+ if (rc != SUDO_RC_OK)
+ debug_return_int(rc);
+
+ PyObject *py_version = NULL,
+ *py_plugin_options = NULL,
+ *py_kwargs = NULL;
+
+ if ((py_kwargs = PyDict_New()) == NULL ||
+ (py_version = py_create_version(PY_GROUP_PLUGIN_VERSION)) == NULL ||
+ (py_plugin_options = py_str_array_to_tuple(plugin_options)) == NULL ||
+ PyDict_SetItemString(py_kwargs, "args", py_plugin_options) != 0 ||
+ PyDict_SetItemString(py_kwargs, "version", py_version))
+ {
+ py_log_last_error("Failed to construct arguments for plugin constructor call.");
+ rc = SUDO_RC_ERROR;
+ } else {
+ rc = python_plugin_construct_custom(&plugin_ctx, py_kwargs);
+ }
+
+ Py_XDECREF(py_version);
+ Py_XDECREF(py_plugin_options);
+ Py_XDECREF(py_kwargs);
+ debug_return_int(rc);
+}
+
+static void
+python_plugin_group_cleanup(void)
+{
+ debug_decl(python_plugin_group_cleanup, PYTHON_DEBUG_CALLBACKS);
+ PyThreadState_Swap(plugin_ctx.py_interpreter);
+ python_plugin_deinit(&plugin_ctx);
+}
+
+static int
+python_plugin_group_query(const char *user, const char *group, const struct passwd *pwd)
+{
+ debug_decl(python_plugin_group_query, PYTHON_DEBUG_CALLBACKS);
+
+ PyThreadState_Swap(plugin_ctx.py_interpreter);
+
+ PyObject *py_pwd = py_from_passwd(pwd);
+ if (py_pwd == NULL) {
+ debug_return_int(SUDO_RC_ERROR);
+ }
+
+ int rc = python_plugin_api_rc_call(&plugin_ctx, CALLBACK_PYNAME(query),
+ Py_BuildValue("(zzO)", user, group, py_pwd));
+ Py_XDECREF(py_pwd);
+
+ debug_return_int(rc);
+}
+
+sudo_dso_public struct sudoers_group_plugin group_plugin = {
+ GROUP_API_VERSION,
+ CALLBACK_CFUNC(init),
+ CALLBACK_CFUNC(cleanup),
+ CALLBACK_CFUNC(query)
+};
diff --git a/plugins/python/python_plugin_io.c b/plugins/python/python_plugin_io.c
new file mode 100644
index 0000000..d554cb5
--- /dev/null
+++ b/plugins/python/python_plugin_io.c
@@ -0,0 +1,276 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 Robert Manner <robert.manner@oneidentity.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * This is an open source non-commercial project. Dear PVS-Studio, please check it.
+ * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
+ */
+
+#include "python_plugin_common.h"
+
+struct IOPluginContext
+{
+ struct PluginContext base_ctx;
+ struct io_plugin *io_plugin;
+};
+
+#define BASE_CTX(io_ctx) (&(io_ctx->base_ctx))
+
+#define PY_IO_PLUGIN_VERSION SUDO_API_MKVERSION(1, 0)
+
+#define CALLBACK_PLUGINFUNC(func_name) io_ctx->io_plugin->func_name
+
+// This also verifies compile time that the name matches the sudo plugin API.
+#define CALLBACK_PYNAME(func_name) ((void)CALLBACK_PLUGINFUNC(func_name), #func_name)
+
+#define MARK_CALLBACK_OPTIONAL(function_name) \
+ do { \
+ python_plugin_mark_callback_optional(plugin_ctx, CALLBACK_PYNAME(function_name), \
+ (void **)&CALLBACK_PLUGINFUNC(function_name)); \
+ } while(0)
+
+sudo_dso_public struct io_plugin *python_io_clone(void);
+
+static int
+_call_plugin_open(struct IOPluginContext *io_ctx, int argc, char * const argv[], char * const command_info[])
+{
+ debug_decl(_call_plugin_open, PYTHON_DEBUG_CALLBACKS);
+ struct PluginContext *plugin_ctx = BASE_CTX(io_ctx);
+ plugin_ctx->call_close = 1;
+
+ if (!PyObject_HasAttrString(plugin_ctx->py_instance, CALLBACK_PYNAME(open))) {
+ debug_return_int(SUDO_RC_OK);
+ }
+
+ int rc = SUDO_RC_ERROR;
+ PyObject *py_argv = py_str_array_to_tuple_with_count(argc, argv);
+ PyObject *py_command_info = py_str_array_to_tuple(command_info);
+
+ if (py_argv != NULL && py_command_info != NULL) {
+ rc = python_plugin_api_rc_call(plugin_ctx, CALLBACK_PYNAME(open),
+ Py_BuildValue("(OO)", py_argv, py_command_info));
+ }
+
+ if (rc != SUDO_RC_OK)
+ plugin_ctx->call_close = 0;
+
+ Py_XDECREF(py_argv);
+ Py_XDECREF(py_command_info);
+ debug_return_int(rc);
+}
+
+static int
+python_plugin_io_open(struct IOPluginContext *io_ctx,
+ unsigned int version, sudo_conv_t conversation,
+ sudo_printf_t sudo_printf, char * const settings[],
+ char * const user_info[], char * const command_info[],
+ int argc, char * const argv[], char * const user_env[],
+ char * const plugin_options[], const char **errstr)
+{
+ debug_decl(python_plugin_io_open, PYTHON_DEBUG_CALLBACKS);
+
+ if (version < SUDO_API_MKVERSION(1, 2)) {
+ sudo_printf(SUDO_CONV_ERROR_MSG,
+ "Error: Python IO plugin requires at least plugin API version 1.2\n");
+ debug_return_int(SUDO_RC_ERROR);
+ }
+
+ int rc = python_plugin_register_logging(conversation, sudo_printf, settings);
+ if (rc != SUDO_RC_OK)
+ debug_return_int(rc);
+
+ struct PluginContext *plugin_ctx = BASE_CTX(io_ctx);
+ rc = python_plugin_init(plugin_ctx, plugin_options, version);
+
+ if (rc != SUDO_RC_OK)
+ debug_return_int(rc);
+
+ rc = python_plugin_construct(plugin_ctx, PY_IO_PLUGIN_VERSION,
+ settings, user_info, user_env, plugin_options);
+ CALLBACK_SET_ERROR(plugin_ctx, errstr);
+ if (rc != SUDO_RC_OK) {
+ debug_return_int(rc);
+ }
+
+ // skip plugin callbacks which are not mandatory
+ MARK_CALLBACK_OPTIONAL(log_ttyin);
+ MARK_CALLBACK_OPTIONAL(log_ttyout);
+ MARK_CALLBACK_OPTIONAL(log_stdin);
+ MARK_CALLBACK_OPTIONAL(log_stdout);
+ MARK_CALLBACK_OPTIONAL(log_stderr);
+ MARK_CALLBACK_OPTIONAL(change_winsize);
+ MARK_CALLBACK_OPTIONAL(log_suspend);
+ // open and close are mandatory
+
+ if (argc > 0) // we only call open if there is request for running sg
+ rc = _call_plugin_open(io_ctx, argc, argv, command_info);
+
+ CALLBACK_SET_ERROR(plugin_ctx, errstr);
+ debug_return_int(rc);
+}
+
+static void
+python_plugin_io_close(struct IOPluginContext *io_ctx, int exit_status, int error)
+{
+ debug_decl(python_plugin_io_close, PYTHON_DEBUG_CALLBACKS);
+ python_plugin_close(BASE_CTX(io_ctx), CALLBACK_PYNAME(close),
+ Py_BuildValue("(ii)", error == 0 ? exit_status : -1, error));
+ debug_return;
+}
+
+static int
+python_plugin_io_show_version(struct IOPluginContext *io_ctx, int verbose)
+{
+ debug_decl(python_plugin_io_show_version, PYTHON_DEBUG_CALLBACKS);
+
+ PyThreadState_Swap(BASE_CTX(io_ctx)->py_interpreter);
+
+ debug_return_int(python_plugin_show_version(BASE_CTX(io_ctx), CALLBACK_PYNAME(show_version),
+ verbose, PY_IO_PLUGIN_VERSION, "io"));
+}
+
+static int
+python_plugin_io_log_ttyin(struct IOPluginContext *io_ctx, const char *buf, unsigned int len, const char **errstr)
+{
+ debug_decl(python_plugin_io_log_ttyin, PYTHON_DEBUG_CALLBACKS);
+ struct PluginContext *plugin_ctx = BASE_CTX(io_ctx);
+ PyThreadState_Swap(plugin_ctx->py_interpreter);
+ int rc = python_plugin_api_rc_call(plugin_ctx, CALLBACK_PYNAME(log_ttyin),
+ Py_BuildValue("(s#)", buf, len));
+ CALLBACK_SET_ERROR(plugin_ctx, errstr);
+ debug_return_int(rc);
+}
+
+static int
+python_plugin_io_log_ttyout(struct IOPluginContext *io_ctx, const char *buf, unsigned int len, const char **errstr)
+{
+ debug_decl(python_plugin_io_log_ttyout, PYTHON_DEBUG_CALLBACKS);
+ struct PluginContext *plugin_ctx = BASE_CTX(io_ctx);
+ PyThreadState_Swap(plugin_ctx->py_interpreter);
+ int rc = python_plugin_api_rc_call(plugin_ctx, CALLBACK_PYNAME(log_ttyout),
+ Py_BuildValue("(s#)", buf, len));
+ CALLBACK_SET_ERROR(plugin_ctx, errstr);
+ debug_return_int(rc);
+}
+
+static int
+python_plugin_io_log_stdin(struct IOPluginContext *io_ctx, const char *buf, unsigned int len, const char **errstr)
+{
+ debug_decl(python_plugin_io_log_stdin, PYTHON_DEBUG_CALLBACKS);
+ struct PluginContext *plugin_ctx = BASE_CTX(io_ctx);
+ PyThreadState_Swap(plugin_ctx->py_interpreter);
+ int rc = python_plugin_api_rc_call(plugin_ctx, CALLBACK_PYNAME(log_stdin),
+ Py_BuildValue("(s#)", buf, len));
+ CALLBACK_SET_ERROR(plugin_ctx, errstr);
+ debug_return_int(rc);
+}
+
+static int
+python_plugin_io_log_stdout(struct IOPluginContext *io_ctx, const char *buf, unsigned int len, const char **errstr)
+{
+ debug_decl(python_plugin_io_log_stdout, PYTHON_DEBUG_CALLBACKS);
+ struct PluginContext *plugin_ctx = BASE_CTX(io_ctx);
+ PyThreadState_Swap(plugin_ctx->py_interpreter);
+ int rc = python_plugin_api_rc_call(plugin_ctx, CALLBACK_PYNAME(log_stdout),
+ Py_BuildValue("(s#)", buf, len));
+ CALLBACK_SET_ERROR(plugin_ctx, errstr);
+ debug_return_int(rc);
+}
+
+static int
+python_plugin_io_log_stderr(struct IOPluginContext *io_ctx, const char *buf, unsigned int len, const char **errstr)
+{
+ debug_decl(python_plugin_io_log_stderr, PYTHON_DEBUG_CALLBACKS);
+ struct PluginContext *plugin_ctx = BASE_CTX(io_ctx);
+ PyThreadState_Swap(plugin_ctx->py_interpreter);
+ int rc = python_plugin_api_rc_call(plugin_ctx, CALLBACK_PYNAME(log_stderr),
+ Py_BuildValue("(s#)", buf, len));
+ CALLBACK_SET_ERROR(plugin_ctx, errstr);
+ debug_return_int(rc);
+}
+
+static int
+python_plugin_io_change_winsize(struct IOPluginContext *io_ctx, unsigned int line, unsigned int cols, const char **errstr)
+{
+ debug_decl(python_plugin_io_change_winsize, PYTHON_DEBUG_CALLBACKS);
+ struct PluginContext *plugin_ctx = BASE_CTX(io_ctx);
+ PyThreadState_Swap(plugin_ctx->py_interpreter);
+ int rc = python_plugin_api_rc_call(plugin_ctx, CALLBACK_PYNAME(change_winsize),
+ Py_BuildValue("(ii)", line, cols));
+ CALLBACK_SET_ERROR(plugin_ctx, errstr);
+ debug_return_int(rc);
+}
+
+static int
+python_plugin_io_log_suspend(struct IOPluginContext *io_ctx, int signo, const char **errstr)
+{
+ debug_decl(python_plugin_io_log_suspend, PYTHON_DEBUG_CALLBACKS);
+ struct PluginContext *plugin_ctx = BASE_CTX(io_ctx);
+ PyThreadState_Swap(plugin_ctx->py_interpreter);
+ int rc = python_plugin_api_rc_call(plugin_ctx, CALLBACK_PYNAME(log_suspend),
+ Py_BuildValue("(i)", signo));
+ CALLBACK_SET_ERROR(plugin_ctx, errstr);
+ debug_return_int(rc);
+}
+
+// generate symbols for loading multiple io plugins:
+sudo_dso_public struct io_plugin python_io;
+#define IO_SYMBOL_NAME(symbol) symbol
+#include "python_plugin_io_multi.inc"
+#define IO_SYMBOL_NAME(symbol) symbol##1
+#include "python_plugin_io_multi.inc"
+#define IO_SYMBOL_NAME(symbol) symbol##2
+#include "python_plugin_io_multi.inc"
+#define IO_SYMBOL_NAME(symbol) symbol##3
+#include "python_plugin_io_multi.inc"
+#define IO_SYMBOL_NAME(symbol) symbol##4
+#include "python_plugin_io_multi.inc"
+#define IO_SYMBOL_NAME(symbol) symbol##5
+#include "python_plugin_io_multi.inc"
+#define IO_SYMBOL_NAME(symbol) symbol##6
+#include "python_plugin_io_multi.inc"
+#define IO_SYMBOL_NAME(symbol) symbol##7
+#include "python_plugin_io_multi.inc"
+
+static struct io_plugin *extra_io_plugins[] = {
+ &python_io1,
+ &python_io2,
+ &python_io3,
+ &python_io4,
+ &python_io5,
+ &python_io6,
+ &python_io7
+};
+
+struct io_plugin *
+python_io_clone(void)
+{
+ static size_t counter = 0;
+ struct io_plugin *next_plugin = NULL;
+
+ size_t max = sizeof(extra_io_plugins) / sizeof(*extra_io_plugins);
+ if (counter < max) {
+ next_plugin = extra_io_plugins[counter];
+ ++counter;
+ } else if (counter == max) {
+ ++counter;
+ py_sudo_log(SUDO_CONV_ERROR_MSG, "sudo: loading more than %d sudo python IO plugins is not supported\n", counter);
+ }
+
+ return next_plugin;
+}
diff --git a/plugins/python/python_plugin_io_multi.inc b/plugins/python/python_plugin_io_multi.inc
new file mode 100644
index 0000000..d5d58d2
--- /dev/null
+++ b/plugins/python/python_plugin_io_multi.inc
@@ -0,0 +1,99 @@
+/* The purpose of this file is to generate a io_plugin symbols,
+ * with an I/O plugin context which is unique to it and its functions.
+ * The callbacks inside are just wrappers around the real functions in python_plugin_io.c,
+ * their only purpose is to add the unique context to each separate io_plugin call.
+ */
+
+#define PLUGIN_CTX IO_SYMBOL_NAME(plugin_ctx)
+#define CALLBACK_CFUNC(func_name) IO_SYMBOL_NAME(_python_plugin_io_ ## func_name)
+
+extern struct io_plugin IO_SYMBOL_NAME(python_io);
+static struct IOPluginContext PLUGIN_CTX = { { NULL }, &IO_SYMBOL_NAME(python_io) };
+
+static int
+CALLBACK_CFUNC(open)(
+ unsigned int version, sudo_conv_t conversation,
+ sudo_printf_t sudo_printf, char * const settings[],
+ char * const user_info[], char * const command_info[],
+ int argc, char * const argv[], char * const user_env[],
+ char * const plugin_options[], const char **errstr)
+{
+ return python_plugin_io_open(&PLUGIN_CTX, version, conversation,
+ sudo_printf, settings, user_info, command_info, argc, argv, user_env, plugin_options, errstr);
+}
+
+static void
+CALLBACK_CFUNC(close)(int exit_status, int error)
+{
+ python_plugin_io_close(&PLUGIN_CTX, exit_status, error);
+}
+
+static int
+CALLBACK_CFUNC(show_version)(int verbose)
+{
+ return python_plugin_io_show_version(&PLUGIN_CTX, verbose);
+}
+
+static int
+CALLBACK_CFUNC(log_ttyin)(const char *buf, unsigned int len, const char **errstr)
+{
+ return python_plugin_io_log_ttyin(&PLUGIN_CTX, buf, len, errstr);
+}
+
+static int
+CALLBACK_CFUNC(log_ttyout)(const char *buf, unsigned int len, const char **errstr)
+{
+ return python_plugin_io_log_ttyout(&PLUGIN_CTX, buf, len, errstr);
+}
+
+static int
+CALLBACK_CFUNC(log_stdin)(const char *buf, unsigned int len, const char **errstr)
+{
+ return python_plugin_io_log_stdin(&PLUGIN_CTX, buf, len, errstr);
+}
+
+static int
+CALLBACK_CFUNC(log_stdout)(const char *buf, unsigned int len, const char **errstr)
+{
+ return python_plugin_io_log_stdout(&PLUGIN_CTX, buf, len, errstr);
+}
+
+static int
+CALLBACK_CFUNC(log_stderr)(const char *buf, unsigned int len, const char **errstr)
+{
+ return python_plugin_io_log_stderr(&PLUGIN_CTX, buf, len, errstr);
+}
+
+static int
+CALLBACK_CFUNC(change_winsize)(unsigned int line, unsigned int cols, const char **errstr)
+{
+ return python_plugin_io_change_winsize(&PLUGIN_CTX, line, cols, errstr);
+}
+
+static int
+CALLBACK_CFUNC(log_suspend)(int signo, const char **errstr)
+{
+ return python_plugin_io_log_suspend(&PLUGIN_CTX, signo, errstr);
+}
+
+struct io_plugin IO_SYMBOL_NAME(python_io) = {
+ SUDO_IO_PLUGIN,
+ SUDO_API_VERSION,
+ CALLBACK_CFUNC(open),
+ CALLBACK_CFUNC(close),
+ CALLBACK_CFUNC(show_version),
+ CALLBACK_CFUNC(log_ttyin),
+ CALLBACK_CFUNC(log_ttyout),
+ CALLBACK_CFUNC(log_stdin),
+ CALLBACK_CFUNC(log_stdout),
+ CALLBACK_CFUNC(log_stderr),
+ NULL, // register_hooks,
+ NULL, // deregister_hooks,
+ CALLBACK_CFUNC(change_winsize),
+ CALLBACK_CFUNC(log_suspend),
+ NULL // event_alloc
+};
+
+#undef PLUGIN_CTX
+#undef CALLBACK_CFUNC
+#undef IO_SYMBOL_NAME
diff --git a/plugins/python/python_plugin_policy.c b/plugins/python/python_plugin_policy.c
new file mode 100644
index 0000000..5f356c2
--- /dev/null
+++ b/plugins/python/python_plugin_policy.c
@@ -0,0 +1,289 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 Robert Manner <robert.manner@oneidentity.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * This is an open source non-commercial project. Dear PVS-Studio, please check it.
+ * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
+ */
+
+#include "python_plugin_common.h"
+
+
+static struct PluginContext plugin_ctx;
+
+extern struct policy_plugin python_policy;
+
+#define PY_POLICY_PLUGIN_VERSION SUDO_API_MKVERSION(1, 0)
+
+#define CALLBACK_PLUGINFUNC(func_name) python_policy.func_name
+#define CALLBACK_CFUNC(func_name) python_plugin_policy_ ## func_name
+
+// This also verifies compile time that the name matches the sudo plugin API.
+#define CALLBACK_PYNAME(func_name) ((void)CALLBACK_PLUGINFUNC(func_name), #func_name)
+
+#define MARK_CALLBACK_OPTIONAL(function_name) \
+ do { \
+ python_plugin_mark_callback_optional(&plugin_ctx, CALLBACK_PYNAME(function_name), \
+ (void **)&CALLBACK_PLUGINFUNC(function_name)); \
+ } while(0)
+
+
+static int
+python_plugin_policy_open(unsigned int version, sudo_conv_t conversation,
+ sudo_printf_t sudo_printf, char * const settings[],
+ char * const user_info[], char * const user_env[],
+ char * const plugin_options[], const char **errstr)
+{
+ debug_decl(python_plugin_policy_open, PYTHON_DEBUG_CALLBACKS);
+
+ if (version < SUDO_API_MKVERSION(1, 2)) {
+ sudo_printf(SUDO_CONV_ERROR_MSG,
+ "Error: Python policy plugin requires at least plugin API version 1.2\n");
+ debug_return_int(SUDO_RC_ERROR);
+ }
+
+ int rc = python_plugin_register_logging(conversation, sudo_printf, settings);
+ if (rc != SUDO_RC_OK)
+ debug_return_int(rc);
+
+ rc = python_plugin_init(&plugin_ctx, plugin_options, version);
+ if (rc != SUDO_RC_OK)
+ debug_return_int(rc);
+
+ rc = python_plugin_construct(&plugin_ctx, PY_POLICY_PLUGIN_VERSION, settings,
+ user_info, user_env, plugin_options);
+ CALLBACK_SET_ERROR(&plugin_ctx, errstr);
+ if (rc != SUDO_RC_OK) {
+ debug_return_int(rc);
+ }
+
+ // skip plugin callbacks which are not mandatory
+ MARK_CALLBACK_OPTIONAL(list);
+ MARK_CALLBACK_OPTIONAL(validate);
+ MARK_CALLBACK_OPTIONAL(invalidate);
+ MARK_CALLBACK_OPTIONAL(init_session);
+ // check_policy, open and close are mandatory
+
+ debug_return_int(rc);
+}
+
+static void
+python_plugin_policy_close(int exit_status, int error)
+{
+ debug_decl(python_plugin_policy_close, PYTHON_DEBUG_CALLBACKS);
+ python_plugin_close(&plugin_ctx, CALLBACK_PYNAME(close),
+ Py_BuildValue("(ii)", error == 0 ? exit_status : -1, error));
+ debug_return;
+}
+
+static int
+python_plugin_policy_check(int argc, char * const argv[],
+ char *env_add[], char **command_info_out[],
+ char **argv_out[], char **user_env_out[], const char **errstr)
+{
+ debug_decl(python_plugin_policy_check, PYTHON_DEBUG_CALLBACKS);
+ int rc = SUDO_RC_ERROR;
+
+ PyThreadState_Swap(plugin_ctx.py_interpreter);
+
+ *command_info_out = *argv_out = *user_env_out = NULL;
+
+ PyObject *py_argv = py_str_array_to_tuple_with_count(argc, argv);
+
+ PyObject *py_env_add = py_str_array_to_tuple(env_add);
+ PyObject *py_result = NULL;
+
+ if (py_argv == NULL || py_env_add == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR, "Failed to create some of the arguments for the python call "
+ "(py_argv=%p py_env_add=%p)\n", (void *)py_argv, (void *)py_env_add);
+ goto cleanup;
+ }
+
+ py_result = python_plugin_api_call(&plugin_ctx, CALLBACK_PYNAME(check_policy),
+ Py_BuildValue("(OO)", py_argv, py_env_add));
+ CALLBACK_SET_ERROR(&plugin_ctx, errstr);
+ if (py_result == NULL)
+ goto cleanup;
+
+ PyObject *py_rc = NULL,
+ *py_command_info_out = NULL,
+ *py_argv_out = NULL,
+ *py_user_env_out = NULL;
+ if (PyTuple_Check(py_result))
+ {
+ if (!PyArg_ParseTuple(py_result, "O!|O!O!O!:python_plugin.check_policy",
+ &PyLong_Type, &py_rc,
+ &PyTuple_Type, &py_command_info_out,
+ &PyTuple_Type, &py_argv_out,
+ &PyTuple_Type, &py_user_env_out))
+ {
+ goto cleanup;
+ }
+ } else {
+ py_rc = py_result;
+ }
+
+ if (py_command_info_out != NULL)
+ *command_info_out = py_str_array_from_tuple(py_command_info_out);
+
+ if (py_argv_out != NULL)
+ *argv_out = py_str_array_from_tuple(py_argv_out);
+
+ if (py_user_env_out != NULL)
+ *user_env_out = py_str_array_from_tuple(py_user_env_out);
+
+ rc = python_plugin_rc_to_int(py_rc);
+
+cleanup:
+ if (PyErr_Occurred()) {
+ py_log_last_error(NULL);
+ rc = SUDO_RC_ERROR;
+ free(*command_info_out);
+ free(*argv_out);
+ free(*user_env_out);
+ *command_info_out = *argv_out = *user_env_out = NULL;
+ }
+
+ Py_XDECREF(py_argv);
+ Py_XDECREF(py_env_add);
+ Py_XDECREF(py_result);
+
+ if (rc == SUDO_RC_ACCEPT)
+ plugin_ctx.call_close = 1;
+
+ debug_return_int(rc);
+}
+
+static int
+python_plugin_policy_list(int argc, char * const argv[], int verbose, const char *list_user, const char **errstr)
+{
+ debug_decl(python_plugin_policy_list, PYTHON_DEBUG_CALLBACKS);
+
+ PyThreadState_Swap(plugin_ctx.py_interpreter);
+
+ PyObject *py_argv = py_str_array_to_tuple_with_count(argc, argv);
+ if (py_argv == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR, "%s: Failed to create argv argument for the python call\n", __func__);
+ debug_return_int(SUDO_RC_ERROR);
+ }
+
+ int rc = python_plugin_api_rc_call(&plugin_ctx, CALLBACK_PYNAME(list),
+ Py_BuildValue("(Oiz)", py_argv, verbose, list_user));
+
+ Py_XDECREF(py_argv);
+
+ CALLBACK_SET_ERROR(&plugin_ctx, errstr);
+ debug_return_int(rc);
+}
+
+static int
+python_plugin_policy_version(int verbose)
+{
+ debug_decl(python_plugin_policy_version, PYTHON_DEBUG_CALLBACKS);
+
+ PyThreadState_Swap(plugin_ctx.py_interpreter);
+
+ debug_return_int(python_plugin_show_version(&plugin_ctx, CALLBACK_PYNAME(show_version),
+ verbose, PY_POLICY_PLUGIN_VERSION, "policy"));
+}
+
+static int
+python_plugin_policy_validate(const char **errstr)
+{
+ debug_decl(python_plugin_policy_validate, PYTHON_DEBUG_CALLBACKS);
+ PyThreadState_Swap(plugin_ctx.py_interpreter);
+ int rc = python_plugin_api_rc_call(&plugin_ctx, CALLBACK_PYNAME(validate), NULL);
+ CALLBACK_SET_ERROR(&plugin_ctx, errstr);
+ debug_return_int(rc);
+}
+
+static void
+python_plugin_policy_invalidate(int unlinkit)
+{
+ debug_decl(python_plugin_policy_invalidate, PYTHON_DEBUG_CALLBACKS);
+ PyThreadState_Swap(plugin_ctx.py_interpreter);
+ python_plugin_api_rc_call(&plugin_ctx, CALLBACK_PYNAME(invalidate),
+ Py_BuildValue("(i)", unlinkit));
+ debug_return;
+}
+
+static int
+python_plugin_policy_init_session(struct passwd *pwd, char **user_env[], const char **errstr)
+{
+ debug_decl(python_plugin_policy_init_session, PYTHON_DEBUG_CALLBACKS);
+ int rc = SUDO_RC_ERROR;
+ PyThreadState_Swap(plugin_ctx.py_interpreter);
+ PyObject *py_pwd = NULL, *py_user_env = NULL, *py_result = NULL;
+
+ py_pwd = py_from_passwd(pwd);
+ if (py_pwd == NULL)
+ goto cleanup;
+
+ py_user_env = py_str_array_to_tuple(*user_env);
+ if (py_user_env == NULL)
+ goto cleanup;
+
+ py_result = python_plugin_api_call(&plugin_ctx, CALLBACK_PYNAME(init_session),
+ Py_BuildValue("(OO)", py_pwd, py_user_env));
+ CALLBACK_SET_ERROR(&plugin_ctx, errstr);
+ if (py_result == NULL)
+ goto cleanup;
+
+ PyObject *py_user_env_out = NULL, *py_rc = NULL;
+ if (PyTuple_Check(py_result)) {
+ if (!PyArg_ParseTuple(py_result, "O!|O!:python_plugin.init_session",
+ &PyLong_Type, &py_rc,
+ &PyTuple_Type, &py_user_env_out)) {
+ goto cleanup;
+ }
+ } else {
+ py_rc = py_result;
+ }
+
+ if (py_user_env_out != NULL) {
+ str_array_free(user_env);
+ *user_env = py_str_array_from_tuple(py_user_env_out);
+ if (*user_env == NULL)
+ goto cleanup;
+ }
+
+ rc = python_plugin_rc_to_int(py_rc);
+
+cleanup:
+ Py_XDECREF(py_pwd);
+ Py_XDECREF(py_user_env);
+ Py_XDECREF(py_result);
+
+ debug_return_int(rc);
+}
+
+sudo_dso_public struct policy_plugin python_policy = {
+ SUDO_POLICY_PLUGIN,
+ SUDO_API_VERSION,
+ CALLBACK_CFUNC(open),
+ CALLBACK_CFUNC(close),
+ CALLBACK_CFUNC(version),
+ CALLBACK_CFUNC(check),
+ CALLBACK_CFUNC(list),
+ CALLBACK_CFUNC(validate),
+ CALLBACK_CFUNC(invalidate),
+ CALLBACK_CFUNC(init_session),
+ NULL, /* register_hooks */
+ NULL, /* deregister_hooks */
+ NULL /* event_alloc */
+};
diff --git a/plugins/python/regress/check_python_examples.c b/plugins/python/regress/check_python_examples.c
new file mode 100644
index 0000000..b9b9077
--- /dev/null
+++ b/plugins/python/regress/check_python_examples.c
@@ -0,0 +1,1616 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2020 Robert Manner <robert.manner@oneidentity.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * This is an open source non-commercial project. Dear PVS-Studio, please check it.
+ * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
+ */
+
+#include "testhelpers.h"
+#include <unistd.h>
+
+#include "sudo_dso.h"
+
+#define DECL_PLUGIN(type, variable_name) \
+ static struct type *variable_name = NULL; \
+ static struct type variable_name ## _original
+
+#define RESTORE_PYTHON_PLUGIN(variable_name) \
+ memcpy(variable_name, &(variable_name ## _original), sizeof(variable_name ## _original))
+
+#define SAVE_PYTHON_PLUGIN(variable_name) \
+ memcpy(&(variable_name ## _original), variable_name, sizeof(variable_name ## _original))
+
+static const char *python_plugin_so_path = NULL;
+static void *python_plugin_handle = NULL;
+DECL_PLUGIN(io_plugin, python_io);
+DECL_PLUGIN(policy_plugin, python_policy);
+DECL_PLUGIN(approval_plugin, python_approval);
+DECL_PLUGIN(audit_plugin, python_audit);
+DECL_PLUGIN(sudoers_group_plugin, group_plugin);
+
+static struct passwd example_pwd;
+static bool verbose;
+
+static int _init_symbols(void);
+static int _unlink_symbols(void);
+
+static void
+create_plugin_options(const char *module_name, const char *class_name, const char *extra_option)
+{
+ char opt_module_path[PATH_MAX + 256];
+ char opt_classname[PATH_MAX + 256];
+ snprintf(opt_module_path, sizeof(opt_module_path),
+ "ModulePath=" SRC_DIR "/%s.py", module_name);
+
+ snprintf(opt_classname, sizeof(opt_classname), "ClassName=%s", class_name);
+
+ str_array_free(&data.plugin_options);
+ size_t count = 3 + (extra_option != NULL);
+ data.plugin_options = create_str_array(count, opt_module_path,
+ opt_classname, extra_option, NULL);
+}
+
+static void
+create_io_plugin_options(const char *log_path)
+{
+ char opt_logpath[PATH_MAX + 16];
+ snprintf(opt_logpath, sizeof(opt_logpath), "LogPath=%s", log_path);
+ create_plugin_options("example_io_plugin", "SudoIOPlugin", opt_logpath);
+}
+
+static void
+create_debugging_plugin_options(void)
+{
+ create_plugin_options("example_debugging", "DebugDemoPlugin", NULL);
+}
+
+static void
+create_audit_plugin_options(const char *extra_argument)
+{
+ create_plugin_options("example_audit_plugin", "SudoAuditPlugin", extra_argument);
+}
+
+static void
+create_conversation_plugin_options(void)
+{
+ char opt_logpath[PATH_MAX + 16];
+ snprintf(opt_logpath, sizeof(opt_logpath), "LogPath=%s", data.tmp_dir);
+ create_plugin_options("example_conversation", "ReasonLoggerIOPlugin", opt_logpath);
+}
+
+static void
+create_policy_plugin_options(void)
+{
+ create_plugin_options("example_policy_plugin", "SudoPolicyPlugin", NULL);
+}
+
+static int
+init(void)
+{
+ // always start each test from clean state
+ memset(&data, 0, sizeof(data));
+
+ memset(&example_pwd, 0, sizeof(example_pwd));
+ example_pwd.pw_name = (char *)"pw_name";
+ example_pwd.pw_passwd = (char *)"pw_passwd";
+ example_pwd.pw_gecos = (char *)"pw_gecos";
+ example_pwd.pw_shell = (char *)"pw_shell";
+ example_pwd.pw_dir = (char *)"pw_dir";
+ example_pwd.pw_uid = (uid_t)1001;
+ example_pwd.pw_gid = (gid_t)101;
+
+ VERIFY_TRUE(asprintf(&data.tmp_dir, TEMP_PATH_TEMPLATE) >= 0);
+ VERIFY_NOT_NULL(mkdtemp(data.tmp_dir));
+
+ sudo_conf_clear_paths();
+
+ // some default values for the plugin open:
+ data.settings = create_str_array(1, NULL);
+ data.user_info = create_str_array(1, NULL);
+ data.command_info = create_str_array(1, NULL);
+ data.plugin_argc = 0;
+ data.plugin_argv = create_str_array(1, NULL);
+ data.user_env = create_str_array(1, NULL);
+
+ VERIFY_TRUE(_init_symbols());
+ return true;
+}
+
+static int
+cleanup(int success)
+{
+ if (!success) {
+ printf("\nThe output of the plugin:\n%s", data.stdout_str);
+ printf("\nThe error output of the plugin:\n%s", data.stderr_str);
+ }
+
+ VERIFY_TRUE(rmdir_recursive(data.tmp_dir));
+ if (data.tmp_dir2) {
+ VERIFY_TRUE(rmdir_recursive(data.tmp_dir2));
+ }
+
+ free(data.tmp_dir);
+ free(data.tmp_dir2);
+
+ str_array_free(&data.settings);
+ str_array_free(&data.user_info);
+ str_array_free(&data.command_info);
+ str_array_free(&data.plugin_argv);
+ str_array_free(&data.user_env);
+ str_array_free(&data.plugin_options);
+
+ return true;
+}
+
+static int
+check_example_io_plugin_version_display(int is_verbose)
+{
+ const char *errstr = NULL;
+ create_io_plugin_options(data.tmp_dir);
+
+ VERIFY_INT(python_io->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.command_info, data.plugin_argc, data.plugin_argv, data.user_env,
+ data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_INT(python_io->show_version(is_verbose), SUDO_RC_OK);
+
+ python_io->close(0, 0); // this should not call the python plugin close as there was no command run invocation
+
+ if (is_verbose) {
+ // Note: the exact python version is environment dependent
+ VERIFY_STR_CONTAINS(data.stdout_str, "Python interpreter version:");
+ *strstr(data.stdout_str, "Python interpreter version:") = '\0';
+ VERIFY_STDOUT(expected_path("check_example_io_plugin_version_display_full.stdout"));
+ } else {
+ VERIFY_STDOUT(expected_path("check_example_io_plugin_version_display.stdout"));
+ }
+
+ VERIFY_STDERR(expected_path("check_example_io_plugin_version_display.stderr"));
+ VERIFY_FILE("sudo.log", expected_path("check_example_io_plugin_version_display.stored"));
+
+ return true;
+}
+
+static int
+check_example_io_plugin_command_log(void)
+{
+ const char *errstr = NULL;
+ create_io_plugin_options(data.tmp_dir);
+
+ str_array_free(&data.plugin_argv);
+ data.plugin_argc = 2;
+ data.plugin_argv = create_str_array(3, "id", "--help", NULL);
+
+ str_array_free(&data.command_info);
+ data.command_info = create_str_array(3, "command=/bin/id", "runas_uid=0", NULL);
+
+ VERIFY_INT(python_io->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.command_info, data.plugin_argc, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io->log_stdin("some standard input", strlen("some standard input"), &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io->log_stdout("some standard output", strlen("some standard output"), &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io->log_stderr("some standard error", strlen("some standard error"), &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io->log_suspend(SIGTSTP, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io->log_suspend(SIGCONT, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io->change_winsize(200, 100, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io->log_ttyin("some tty input", strlen("some tty input"), &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io->log_ttyout("some tty output", strlen("some tty output"), &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ python_io->close(1, 0); // successful execution, command returned 1
+
+ VERIFY_STDOUT(expected_path("check_example_io_plugin_command_log.stdout"));
+ VERIFY_STDERR(expected_path("check_example_io_plugin_command_log.stderr"));
+ VERIFY_FILE("sudo.log", expected_path("check_example_io_plugin_command_log.stored"));
+
+ return true;
+}
+
+typedef struct io_plugin * (io_clone_func)(void);
+
+static int
+check_example_io_plugin_command_log_multiple(void)
+{
+ const char *errstr = NULL;
+
+ // verify multiple python io plugin symbols are available
+ io_clone_func *python_io_clone = (io_clone_func *)sudo_dso_findsym(python_plugin_handle, "python_io_clone");
+ VERIFY_PTR_NE(python_io_clone, NULL);
+
+ struct io_plugin *python_io2 = NULL;
+
+ for (int i = 0; i < 7; ++i) {
+ python_io2 = (*python_io_clone)();
+ VERIFY_PTR_NE(python_io2, NULL);
+ VERIFY_PTR_NE(python_io2, python_io);
+ }
+
+ // open the first plugin and let it log to tmp_dir
+ create_io_plugin_options(data.tmp_dir);
+
+ str_array_free(&data.plugin_argv);
+ data.plugin_argc = 2;
+ data.plugin_argv = create_str_array(3, "id", "--help", NULL);
+
+ str_array_free(&data.command_info);
+ data.command_info = create_str_array(3, "command=/bin/id", "runas_uid=0", NULL);
+
+ VERIFY_INT(python_io->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.command_info, data.plugin_argc, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ // For verifying the error message of no more plugin. It should be displayed only once.
+ VERIFY_PTR((*python_io_clone)(), NULL);
+ VERIFY_PTR((*python_io_clone)(), NULL);
+
+ // open the second plugin with another log directory
+ VERIFY_TRUE(asprintf(&data.tmp_dir2, TEMP_PATH_TEMPLATE) >= 0);
+ VERIFY_NOT_NULL(mkdtemp(data.tmp_dir2));
+ create_io_plugin_options(data.tmp_dir2);
+
+ str_array_free(&data.plugin_argv);
+ data.plugin_argc = 1;
+ data.plugin_argv = create_str_array(2, "whoami", NULL);
+
+ str_array_free(&data.command_info);
+ data.command_info = create_str_array(3, "command=/bin/whoami", "runas_uid=1", NULL);
+
+ VERIFY_INT(python_io2->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.command_info, data.plugin_argc, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ VERIFY_INT(python_io->log_stdin("stdin for plugin 1", strlen("stdin for plugin 1"), &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io2->log_stdin("stdin for plugin 2", strlen("stdin for plugin 2"), &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io->log_stdout("stdout for plugin 1", strlen("stdout for plugin 1"), &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io2->log_stdout("stdout for plugin 2", strlen("stdout for plugin 2"), &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io->log_stderr("stderr for plugin 1", strlen("stderr for plugin 1"), &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io2->log_stderr("stderr for plugin 2", strlen("stderr for plugin 2"), &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io->log_suspend(SIGTSTP, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io2->log_suspend(SIGSTOP, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io->log_suspend(SIGCONT, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io2->log_suspend(SIGCONT, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io->change_winsize(20, 10, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io2->change_winsize(30, 40, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io->log_ttyin("tty input for plugin 1", strlen("tty input for plugin 1"), &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io2->log_ttyin("tty input for plugin 2", strlen("tty input for plugin 2"), &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io->log_ttyout("tty output for plugin 1", strlen("tty output for plugin 1"), &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io2->log_ttyout("tty output for plugin 2", strlen("tty output for plugin 2"), &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ python_io->close(1, 0); // successful execution, command returned 1
+ python_io2->close(2, 0); // command returned 2
+
+ VERIFY_STDOUT(expected_path("check_example_io_plugin_command_log_multiple.stdout"));
+ VERIFY_STDERR(expected_path("check_example_io_plugin_command_log_multiple.stderr"));
+ VERIFY_FILE("sudo.log", expected_path("check_example_io_plugin_command_log_multiple1.stored"));
+ VERIFY_TRUE(verify_file(data.tmp_dir2, "sudo.log", expected_path("check_example_io_plugin_command_log_multiple2.stored")));
+
+ return true;
+}
+
+static int
+check_example_io_plugin_failed_to_start_command(void)
+{
+ const char *errstr = NULL;
+
+ create_io_plugin_options(data.tmp_dir);
+
+ str_array_free(&data.plugin_argv);
+ data.plugin_argc = 1;
+ data.plugin_argv = create_str_array(2, "cmd", NULL);
+
+ str_array_free(&data.command_info);
+ data.command_info = create_str_array(3, "command=/usr/share/cmd", "runas_uid=0", NULL);
+
+ VERIFY_INT(python_io->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.command_info, data.plugin_argc, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ python_io->close(0, EPERM); // execve returned with error
+
+ VERIFY_STDOUT(expected_path("check_example_io_plugin_failed_to_start_command.stdout"));
+ VERIFY_STDERR(expected_path("check_example_io_plugin_failed_to_start_command.stderr"));
+ VERIFY_FILE("sudo.log", expected_path("check_example_io_plugin_failed_to_start_command.stored"));
+
+ return true;
+}
+
+static int
+check_example_io_plugin_fails_with_python_backtrace(void)
+{
+ const char *errstr = NULL;
+
+ create_io_plugin_options("/some/not/writable/directory");
+
+ VERIFY_INT(python_io->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.command_info, data.plugin_argc, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_ERROR);
+ VERIFY_PTR(errstr, NULL);
+
+ VERIFY_STDOUT(expected_path("check_example_io_plugin_fails_with_python_backtrace.stdout"));
+ VERIFY_STDERR(expected_path("check_example_io_plugin_fails_with_python_backtrace.stderr"));
+
+ python_io->close(0, 0);
+ return true;
+}
+
+static int
+check_io_plugin_reports_error(void)
+{
+ const char *errstr = NULL;
+ str_array_free(&data.plugin_options);
+ data.plugin_options = create_str_array(
+ 3,
+ "ModulePath=" SRC_DIR "/regress/plugin_errorstr.py",
+ "ClassName=ConstructErrorPlugin",
+ NULL
+ );
+
+ VERIFY_INT(python_io->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.command_info, data.plugin_argc, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_ERROR);
+
+ VERIFY_STR(errstr, "Something wrong in plugin constructor");
+ errstr = NULL;
+
+ python_io->close(0, 0);
+
+ str_array_free(&data.plugin_options);
+ data.plugin_options = create_str_array(
+ 3,
+ "ModulePath=" SRC_DIR "/regress/plugin_errorstr.py",
+ "ClassName=ErrorMsgPlugin",
+ NULL
+ );
+
+ VERIFY_INT(python_io->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.command_info, data.plugin_argc, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ VERIFY_INT(python_io->log_stdin("", 0, &errstr), SUDO_RC_ERROR);
+ VERIFY_STR(errstr, "Something wrong in log_stdin");
+
+ errstr = (void *)13;
+ VERIFY_INT(python_io->log_stdout("", 0, &errstr), SUDO_RC_ERROR);
+ VERIFY_STR(errstr, "Something wrong in log_stdout");
+
+ errstr = NULL;
+ VERIFY_INT(python_io->log_stderr("", 0, &errstr), SUDO_RC_ERROR);
+ VERIFY_STR(errstr, "Something wrong in log_stderr");
+
+ errstr = NULL;
+ VERIFY_INT(python_io->log_ttyin("", 0, &errstr), SUDO_RC_ERROR);
+ VERIFY_STR(errstr, "Something wrong in log_ttyin");
+
+ errstr = NULL;
+ VERIFY_INT(python_io->log_ttyout("", 0, &errstr), SUDO_RC_ERROR);
+ VERIFY_STR(errstr, "Something wrong in log_ttyout");
+
+ errstr = NULL;
+ VERIFY_INT(python_io->log_suspend(SIGTSTP, &errstr), SUDO_RC_ERROR);
+ VERIFY_STR(errstr, "Something wrong in log_suspend");
+
+ errstr = NULL;
+ VERIFY_INT(python_io->change_winsize(200, 100, &errstr), SUDO_RC_ERROR);
+ VERIFY_STR(errstr, "Something wrong in change_winsize");
+
+ python_io->close(0, 0);
+
+ VERIFY_STR(data.stderr_str, "");
+ VERIFY_STR(data.stdout_str, "");
+ return true;
+}
+
+static int
+check_example_group_plugin(void)
+{
+ create_plugin_options("example_group_plugin", "SudoGroupPlugin", NULL);
+
+ VERIFY_INT(group_plugin->init(GROUP_API_VERSION, fake_printf, data.plugin_options), SUDO_RC_OK);
+
+ VERIFY_INT(group_plugin->query("test", "mygroup", NULL), SUDO_RC_OK);
+ VERIFY_INT(group_plugin->query("testuser2", "testgroup", NULL), SUDO_RC_OK);
+ VERIFY_INT(group_plugin->query("testuser2", "mygroup", NULL), SUDO_RC_REJECT);
+ VERIFY_INT(group_plugin->query("test", "testgroup", NULL), SUDO_RC_REJECT);
+
+ group_plugin->cleanup();
+ VERIFY_STR(data.stderr_str, "");
+ VERIFY_STR(data.stdout_str, "");
+ return true;
+}
+
+#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
+static const char *
+create_debug_config(const char *debug_spec)
+{
+ char *result = NULL;
+
+ static char config_path[PATH_MAX] = "/";
+ snprintf(config_path, sizeof(config_path), "%s/sudo.conf", data.tmp_dir);
+
+ char *content = NULL;
+ if (asprintf(&content, "Debug %s %s/debug.log %s\n",
+ "python_plugin.so", data.tmp_dir, debug_spec) < 0)
+ {
+ printf("Failed to allocate string\n");
+ goto cleanup;
+ }
+
+ if (fwriteall(config_path, content) != true) {
+ printf("Failed to write '%s'\n", config_path);
+ goto cleanup;
+ }
+
+ result = config_path;
+
+cleanup:
+ free(content);
+
+ return result;
+}
+
+static int
+check_example_group_plugin_is_able_to_debug(void)
+{
+ const char *config_path = create_debug_config("py_calls@diag");
+ VERIFY_NOT_NULL(config_path);
+ VERIFY_INT(sudo_conf_read(config_path, SUDO_CONF_ALL), true);
+
+ create_plugin_options("example_group_plugin", "SudoGroupPlugin", NULL);
+
+ group_plugin->init(GROUP_API_VERSION, fake_printf, data.plugin_options);
+
+ group_plugin->query("user", "group", &example_pwd);
+
+ group_plugin->cleanup();
+
+ VERIFY_STR(data.stderr_str, "");
+ VERIFY_STR(data.stdout_str, "");
+
+ VERIFY_LOG_LINES(expected_path("check_example_group_plugin_is_able_to_debug.log"));
+
+ return true;
+}
+#endif /* FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION */
+
+static int
+check_plugin_unload(void)
+{
+ // You can call this test to avoid having a lot of subinterpreters
+ // (each plugin->open starts one, and only plugin unlink closes)
+ // It only verifies that python was shut down correctly.
+ VERIFY_TRUE(Py_IsInitialized());
+ VERIFY_TRUE(_unlink_symbols());
+ VERIFY_FALSE(Py_IsInitialized()); // python interpreter could be stopped
+ return true;
+}
+
+#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
+static int
+check_example_debugging(const char *debug_spec)
+{
+ const char *errstr = NULL;
+ const char *config_path = create_debug_config(debug_spec);
+ VERIFY_NOT_NULL(config_path);
+ VERIFY_INT(sudo_conf_read(config_path, SUDO_CONF_ALL), true);
+
+ create_debugging_plugin_options();
+
+ str_array_free(&data.settings);
+ char *debug_flags_setting = NULL;
+ VERIFY_TRUE(asprintf(&debug_flags_setting, "debug_flags=%s/debug.log %s", data.tmp_dir, debug_spec) >= 0);
+
+ data.settings = create_str_array(3, debug_flags_setting, "plugin_path=python_plugin.so", NULL);
+
+ VERIFY_INT(python_io->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.command_info, data.plugin_argc, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ python_io->close(0, 0);
+
+ VERIFY_STR(data.stderr_str, "");
+ VERIFY_STR(data.stdout_str, "");
+
+ VERIFY_LOG_LINES(expected_path("check_example_debugging_%s.log", debug_spec));
+
+ free(debug_flags_setting);
+ return true;
+}
+#endif /* FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION */
+
+static int
+check_loading_fails(const char *name)
+{
+ const char *errstr = NULL;
+
+ VERIFY_INT(python_io->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.command_info, data.plugin_argc, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_ERROR);
+ VERIFY_PTR(errstr, NULL);
+ python_io->close(0, 0);
+
+ VERIFY_STDOUT(expected_path("check_loading_fails_%s.stdout", name));
+ VERIFY_STDERR(expected_path("check_loading_fails_%s.stderr", name));
+
+ return true;
+}
+
+static int
+check_loading_fails_with_missing_path(void)
+{
+ str_array_free(&data.plugin_options);
+ data.plugin_options = create_str_array(2, "ClassName=DebugDemoPlugin", NULL);
+ return check_loading_fails("missing_path");
+}
+
+static int
+check_loading_succeeds_with_missing_classname(void)
+{
+ str_array_free(&data.plugin_options);
+ data.plugin_options = create_str_array(2, "ModulePath=" SRC_DIR "/example_debugging.py", NULL);
+
+ const char *errstr = NULL;
+
+ VERIFY_INT(python_io->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.command_info, data.plugin_argc, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_io->show_version(1), SUDO_RC_OK);
+ python_io->close(0, 0);
+
+ VERIFY_STDOUT(expected_path("check_loading_succeeds_with_missing_classname.stdout"));
+ VERIFY_STR(data.stderr_str, "");
+
+ return true;
+}
+
+static int
+check_loading_fails_with_missing_classname(void)
+{
+ str_array_free(&data.plugin_options);
+ data.plugin_options = create_str_array(2, "ModulePath=" SRC_DIR "/regress/plugin_errorstr.py", NULL);
+ return check_loading_fails("missing_classname");
+}
+
+static int
+check_loading_fails_with_wrong_classname(void)
+{
+ create_plugin_options("example_debugging", "MispelledPluginName", NULL);
+ return check_loading_fails("wrong_classname");
+}
+
+static int
+check_loading_fails_with_wrong_path(void)
+{
+ str_array_free(&data.plugin_options);
+ data.plugin_options = create_str_array(3, "ModulePath=/wrong_path.py", "ClassName=PluginName", NULL);
+ return check_loading_fails("wrong_path");
+}
+
+static int
+check_example_conversation_plugin_reason_log(int simulate_suspend, const char *description)
+{
+ const char *errstr = NULL;
+
+ create_conversation_plugin_options();
+
+ str_array_free(&data.plugin_argv); // have a command run
+ data.plugin_argc = 1;
+ data.plugin_argv = create_str_array(2, "/bin/whoami", NULL);
+
+ data.conv_replies[0] = "my fake reason";
+ data.conv_replies[1] = "my real secret reason";
+
+ sudo_conv_t conversation = simulate_suspend ? fake_conversation_with_suspend : fake_conversation;
+
+ VERIFY_INT(python_io->open(SUDO_API_VERSION, conversation, fake_printf, data.settings,
+ data.user_info, data.command_info, data.plugin_argc, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ python_io->close(0, 0);
+
+ VERIFY_STDOUT(expected_path("check_example_conversation_plugin_reason_log_%s.stdout", description));
+ VERIFY_STDERR(expected_path("check_example_conversation_plugin_reason_log_%s.stderr", description));
+ VERIFY_CONV(expected_path("check_example_conversation_plugin_reason_log_%s.conversation", description));
+ VERIFY_FILE("sudo_reasons.txt", expected_path("check_example_conversation_plugin_reason_log_%s.stored", description));
+ return true;
+}
+
+static int
+check_example_conversation_plugin_user_interrupts(void)
+{
+ const char *errstr = NULL;
+
+ create_conversation_plugin_options();
+
+ str_array_free(&data.plugin_argv); // have a command run
+ data.plugin_argc = 1;
+ data.plugin_argv = create_str_array(2, "/bin/whoami", NULL);
+
+ data.conv_replies[0] = NULL; // this simulates user interrupt for the first question
+
+ VERIFY_INT(python_io->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.command_info, data.plugin_argc, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_REJECT);
+ VERIFY_PTR(errstr, NULL);
+ python_io->close(0, 0);
+
+ VERIFY_STDOUT(expected_path("check_example_conversation_plugin_user_interrupts.stdout"));
+ VERIFY_STDERR(expected_path("check_example_conversation_plugin_user_interrupts.stderr"));
+ VERIFY_CONV(expected_path("check_example_conversation_plugin_user_interrupts.conversation"));
+ return true;
+}
+
+static int
+check_example_policy_plugin_version_display(int is_verbose)
+{
+ const char *errstr = NULL;
+
+ create_policy_plugin_options();
+
+ VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.user_env, data.plugin_options, &errstr),
+ SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_INT(python_policy->show_version(is_verbose), SUDO_RC_OK);
+
+ python_policy->close(0, 0); // this should not call the python plugin close as there was no command run invocation
+
+ if (is_verbose) {
+ // Note: the exact python version is environment dependent
+ VERIFY_STR_CONTAINS(data.stdout_str, "Python interpreter version:");
+ *strstr(data.stdout_str, "Python interpreter version:") = '\0';
+ VERIFY_STDOUT(expected_path("check_example_policy_plugin_version_display_full.stdout"));
+ } else {
+ VERIFY_STDOUT(expected_path("check_example_policy_plugin_version_display.stdout"));
+ }
+
+ VERIFY_STDERR(expected_path("check_example_policy_plugin_version_display.stderr"));
+
+ return true;
+}
+
+static int
+check_example_policy_plugin_accepted_execution(void)
+{
+ const char *errstr = NULL;
+
+ create_policy_plugin_options();
+
+ str_array_free(&data.plugin_argv);
+ data.plugin_argc = 2;
+ data.plugin_argv = create_str_array(3, "/bin/whoami", "--help", NULL);
+
+ str_array_free(&data.user_env);
+ data.user_env = create_str_array(3, "USER_ENV1=VALUE1", "USER_ENV2=value2", NULL);
+
+ VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.user_env, data.plugin_options, &errstr),
+ SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ char **env_add = create_str_array(3, "REQUESTED_ENV1=VALUE1", "REQUESTED_ENV2=value2", NULL);
+
+ char **argv_out, **user_env_out, **command_info_out; // free to contain garbage
+
+ VERIFY_INT(python_policy->check_policy(data.plugin_argc, data.plugin_argv, env_add,
+ &command_info_out, &argv_out, &user_env_out, &errstr),
+ SUDO_RC_ACCEPT);
+ VERIFY_PTR(errstr, NULL);
+
+ VERIFY_STR_SET(command_info_out, 4, "command=/bin/whoami", "runas_uid=0", "runas_gid=0", NULL);
+ VERIFY_STR_SET(user_env_out, 5, "USER_ENV1=VALUE1", "USER_ENV2=value2",
+ "REQUESTED_ENV1=VALUE1", "REQUESTED_ENV2=value2", NULL);
+ VERIFY_STR_SET(argv_out, 3, "/bin/whoami", "--help", NULL);
+
+ VERIFY_INT(python_policy->init_session(&example_pwd, &user_env_out, &errstr), SUDO_RC_ACCEPT);
+ VERIFY_PTR(errstr, NULL);
+
+ // init session is able to modify the user env:
+ VERIFY_STR_SET(user_env_out, 6, "USER_ENV1=VALUE1", "USER_ENV2=value2",
+ "REQUESTED_ENV1=VALUE1", "REQUESTED_ENV2=value2", "PLUGIN_EXAMPLE_ENV=1", NULL);
+
+ python_policy->close(3, 0); // successful execution returned exit code 3
+
+ VERIFY_STDOUT(expected_path("check_example_policy_plugin_accepted_execution.stdout"));
+ VERIFY_STDERR(expected_path("check_example_policy_plugin_accepted_execution.stderr"));
+
+ str_array_free(&env_add);
+ str_array_free(&user_env_out);
+ str_array_free(&command_info_out);
+ str_array_free(&argv_out);
+ return true;
+}
+
+static int
+check_example_policy_plugin_failed_execution(void)
+{
+ const char *errstr = NULL;
+
+ create_policy_plugin_options();
+
+ str_array_free(&data.plugin_argv);
+ data.plugin_argc = 2;
+ data.plugin_argv = create_str_array(3, "/bin/id", "--help", NULL);
+
+ VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.user_env, data.plugin_options, &errstr),
+ SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ char **argv_out, **user_env_out, **command_info_out; // free to contain garbage
+
+ VERIFY_INT(python_policy->check_policy(data.plugin_argc, data.plugin_argv, NULL,
+ &command_info_out, &argv_out, &user_env_out, &errstr),
+ SUDO_RC_ACCEPT);
+ VERIFY_PTR(errstr, NULL);
+
+ // pwd is unset (user is not part of /etc/passwd)
+ VERIFY_INT(python_policy->init_session(NULL, &user_env_out, &errstr), SUDO_RC_ACCEPT);
+ VERIFY_PTR(errstr, NULL);
+
+ python_policy->close(12345, ENOENT); // failed to execute
+
+ VERIFY_STDOUT(expected_path("check_example_policy_plugin_failed_execution.stdout"));
+ VERIFY_STDERR(expected_path("check_example_policy_plugin_failed_execution.stderr"));
+
+ str_array_free(&user_env_out);
+ str_array_free(&command_info_out);
+ str_array_free(&argv_out);
+ return true;
+}
+
+static int
+check_example_policy_plugin_denied_execution(void)
+{
+ const char *errstr = NULL;
+
+ create_policy_plugin_options();
+
+ str_array_free(&data.plugin_argv);
+ data.plugin_argc = 1;
+ data.plugin_argv = create_str_array(2, "/bin/passwd", NULL);
+
+ VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.user_env, data.plugin_options, &errstr),
+ SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ char **argv_out, **user_env_out, **command_info_out; // free to contain garbage
+
+ VERIFY_INT(python_policy->check_policy(data.plugin_argc, data.plugin_argv, NULL,
+ &command_info_out, &argv_out, &user_env_out, &errstr),
+ SUDO_RC_REJECT);
+ VERIFY_PTR(errstr, NULL);
+
+ VERIFY_PTR(command_info_out, NULL);
+ VERIFY_PTR(argv_out, NULL);
+ VERIFY_PTR(user_env_out, NULL);
+
+ python_policy->close(0, 0); // there was no execution
+
+ VERIFY_STDOUT(expected_path("check_example_policy_plugin_denied_execution.stdout"));
+ VERIFY_STDERR(expected_path("check_example_policy_plugin_denied_execution.stderr"));
+
+ return true;
+}
+
+static int
+check_example_policy_plugin_list(void)
+{
+ const char *errstr = NULL;
+
+ create_policy_plugin_options();
+
+ VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.user_env, data.plugin_options, &errstr),
+ SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ snprintf_append(data.stdout_str, MAX_OUTPUT, "-- minimal --\n");
+ VERIFY_INT(python_policy->list(data.plugin_argc, data.plugin_argv, false, NULL, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ snprintf_append(data.stdout_str, MAX_OUTPUT, "\n-- minimal (verbose) --\n");
+ VERIFY_INT(python_policy->list(data.plugin_argc, data.plugin_argv, true, NULL, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ snprintf_append(data.stdout_str, MAX_OUTPUT, "\n-- with user --\n");
+ VERIFY_INT(python_policy->list(data.plugin_argc, data.plugin_argv, false, "testuser", &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ snprintf_append(data.stdout_str, MAX_OUTPUT, "\n-- with user (verbose) --\n");
+ VERIFY_INT(python_policy->list(data.plugin_argc, data.plugin_argv, true, "testuser", &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ snprintf_append(data.stdout_str, MAX_OUTPUT, "\n-- with allowed program --\n");
+ str_array_free(&data.plugin_argv);
+ data.plugin_argc = 3;
+ data.plugin_argv = create_str_array(4, "/bin/id", "some", "arguments", NULL);
+ VERIFY_INT(python_policy->list(data.plugin_argc, data.plugin_argv, false, NULL, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ snprintf_append(data.stdout_str, MAX_OUTPUT, "\n-- with allowed program (verbose) --\n");
+ VERIFY_INT(python_policy->list(data.plugin_argc, data.plugin_argv, true, NULL, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ snprintf_append(data.stdout_str, MAX_OUTPUT, "\n-- with denied program --\n");
+ str_array_free(&data.plugin_argv);
+ data.plugin_argc = 1;
+ data.plugin_argv = create_str_array(2, "/bin/passwd", NULL);
+ VERIFY_INT(python_policy->list(data.plugin_argc, data.plugin_argv, false, NULL, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ snprintf_append(data.stdout_str, MAX_OUTPUT, "\n-- with denied program (verbose) --\n");
+ VERIFY_INT(python_policy->list(data.plugin_argc, data.plugin_argv, true, NULL, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ python_policy->close(0, 0); // there was no execution
+
+ VERIFY_STDOUT(expected_path("check_example_policy_plugin_list.stdout"));
+ VERIFY_STDERR(expected_path("check_example_policy_plugin_list.stderr"));
+
+ return true;
+}
+
+static int
+check_example_policy_plugin_validate_invalidate(void)
+{
+ const char *errstr = NULL;
+
+#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
+ // the plugin does not do any meaningful for these, so using log to validate instead
+ const char *config_path = create_debug_config("py_calls@diag");
+ VERIFY_NOT_NULL(config_path);
+ VERIFY_INT(sudo_conf_read(config_path, SUDO_CONF_ALL), true);
+#endif
+
+ create_policy_plugin_options();
+
+ VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.user_env, data.plugin_options, &errstr),
+ SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ VERIFY_INT(python_policy->validate(&errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ python_policy->invalidate(true);
+ python_policy->invalidate(false);
+
+ python_policy->close(0, 0); // no command execution
+
+#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
+ VERIFY_LOG_LINES(expected_path("check_example_policy_plugin_validate_invalidate.log"));
+#endif
+ VERIFY_STR(data.stderr_str, "");
+ VERIFY_STR(data.stdout_str, "");
+ return true;
+}
+
+static int
+check_policy_plugin_callbacks_are_optional(void)
+{
+ const char *errstr = NULL;
+
+ create_debugging_plugin_options();
+
+ VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.user_env, data.plugin_options, &errstr),
+ SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ VERIFY_PTR(python_policy->list, NULL);
+ VERIFY_PTR(python_policy->validate, NULL);
+ VERIFY_PTR(python_policy->invalidate, NULL);
+ VERIFY_PTR_NE(python_policy->check_policy, NULL); // (not optional)
+ VERIFY_PTR(python_policy->init_session, NULL);
+
+ // show_version always displays the plugin, but it is optional in the python layer
+ VERIFY_PTR_NE(python_policy->show_version, NULL);
+ VERIFY_INT(python_policy->show_version(1), SUDO_RC_OK);
+
+ python_policy->close(0, 0);
+ return true;
+}
+
+static int
+check_policy_plugin_reports_error(void)
+{
+ const char *errstr = NULL;
+ str_array_free(&data.plugin_options);
+ data.plugin_options = create_str_array(
+ 3,
+ "ModulePath=" SRC_DIR "/regress/plugin_errorstr.py",
+ "ClassName=ConstructErrorPlugin",
+ NULL
+ );
+
+ VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.user_env, data.plugin_options, &errstr), SUDO_RC_ERROR);
+ VERIFY_STR(errstr, "Something wrong in plugin constructor");
+ errstr = NULL;
+
+ python_policy->close(0, 0);
+
+ str_array_free(&data.plugin_options);
+ data.plugin_options = create_str_array(
+ 3,
+ "ModulePath=" SRC_DIR "/regress/plugin_errorstr.py",
+ "ClassName=ErrorMsgPlugin",
+ NULL
+ );
+
+ data.plugin_argc = 1;
+ str_array_free(&data.plugin_argv);
+ data.plugin_argv = create_str_array(2, "id", NULL);
+
+ VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ char **command_info_out = NULL;
+ char **argv_out = NULL;
+ char **user_env_out = NULL;
+
+ VERIFY_INT(python_policy->list(data.plugin_argc, data.plugin_argv, true, NULL, &errstr), SUDO_RC_ERROR);
+ VERIFY_STR(errstr, "Something wrong in list");
+
+ errstr = NULL;
+ VERIFY_INT(python_policy->validate(&errstr), SUDO_RC_ERROR);
+ VERIFY_STR(errstr, "Something wrong in validate");
+
+ errstr = NULL;
+ VERIFY_INT(python_policy->check_policy(data.plugin_argc, data.plugin_argv, data.user_env,
+ &command_info_out, &argv_out, &user_env_out, &errstr),
+ SUDO_RC_ERROR);
+ VERIFY_STR(errstr, "Something wrong in check_policy");
+
+ errstr = NULL;
+ VERIFY_INT(python_policy->init_session(&example_pwd, &user_env_out, &errstr), SUDO_RC_ERROR);
+ VERIFY_STR(errstr, "Something wrong in init_session");
+
+ python_policy->close(0, 0);
+
+ VERIFY_STR(data.stderr_str, "");
+ VERIFY_STR(data.stdout_str, "");
+ return true;
+}
+
+static int
+check_io_plugin_callbacks_are_optional(void)
+{
+ const char *errstr = NULL;
+
+ create_debugging_plugin_options();
+
+ VERIFY_INT(python_io->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.command_info, data.plugin_argc, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ VERIFY_PTR(python_io->log_stdin, NULL);
+ VERIFY_PTR(python_io->log_stdout, NULL);
+ VERIFY_PTR(python_io->log_stderr, NULL);
+ VERIFY_PTR(python_io->log_ttyin, NULL);
+ VERIFY_PTR(python_io->log_ttyout, NULL);
+ VERIFY_PTR(python_io->change_winsize, NULL);
+
+ // show_version always displays the plugin, but it is optional in the python layer
+ VERIFY_PTR_NE(python_io->show_version, NULL);
+ VERIFY_INT(python_io->show_version(1), SUDO_RC_OK);
+
+ python_io->close(0, 0);
+ return true;
+}
+
+static int
+check_python_plugins_do_not_affect_each_other(void)
+{
+ const char *errstr = NULL;
+
+ // We test here that one plugin is not able to effect the environment of another
+ // This is important so they do not ruin or depend on each other's state.
+ create_plugin_options("regress/plugin_conflict", "ConflictPlugin", "Path=path_for_first_plugin");
+
+ VERIFY_INT(python_io->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.command_info, data.plugin_argc, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ create_plugin_options("regress/plugin_conflict", "ConflictPlugin", "Path=path_for_second_plugin");
+ VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings,
+ data.user_info, data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ python_io->close(0, 0);
+ python_policy->close(0, 0);
+
+ VERIFY_STDOUT(expected_path("check_python_plugins_do_not_affect_each_other.stdout"));
+ VERIFY_STR(data.stderr_str, "");
+ return true;
+}
+
+static int
+check_example_audit_plugin_receives_accept(void)
+{
+ create_audit_plugin_options("");
+ const char *errstr = NULL;
+
+ str_array_free(&data.plugin_argv);
+ data.plugin_argv = create_str_array(6, "sudo", "-u", "user", "id", "--help", NULL);
+
+ str_array_free(&data.user_env);
+ data.user_env = create_str_array(3, "KEY1=VALUE1", "KEY2=VALUE2", NULL);
+
+ str_array_free(&data.user_info);
+ data.user_info = create_str_array(3, "user=testuser1", "uid=123", NULL);
+
+ VERIFY_INT(python_audit->open(SUDO_API_VERSION, fake_conversation, fake_printf,
+ data.settings, data.user_info, 3, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ str_array_free(&data.command_info);
+ data.command_info = create_str_array(2, "command=/sbin/id", NULL);
+
+ str_array_free(&data.plugin_argv);
+ data.plugin_argv = create_str_array(3, "id", "--help", NULL);
+
+ VERIFY_INT(python_audit->accept("accepter plugin name", SUDO_POLICY_PLUGIN,
+ data.command_info, data.plugin_argv,
+ data.user_env, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ python_audit->close(SUDO_PLUGIN_WAIT_STATUS, W_EXITCODE(2, 0)); // process exited with 2
+
+ VERIFY_STDOUT(expected_path("check_example_audit_plugin_receives_accept.stdout"));
+ VERIFY_STR(data.stderr_str, "");
+
+ return true;
+}
+
+static int
+check_example_audit_plugin_receives_reject(void)
+{
+ create_audit_plugin_options(NULL);
+ const char *errstr = NULL;
+
+ str_array_free(&data.plugin_argv);
+ data.plugin_argv = create_str_array(3, "sudo", "passwd", NULL);
+
+ str_array_free(&data.user_info);
+ data.user_info = create_str_array(3, "user=root", "uid=0", NULL);
+
+ VERIFY_INT(python_audit->open(SUDO_API_VERSION, fake_conversation, fake_printf,
+ data.settings, data.user_info, 1, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ VERIFY_INT(python_audit->reject("rejecter plugin name", SUDO_IO_PLUGIN,
+ "Rejected just because!", data.command_info,
+ &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ python_audit->close(SUDO_PLUGIN_NO_STATUS, 0); // program was not run
+
+ VERIFY_STDOUT(expected_path("check_example_audit_plugin_receives_reject.stdout"));
+ VERIFY_STR(data.stderr_str, "");
+
+ return true;
+}
+
+static int
+check_example_audit_plugin_receives_error(void)
+{
+ create_audit_plugin_options("");
+ const char *errstr = NULL;
+
+ str_array_free(&data.plugin_argv);
+ data.plugin_argv = create_str_array(5, "sudo", "-u", "user", "id", NULL);
+
+ VERIFY_INT(python_audit->open(SUDO_API_VERSION, fake_conversation, fake_printf,
+ data.settings, data.user_info, 3, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ str_array_free(&data.command_info);
+ data.command_info = create_str_array(2, "command=/sbin/id", NULL);
+
+ VERIFY_INT(python_audit->error("errorer plugin name", SUDO_AUDIT_PLUGIN,
+ "Some error has happened", data.command_info,
+ &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ python_audit->close(SUDO_PLUGIN_SUDO_ERROR, 222);
+
+ VERIFY_STDOUT(expected_path("check_example_audit_plugin_receives_error.stdout"));
+ VERIFY_STR(data.stderr_str, "");
+
+ return true;
+}
+
+typedef struct audit_plugin * (audit_clone_func)(void);
+
+static int
+check_example_audit_plugin_workflow_multiple(void)
+{
+ // verify multiple python audit plugins are available
+ audit_clone_func *python_audit_clone = (audit_clone_func *)sudo_dso_findsym(
+ python_plugin_handle, "python_audit_clone");
+ VERIFY_PTR_NE(python_audit_clone, NULL);
+
+ struct audit_plugin *python_audit2 = NULL;
+
+ for (int i = 0; i < 7; ++i) {
+ python_audit2 = (*python_audit_clone)();
+ VERIFY_PTR_NE(python_audit2, NULL);
+ VERIFY_PTR_NE(python_audit2, python_audit);
+ }
+
+ const char *errstr = NULL;
+
+ str_array_free(&data.plugin_argv);
+ data.plugin_argv = create_str_array(6, "sudo", "-u", "user", "id", "--help", NULL);
+
+ str_array_free(&data.user_env);
+ data.user_env = create_str_array(3, "KEY1=VALUE1", "KEY2=VALUE2", NULL);
+
+ str_array_free(&data.user_info);
+ data.user_info = create_str_array(3, "user=default", "uid=1000", NULL);
+
+ create_audit_plugin_options("Id=1");
+ VERIFY_INT(python_audit->open(SUDO_API_VERSION, fake_conversation, fake_printf,
+ data.settings, data.user_info, 3, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ // For verifying the error message of no more plugin. It should be displayed only once.
+ VERIFY_PTR((*python_audit_clone)(), NULL);
+ VERIFY_PTR((*python_audit_clone)(), NULL);
+
+ create_audit_plugin_options("Id=2");
+ VERIFY_INT(python_audit2->open(SUDO_API_VERSION, fake_conversation, fake_printf,
+ data.settings, data.user_info, 3, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ str_array_free(&data.command_info);
+ data.command_info = create_str_array(2, "command=/sbin/id", NULL);
+
+ str_array_free(&data.plugin_argv);
+ data.plugin_argv = create_str_array(3, "id", "--help", NULL);
+
+ VERIFY_INT(python_audit->accept("accepter plugin name", SUDO_POLICY_PLUGIN,
+ data.command_info, data.plugin_argv,
+ data.user_env, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ VERIFY_INT(python_audit2->accept("accepter plugin name", SUDO_POLICY_PLUGIN,
+ data.command_info, data.plugin_argv,
+ data.user_env, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ python_audit->close(SUDO_PLUGIN_WAIT_STATUS, W_EXITCODE(0, 11)); // process got signal 11
+ python_audit2->close(SUDO_PLUGIN_WAIT_STATUS, W_EXITCODE(0, 11));
+
+ VERIFY_STDOUT(expected_path("check_example_audit_plugin_workflow_multiple.stdout"));
+ VERIFY_STDERR(expected_path("check_example_audit_plugin_workflow_multiple.stderr"));
+
+ return true;
+}
+
+static int
+check_example_audit_plugin_version_display(void)
+{
+ create_audit_plugin_options(NULL);
+ const char *errstr = NULL;
+
+ str_array_free(&data.user_info);
+ data.user_info = create_str_array(3, "user=root", "uid=0", NULL);
+
+ str_array_free(&data.plugin_argv);
+ data.plugin_argv = create_str_array(3, "sudo", "-V", NULL);
+
+ VERIFY_INT(python_audit->open(SUDO_API_VERSION, fake_conversation, fake_printf,
+ data.settings, data.user_info, 2, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ VERIFY_INT(python_audit->show_version(false), SUDO_RC_OK);
+ VERIFY_INT(python_audit->show_version(true), SUDO_RC_OK);
+
+ python_audit->close(SUDO_PLUGIN_SUDO_ERROR, 222);
+
+ VERIFY_STDOUT(expected_path("check_example_audit_plugin_version_display.stdout"));
+ VERIFY_STR(data.stderr_str, "");
+
+ return true;
+}
+
+static int
+check_audit_plugin_callbacks_are_optional(void)
+{
+ const char *errstr = NULL;
+
+ create_debugging_plugin_options();
+
+ VERIFY_INT(python_audit->open(SUDO_API_VERSION, fake_conversation, fake_printf,
+ data.settings, data.user_info, 2, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr),
+ SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ VERIFY_PTR(python_audit->accept, NULL);
+ VERIFY_PTR(python_audit->reject, NULL);
+ VERIFY_PTR(python_audit->error, NULL);
+
+ // show_version always displays the plugin, but it is optional in the python layer
+ VERIFY_PTR_NE(python_audit->show_version, NULL);
+ VERIFY_INT(python_audit->show_version(1), SUDO_RC_OK);
+
+ python_audit->close(SUDO_PLUGIN_NO_STATUS, 0);
+ return true;
+}
+
+static int
+check_audit_plugin_reports_error(void)
+{
+ const char *errstr = NULL;
+ create_plugin_options("regress/plugin_errorstr", "ConstructErrorPlugin", NULL);
+
+ VERIFY_INT(python_audit->open(SUDO_API_VERSION, fake_conversation, fake_printf,
+ data.settings, data.user_info, 0, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_ERROR);
+
+ VERIFY_STR(errstr, "Something wrong in plugin constructor");
+ errstr = NULL;
+
+ python_audit->close(SUDO_PLUGIN_NO_STATUS, 0);
+
+ create_plugin_options("regress/plugin_errorstr", "ErrorMsgPlugin", NULL);
+
+ VERIFY_INT(python_audit->open(SUDO_API_VERSION, fake_conversation, fake_printf,
+ data.settings, data.user_info, 0, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_ERROR);
+ VERIFY_STR(errstr, "Something wrong in open");
+
+ errstr = NULL;
+ VERIFY_INT(python_audit->accept("plugin name", SUDO_POLICY_PLUGIN,
+ data.command_info, data.plugin_argv,
+ data.user_env, &errstr), SUDO_RC_ERROR);
+ VERIFY_STR(errstr, "Something wrong in accept");
+
+ errstr = NULL;
+ VERIFY_INT(python_audit->reject("plugin name", SUDO_POLICY_PLUGIN,
+ "audit message", data.command_info,
+ &errstr), SUDO_RC_ERROR);
+ VERIFY_STR(errstr, "Something wrong in reject");
+
+ errstr = NULL;
+ VERIFY_INT(python_audit->error("plugin name", SUDO_POLICY_PLUGIN,
+ "audit message", data.command_info,
+ &errstr), SUDO_RC_ERROR);
+ VERIFY_STR(errstr, "Something wrong in error");
+
+ python_audit->close(SUDO_PLUGIN_NO_STATUS, 0);
+
+ VERIFY_STR(data.stderr_str, "");
+ VERIFY_STR(data.stdout_str, "");
+ return true;
+}
+
+static int
+check_example_approval_plugin(const char *date_str, const char *expected_error)
+{
+ const char *errstr = NULL;
+
+ create_plugin_options("example_approval_plugin", "BusinessHoursApprovalPlugin", NULL);
+
+ VERIFY_INT(python_approval->open(SUDO_API_VERSION, fake_conversation, fake_printf,
+ data.settings, data.user_info, 0, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+
+ VERIFY_TRUE(mock_python_datetime_now("example_approval_plugin", date_str));
+
+ int expected_rc = (expected_error == NULL) ? SUDO_RC_ACCEPT : SUDO_RC_REJECT;
+
+ VERIFY_INT(python_approval->check(data.command_info, data.plugin_argv, data.user_env, &errstr),
+ expected_rc);
+
+ if (expected_error == NULL) {
+ VERIFY_PTR(errstr, NULL);
+ VERIFY_STR(data.stdout_str, "");
+ } else {
+ VERIFY_STR(errstr, expected_error);
+ VERIFY_STR_CONTAINS(data.stdout_str, expected_error); // (ends with \n)
+ }
+ VERIFY_STR(data.stderr_str, "");
+
+ python_approval->close();
+
+ return true;
+}
+
+typedef struct approval_plugin * (approval_clone_func)(void);
+
+static int
+check_multiple_approval_plugin_and_arguments(void)
+{
+ // verify multiple python approval plugins are available
+ approval_clone_func *python_approval_clone = (approval_clone_func *)sudo_dso_findsym(
+ python_plugin_handle, "python_approval_clone");
+ VERIFY_PTR_NE(python_approval_clone, NULL);
+
+ struct approval_plugin *python_approval2 = NULL;
+
+ for (int i = 0; i < 7; ++i) {
+ python_approval2 = (*python_approval_clone)();
+ VERIFY_PTR_NE(python_approval2, NULL);
+ VERIFY_PTR_NE(python_approval2, python_approval);
+ }
+
+ const char *errstr = NULL;
+ create_plugin_options("regress/plugin_approval_test", "ApprovalTestPlugin", "Id=1");
+
+ str_array_free(&data.plugin_argv);
+ data.plugin_argv = create_str_array(6, "sudo", "-u", "user", "whoami", "--help", NULL);
+
+ str_array_free(&data.user_env);
+ data.user_env = create_str_array(3, "USER_ENV1=VALUE1", "USER_ENV2=value2", NULL);
+
+ str_array_free(&data.user_info);
+ data.user_info = create_str_array(3, "INFO1=VALUE1", "info2=value2", NULL);
+
+ str_array_free(&data.settings);
+ data.settings = create_str_array(3, "SETTING1=VALUE1", "setting2=value2", NULL);
+
+ VERIFY_INT(python_approval->open(SUDO_API_VERSION, fake_conversation, fake_printf,
+ data.settings, data.user_info, 3, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ // For verifying the error message of no more plugin. It should be displayed only once.
+ VERIFY_PTR((*python_approval_clone)(), NULL);
+ VERIFY_PTR((*python_approval_clone)(), NULL);
+
+ create_plugin_options("regress/plugin_approval_test", "ApprovalTestPlugin", "Id=2");
+ VERIFY_INT(python_approval2->open(SUDO_API_VERSION, fake_conversation, fake_printf,
+ data.settings, data.user_info, 3, data.plugin_argv,
+ data.user_env, data.plugin_options, &errstr), SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ VERIFY_INT(python_approval->show_version(false), SUDO_RC_OK);
+ VERIFY_INT(python_approval2->show_version(true), SUDO_RC_OK);
+
+ str_array_free(&data.command_info);
+ data.command_info = create_str_array(3, "CMDINFO1=value1", "CMDINFO2=VALUE2", NULL);
+
+ str_array_free(&data.plugin_argv);
+ data.plugin_argv = create_str_array(3, "whoami", "--help", NULL);
+
+ VERIFY_INT(python_approval->check(data.command_info, data.plugin_argv, data.user_env, &errstr),
+ SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ VERIFY_INT(python_approval2->check(data.command_info, data.plugin_argv, data.user_env, &errstr),
+ SUDO_RC_OK);
+ VERIFY_PTR(errstr, NULL);
+
+ python_approval->close();
+ python_approval2->close();
+
+ VERIFY_STDOUT(expected_path("check_multiple_approval_plugin_and_arguments.stdout"));
+ VERIFY_STDERR(expected_path("check_multiple_approval_plugin_and_arguments.stderr"));
+
+ return true;
+}
+
+
+static int
+_init_symbols(void)
+{
+ if (python_plugin_handle != NULL) {
+ // symbols are already loaded, we just restore
+ RESTORE_PYTHON_PLUGIN(python_io);
+ RESTORE_PYTHON_PLUGIN(python_policy);
+ RESTORE_PYTHON_PLUGIN(python_approval);
+ RESTORE_PYTHON_PLUGIN(python_audit);
+ RESTORE_PYTHON_PLUGIN(group_plugin);
+ return true;
+ }
+
+ // we load the symbols
+ python_plugin_handle = sudo_dso_load(python_plugin_so_path, SUDO_DSO_LAZY|SUDO_DSO_GLOBAL);
+ VERIFY_PTR_NE(python_plugin_handle, NULL);
+
+ python_io = sudo_dso_findsym(python_plugin_handle, "python_io");
+ VERIFY_PTR_NE(python_io, NULL);
+
+ group_plugin = sudo_dso_findsym(python_plugin_handle, "group_plugin");
+ VERIFY_PTR_NE(group_plugin, NULL);
+
+ python_policy = sudo_dso_findsym(python_plugin_handle, "python_policy");
+ VERIFY_PTR_NE(python_policy, NULL);
+
+ python_audit = sudo_dso_findsym(python_plugin_handle, "python_audit");
+ VERIFY_PTR_NE(python_audit, NULL);
+
+ python_approval = sudo_dso_findsym(python_plugin_handle, "python_approval");
+ VERIFY_PTR_NE(python_approval, NULL);
+
+ SAVE_PYTHON_PLUGIN(python_io);
+ SAVE_PYTHON_PLUGIN(python_policy);
+ SAVE_PYTHON_PLUGIN(python_approval);
+ SAVE_PYTHON_PLUGIN(python_audit);
+ SAVE_PYTHON_PLUGIN(group_plugin);
+
+ return true;
+}
+
+static int
+_unlink_symbols(void)
+{
+ python_io = NULL;
+ group_plugin = NULL;
+ python_policy = NULL;
+ python_approval = NULL;
+ python_audit = NULL;
+ VERIFY_INT(sudo_dso_unload(python_plugin_handle), 0);
+ python_plugin_handle = NULL;
+ VERIFY_FALSE(Py_IsInitialized());
+ return true;
+}
+
+int
+main(int argc, char *argv[])
+{
+ int ch, errors = 0, ntests = 0;
+
+ while ((ch = getopt(argc, argv, "v")) != -1) {
+ switch (ch) {
+ case 'v':
+ verbose = true;
+ break;
+ default:
+ fprintf(stderr, "usage: %s [-v]\n", getprogname());
+ return EXIT_FAILURE;
+ }
+ }
+ argc -= optind;
+ argv += optind;
+
+ if (argc != 1) {
+ printf("Please specify the python_plugin.so as argument!\n");
+ return EXIT_FAILURE;
+ }
+ python_plugin_so_path = argv[0];
+
+ RUN_TEST(check_example_io_plugin_version_display(true));
+ RUN_TEST(check_example_io_plugin_version_display(false));
+ RUN_TEST(check_example_io_plugin_command_log());
+ RUN_TEST(check_example_io_plugin_command_log_multiple());
+ RUN_TEST(check_example_io_plugin_failed_to_start_command());
+ RUN_TEST(check_example_io_plugin_fails_with_python_backtrace());
+ RUN_TEST(check_io_plugin_callbacks_are_optional());
+ RUN_TEST(check_io_plugin_reports_error());
+ RUN_TEST(check_plugin_unload());
+
+ RUN_TEST(check_example_group_plugin());
+#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
+ RUN_TEST(check_example_group_plugin_is_able_to_debug());
+#endif
+ RUN_TEST(check_plugin_unload());
+
+ RUN_TEST(check_loading_fails_with_missing_path());
+ RUN_TEST(check_loading_succeeds_with_missing_classname());
+ RUN_TEST(check_loading_fails_with_missing_classname());
+ RUN_TEST(check_loading_fails_with_wrong_classname());
+ RUN_TEST(check_loading_fails_with_wrong_path());
+ RUN_TEST(check_plugin_unload());
+
+ RUN_TEST(check_example_conversation_plugin_reason_log(false, "without_suspend"));
+ RUN_TEST(check_example_conversation_plugin_reason_log(true, "with_suspend"));
+ RUN_TEST(check_example_conversation_plugin_user_interrupts());
+ RUN_TEST(check_plugin_unload());
+
+ RUN_TEST(check_example_policy_plugin_version_display(true));
+ RUN_TEST(check_example_policy_plugin_version_display(false));
+ RUN_TEST(check_example_policy_plugin_accepted_execution());
+ RUN_TEST(check_example_policy_plugin_failed_execution());
+ RUN_TEST(check_example_policy_plugin_denied_execution());
+ RUN_TEST(check_example_policy_plugin_list());
+ RUN_TEST(check_example_policy_plugin_validate_invalidate());
+ RUN_TEST(check_policy_plugin_callbacks_are_optional());
+ RUN_TEST(check_policy_plugin_reports_error());
+ RUN_TEST(check_plugin_unload());
+
+ RUN_TEST(check_example_audit_plugin_receives_accept());
+ RUN_TEST(check_example_audit_plugin_receives_reject());
+ RUN_TEST(check_example_audit_plugin_receives_error());
+ RUN_TEST(check_example_audit_plugin_workflow_multiple());
+ RUN_TEST(check_example_audit_plugin_version_display());
+ RUN_TEST(check_audit_plugin_callbacks_are_optional());
+ RUN_TEST(check_audit_plugin_reports_error());
+ RUN_TEST(check_plugin_unload());
+
+ // Monday, too early
+ RUN_TEST(check_example_approval_plugin(
+ "2020-02-10T07:55:23", "That is not allowed outside the business hours!"));
+ // Monday, good time
+ RUN_TEST(check_example_approval_plugin("2020-02-10T08:05:23", NULL));
+ // Friday, good time
+ RUN_TEST(check_example_approval_plugin("2020-02-14T17:59:23", NULL));
+ // Friday, too late
+ RUN_TEST(check_example_approval_plugin(
+ "2020-02-10T18:05:23", "That is not allowed outside the business hours!"));
+ // Saturday
+ RUN_TEST(check_example_approval_plugin(
+ "2020-02-15T08:05:23", "That is not allowed on the weekend!"));
+ RUN_TEST(check_multiple_approval_plugin_and_arguments());
+
+ RUN_TEST(check_python_plugins_do_not_affect_each_other());
+ RUN_TEST(check_plugin_unload());
+
+#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
+ RUN_TEST(check_example_debugging("plugin@err"));
+ RUN_TEST(check_example_debugging("plugin@info"));
+ RUN_TEST(check_example_debugging("load@diag"));
+ RUN_TEST(check_example_debugging("sudo_cb@info"));
+ RUN_TEST(check_example_debugging("c_calls@diag"));
+ RUN_TEST(check_example_debugging("c_calls@info"));
+ RUN_TEST(check_example_debugging("py_calls@diag"));
+ RUN_TEST(check_example_debugging("py_calls@info"));
+ RUN_TEST(check_example_debugging("plugin@err"));
+ RUN_TEST(check_plugin_unload());
+#endif /* FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION */
+
+ if (ntests != 0) {
+ printf("%s: %d tests run, %d errors, %d%% success rate\n",
+ getprogname(), ntests, errors, (ntests - errors) * 100 / ntests);
+ }
+
+ return errors;
+}
diff --git a/plugins/python/regress/iohelpers.c b/plugins/python/regress/iohelpers.c
new file mode 100644
index 0000000..d481100
--- /dev/null
+++ b/plugins/python/regress/iohelpers.c
@@ -0,0 +1,180 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2020 Robert Manner <robert.manner@oneidentity.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * This is an open source non-commercial project. Dear PVS-Studio, please check it.
+ * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
+ */
+
+#include "iohelpers.h"
+
+int
+rmdir_recursive(const char *path)
+{
+ char *cmd = NULL;
+ int success = false;
+
+ if (asprintf(&cmd, "rm -rf \"%s\"", path) < 0)
+ return false;
+
+ if (system(cmd) == 0)
+ success = true;
+
+ free(cmd);
+
+ return success;
+}
+
+int
+fwriteall(const char *file_path, const char *string)
+{
+ int success = false;
+
+ FILE *file = fopen(file_path, "w+");
+ if (file == NULL)
+ goto cleanup;
+
+ size_t size = strlen(string);
+ if (fwrite(string, 1, size, file) < size) {
+ goto cleanup;
+ }
+
+ success = true;
+
+cleanup:
+ if (file)
+ fclose(file);
+
+ return success;
+}
+
+int
+freadall(const char *file_path, char *output, size_t max_len)
+{
+ int rc = false;
+ FILE *file = fopen(file_path, "rb");
+ if (file == NULL) {
+ printf("Failed to open file '%s'\n", file_path);
+ goto cleanup;
+ }
+
+ size_t len = fread(output, 1, max_len - 1, file);
+ output[len] = '\0';
+
+ if (ferror(file) != 0) {
+ printf("Failed to read file '%s' (Error %d)\n", file_path, ferror(file));
+ goto cleanup;
+ }
+
+ if (!feof(file)) {
+ printf("File '%s' was bigger than allocated buffer %zu", file_path, max_len);
+ goto cleanup;
+ }
+
+ rc = true;
+
+cleanup:
+ if (file)
+ fclose(file);
+
+ return rc;
+}
+
+int
+vsnprintf_append(char *output, size_t max_output_len, const char *fmt, va_list args)
+{
+ va_list args2;
+ va_copy(args2, args);
+
+ size_t output_len = strlen(output);
+ int rc = vsnprintf(output + output_len, max_output_len - output_len, fmt, args2);
+
+ va_end(args2);
+ return rc;
+}
+
+int
+snprintf_append(char *output, size_t max_output_len, const char *fmt, ...)
+{
+ va_list args;
+ va_start(args, fmt);
+ int rc = vsnprintf_append(output, max_output_len, fmt, args);
+ va_end(args);
+ return rc;
+}
+
+int
+str_array_count(char **str_array)
+{
+ int result = 0;
+ for (; str_array[result] != NULL; ++result) {}
+ return result;
+}
+
+void
+str_array_snprint(char *out_str, size_t max_len, char **str_array, int array_len)
+{
+ if (array_len < 0)
+ array_len = str_array_count(str_array);
+
+ for (int pos = 0; pos < array_len; ++pos) {
+ snprintf_append(out_str, max_len, "%s%s", pos > 0 ? ", " : "", str_array[pos]);
+ }
+}
+
+char *
+str_replaced(const char *source, size_t dest_len, const char *old, const char *new)
+{
+ char *result = malloc(dest_len);
+ char *dest = result;
+ char *pos = NULL;
+ size_t old_len = strlen(old);
+
+ if (result == NULL)
+ return NULL;
+
+ while ((pos = strstr(source, old)) != NULL) {
+ size_t len = snprintf(dest, dest_len,
+ "%.*s%s", (int)(pos - source), source, new);
+ if (len >= dest_len)
+ goto fail;
+
+ dest_len -= len;
+ dest += len;
+ source = pos + old_len;
+ }
+
+ if (strlcpy(dest, source, dest_len) >= dest_len)
+ goto fail;
+
+ return result;
+
+fail:
+ free(result);
+ return strdup("str_replace_all failed, string too long");
+}
+
+void
+str_replace_in_place(char *string, size_t max_length, const char *old, const char *new)
+{
+ char *replaced = str_replaced(string, max_length, old, new);
+ if (replaced != NULL) {
+ strlcpy(string, replaced, max_length);
+ free(replaced);
+ }
+}
diff --git a/plugins/python/regress/iohelpers.h b/plugins/python/regress/iohelpers.h
new file mode 100644
index 0000000..ed21d56
--- /dev/null
+++ b/plugins/python/regress/iohelpers.h
@@ -0,0 +1,58 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2020 Robert Manner <robert.manner@oneidentity.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef PYTHON_IO_HELPERS
+#define PYTHON_IO_HELPERS
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#ifdef HAVE_STDBOOL_H
+# include <stdbool.h>
+#else
+# include "compat/stdbool.h"
+#endif /* HAVE_STDBOOL_H */
+#include <string.h>
+#include <stdarg.h>
+#include <signal.h>
+#include <pwd.h>
+
+#include "sudo_compat.h"
+
+#define MAX_OUTPUT (2 << 16)
+
+int rmdir_recursive(const char *path);
+
+int fwriteall(const char *file_path, const char *string);
+int freadall(const char *file_path, char *output, size_t max_len);
+
+// allocates new string with the content of 'string' but 'old' replaced to 'new'
+// The allocated array will be dest_length size and null terminated correctly.
+char *str_replaced(const char *string, size_t dest_length, const char *old, const char *new);
+
+// same, but "string" must be able to store 'max_length' number of characters including the null terminator
+void str_replace_in_place(char *string, size_t max_length, const char *old, const char *new);
+
+int vsnprintf_append(char *output, size_t max_output_len, const char *fmt, va_list args);
+int snprintf_append(char *output, size_t max_output_len, const char *fmt, ...);
+
+int str_array_count(char **str_array);
+void str_array_snprint(char *out_str, size_t max_len, char **str_array, int array_len);
+
+#endif
diff --git a/plugins/python/regress/plugin_approval_test.py b/plugins/python/regress/plugin_approval_test.py
new file mode 100644
index 0000000..69ea668
--- /dev/null
+++ b/plugins/python/regress/plugin_approval_test.py
@@ -0,0 +1,22 @@
+import sudo
+import json
+
+
+class ApprovalTestPlugin(sudo.Plugin):
+ def __init__(self, plugin_options, **kwargs):
+ id = sudo.options_as_dict(plugin_options).get("Id", "")
+ super().__init__(plugin_options=plugin_options, **kwargs)
+ self._id = "(APPROVAL {})".format(id)
+ sudo.log_info("{} Constructed:".format(self._id))
+ sudo.log_info(json.dumps(self.__dict__, indent=4, sort_keys=True))
+
+ def __del__(self):
+ sudo.log_info("{} Destructed successfully".format(self._id))
+
+ def check(self, *args):
+ sudo.log_info("{} Check was called with arguments: "
+ "{}".format(self._id, args))
+
+ def show_version(self, *args):
+ sudo.log_info("{} Show version was called with arguments: "
+ "{}".format(self._id, args))
diff --git a/plugins/python/regress/plugin_conflict.py b/plugins/python/regress/plugin_conflict.py
new file mode 100644
index 0000000..3632193
--- /dev/null
+++ b/plugins/python/regress/plugin_conflict.py
@@ -0,0 +1,11 @@
+import sudo
+
+import sys
+
+sys.path = []
+
+class ConflictPlugin(sudo.Plugin):
+ def __init__(self, plugin_options, **kwargs):
+ sudo.log_info("PATH before: {} (should be empty)".format(sys.path))
+ sys.path = [sudo.options_as_dict(plugin_options).get("Path")]
+ sudo.log_info("PATH set: {}".format(sys.path))
diff --git a/plugins/python/regress/plugin_errorstr.py b/plugins/python/regress/plugin_errorstr.py
new file mode 100644
index 0000000..fcbd71d
--- /dev/null
+++ b/plugins/python/regress/plugin_errorstr.py
@@ -0,0 +1,18 @@
+import sudo
+
+
+# The purpose of this class is that all methods you call on its object
+# raises a PluginError with a message containing the name of the called method.
+# Eg. if you call "ErrorMsgPlugin().some_method()" it will raise
+# "Something wrong in some_method"
+class ErrorMsgPlugin(sudo.Plugin):
+ def __getattr__(self, name):
+ def raiser_func(*args):
+ raise sudo.PluginError("Something wrong in " + name)
+
+ return raiser_func
+
+
+class ConstructErrorPlugin(sudo.Plugin):
+ def __init__(self, **kwargs):
+ raise sudo.PluginError("Something wrong in plugin constructor")
diff --git a/plugins/python/regress/testdata/check_example_audit_plugin_receives_accept.stdout b/plugins/python/regress/testdata/check_example_audit_plugin_receives_accept.stdout
new file mode 100644
index 0000000..2c83972
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_audit_plugin_receives_accept.stdout
@@ -0,0 +1,7 @@
+(AUDIT) -- Started by user testuser1 (123) --
+(AUDIT) Requested command: id --help
+(AUDIT) Accepted command: /sbin/id --help
+(AUDIT) By the plugin: accepter plugin name (type=POLICY)
+(AUDIT) Environment: KEY1=VALUE1 KEY2=VALUE2
+(AUDIT) Command returned with exit code 2
+(AUDIT) -- Finished --
diff --git a/plugins/python/regress/testdata/check_example_audit_plugin_receives_error.stdout b/plugins/python/regress/testdata/check_example_audit_plugin_receives_error.stdout
new file mode 100644
index 0000000..cb7068d
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_audit_plugin_receives_error.stdout
@@ -0,0 +1,5 @@
+(AUDIT) -- Started by user ??? (???) --
+(AUDIT) Requested command: id
+(AUDIT) Plugin errorer plugin name (type=AUDIT) got an error: Some error has happened
+(AUDIT) Sudo has run into an error: 222
+(AUDIT) -- Finished --
diff --git a/plugins/python/regress/testdata/check_example_audit_plugin_receives_reject.stdout b/plugins/python/regress/testdata/check_example_audit_plugin_receives_reject.stdout
new file mode 100644
index 0000000..c6e8a5a
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_audit_plugin_receives_reject.stdout
@@ -0,0 +1,5 @@
+(AUDIT) -- Started by user root (0) --
+(AUDIT) Requested command: passwd
+(AUDIT) Rejected by plugin rejecter plugin name (type=IO): Rejected just because!
+(AUDIT) The command was not executed
+(AUDIT) -- Finished --
diff --git a/plugins/python/regress/testdata/check_example_audit_plugin_version_display.stdout b/plugins/python/regress/testdata/check_example_audit_plugin_version_display.stdout
new file mode 100644
index 0000000..1586f46
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_audit_plugin_version_display.stdout
@@ -0,0 +1,6 @@
+(AUDIT) -- Started by user root (0) --
+Python Example Audit Plugin
+Python audit plugin (API 1.0): SudoAuditPlugin (loaded from 'SRC_DIR/example_audit_plugin.py')
+Python Example Audit Plugin (version=1.0)
+(AUDIT) Sudo has run into an error: 222
+(AUDIT) -- Finished --
diff --git a/plugins/python/regress/testdata/check_example_audit_plugin_workflow_multiple.stderr b/plugins/python/regress/testdata/check_example_audit_plugin_workflow_multiple.stderr
new file mode 100644
index 0000000..1d7d4a1
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_audit_plugin_workflow_multiple.stderr
@@ -0,0 +1 @@
+sudo: loading more than 8 sudo python audit plugins is not supported
diff --git a/plugins/python/regress/testdata/check_example_audit_plugin_workflow_multiple.stdout b/plugins/python/regress/testdata/check_example_audit_plugin_workflow_multiple.stdout
new file mode 100644
index 0000000..aa5ee58
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_audit_plugin_workflow_multiple.stdout
@@ -0,0 +1,14 @@
+(AUDIT1) -- Started by user default (1000) --
+(AUDIT1) Requested command: id --help
+(AUDIT2) -- Started by user default (1000) --
+(AUDIT2) Requested command: id --help
+(AUDIT1) Accepted command: /sbin/id --help
+(AUDIT1) By the plugin: accepter plugin name (type=POLICY)
+(AUDIT1) Environment: KEY1=VALUE1 KEY2=VALUE2
+(AUDIT2) Accepted command: /sbin/id --help
+(AUDIT2) By the plugin: accepter plugin name (type=POLICY)
+(AUDIT2) Environment: KEY1=VALUE1 KEY2=VALUE2
+(AUDIT1) Command exited due to signal 11
+(AUDIT1) -- Finished --
+(AUDIT2) Command exited due to signal 11
+(AUDIT2) -- Finished --
diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.conversation b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.conversation
new file mode 100644
index 0000000..43bd2e7
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.conversation
@@ -0,0 +1,3 @@
+Question count: 2
+Question 0: <<Reason: >> (timeout: 120, msg_type=2)
+Question 1: <<Secret reason: >> (timeout: 120, msg_type=5)
diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stderr b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stderr
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stderr
diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stdout b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stdout
new file mode 100644
index 0000000..9d515c9
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stdout
@@ -0,0 +1,3 @@
+Please provide your reason for executing ('/bin/whoami',)
+conversation suspend: signal SIGTSTP
+conversation resume: signal was SIGCONT
diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stored b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stored
new file mode 100644
index 0000000..c0ab857
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_with_suspend.stored
@@ -0,0 +1,3 @@
+Executed /bin/whoami
+Reason: my fake reason
+Hidden reason: my real secret reason
diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.conversation b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.conversation
new file mode 100644
index 0000000..43bd2e7
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.conversation
@@ -0,0 +1,3 @@
+Question count: 2
+Question 0: <<Reason: >> (timeout: 120, msg_type=2)
+Question 1: <<Secret reason: >> (timeout: 120, msg_type=5)
diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stderr b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stderr
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stderr
diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stdout b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stdout
new file mode 100644
index 0000000..7bbfa3f
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stdout
@@ -0,0 +1 @@
+Please provide your reason for executing ('/bin/whoami',)
diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stored b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stored
new file mode 100644
index 0000000..c0ab857
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_conversation_plugin_reason_log_without_suspend.stored
@@ -0,0 +1,3 @@
+Executed /bin/whoami
+Reason: my fake reason
+Hidden reason: my real secret reason
diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.conv b/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.conv
new file mode 100644
index 0000000..59d7202
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.conv
@@ -0,0 +1,2 @@
+Question count: 2
+Question 0: <<Reason: >> (timeout: 120, msg_type=2)
diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.conversation b/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.conversation
new file mode 100644
index 0000000..59d7202
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.conversation
@@ -0,0 +1,2 @@
+Question count: 2
+Question 0: <<Reason: >> (timeout: 120, msg_type=2)
diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.stderr b/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.stderr
new file mode 100644
index 0000000..8a4a528
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.stderr
@@ -0,0 +1 @@
+You did not answer in time
diff --git a/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.stdout b/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.stdout
new file mode 100644
index 0000000..7bbfa3f
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_conversation_plugin_user_interrupts.stdout
@@ -0,0 +1 @@
+Please provide your reason for executing ('/bin/whoami',)
diff --git a/plugins/python/regress/testdata/check_example_debugging_c_calls@diag.log b/plugins/python/regress/testdata/check_example_debugging_c_calls@diag.log
new file mode 100644
index 0000000..c5cd98c
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_debugging_c_calls@diag.log
@@ -0,0 +1,6 @@
+sudo.debug was called with arguments: (DEBUG.ERROR, 'My demo purpose plugin shows this ERROR level debug message')
+sudo.debug was called with arguments: (DEBUG.INFO, 'My demo purpose plugin shows this INFO level debug message')
+LogHandler.emit was called
+LogHandler.emit was called
+sudo.options_as_dict was called with arguments: (('ModulePath=SRC_DIR/example_debugging.py', 'ClassName=DebugDemoPlugin'),)
+sudo.options_as_dict returned result: [('ClassName', 'DebugDemoPlugin'), ('ModulePath', 'SRC_DIR/example_debugging.py')]
diff --git a/plugins/python/regress/testdata/check_example_debugging_c_calls@info.log b/plugins/python/regress/testdata/check_example_debugging_c_calls@info.log
new file mode 100644
index 0000000..7258618
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_debugging_c_calls@info.log
@@ -0,0 +1,11 @@
+__init__ @ SRC_DIR/example_debugging.py:58 calls C function:
+sudo.debug was called with arguments: (DEBUG.ERROR, 'My demo purpose plugin shows this ERROR level debug message')
+__init__ @ SRC_DIR/example_debugging.py:63 calls C function:
+sudo.debug was called with arguments: (DEBUG.INFO, 'My demo purpose plugin shows this INFO level debug message')
+handle @ logging/__init__.py calls C function:
+LogHandler.emit was called
+handle @ logging/__init__.py calls C function:
+LogHandler.emit was called
+__init__ @ SRC_DIR/example_debugging.py:85 calls C function:
+sudo.options_as_dict was called with arguments: (('ModulePath=SRC_DIR/example_debugging.py', 'ClassName=DebugDemoPlugin'),)
+sudo.options_as_dict returned result: [('ClassName', 'DebugDemoPlugin'), ('ModulePath', 'SRC_DIR/example_debugging.py')]
diff --git a/plugins/python/regress/testdata/check_example_debugging_load@diag.log b/plugins/python/regress/testdata/check_example_debugging_load@diag.log
new file mode 100644
index 0000000..15b4bbe
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_debugging_load@diag.log
@@ -0,0 +1,3 @@
+importing module: SRC_DIR/example_debugging.py
+Extending python 'path' with 'SRC_DIR'
+Deinit was called for a python plugin
diff --git a/plugins/python/regress/testdata/check_example_debugging_plugin@err.log b/plugins/python/regress/testdata/check_example_debugging_plugin@err.log
new file mode 100644
index 0000000..aec31ec
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_debugging_plugin@err.log
@@ -0,0 +1,2 @@
+My demo purpose plugin shows this ERROR level debug message
+Python log system shows this ERROR level debug message
diff --git a/plugins/python/regress/testdata/check_example_debugging_plugin@info.log b/plugins/python/regress/testdata/check_example_debugging_plugin@info.log
new file mode 100644
index 0000000..ed72f35
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_debugging_plugin@info.log
@@ -0,0 +1,8 @@
+__init__ @ SRC_DIR/example_debugging.py:58 debugs:
+My demo purpose plugin shows this ERROR level debug message
+__init__ @ SRC_DIR/example_debugging.py:63 debugs:
+My demo purpose plugin shows this INFO level debug message
+handle @ logging/__init__.py debugs:
+Python log system shows this ERROR level debug message
+handle @ logging/__init__.py debugs:
+Python log system shows this INFO level debug message
diff --git a/plugins/python/regress/testdata/check_example_debugging_py_calls@diag.log b/plugins/python/regress/testdata/check_example_debugging_py_calls@diag.log
new file mode 100644
index 0000000..97a89ef
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_debugging_py_calls@diag.log
@@ -0,0 +1,2 @@
+DebugDemoPlugin.__init__ was called with arguments: () [('plugin_options', ('ModulePath=SRC_DIR/example_debugging.py', 'ClassName=DebugDemoPlugin')), ('settings', ('debug_flags=/tmp/sudo_check_python_exampleXXXXXX/debug.log py_calls@diag', 'plugin_path=python_plugin.so')), ('user_env', ()), ('user_info', ()), ('version', '1.0')]
+DebugDemoPlugin.__init__ returned result: <example_debugging.DebugDemoPlugin object>
diff --git a/plugins/python/regress/testdata/check_example_debugging_py_calls@info.log b/plugins/python/regress/testdata/check_example_debugging_py_calls@info.log
new file mode 100644
index 0000000..ae39daf
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_debugging_py_calls@info.log
@@ -0,0 +1,9 @@
+DebugDemoPlugin.__init__ was called with arguments: () [('plugin_options', ('ModulePath=SRC_DIR/example_debugging.py', 'ClassName=DebugDemoPlugin')), ('settings', ('debug_flags=/tmp/sudo_check_python_exampleXXXXXX/debug.log py_calls@info', 'plugin_path=python_plugin.so')), ('user_env', ()), ('user_info', ()), ('version', '1.0')]
+DebugDemoPlugin.__init__ returned result: <example_debugging.DebugDemoPlugin object>
+DebugDemoPlugin function 'log_ttyin' is not implemented
+DebugDemoPlugin function 'log_ttyout' is not implemented
+DebugDemoPlugin function 'log_stdin' is not implemented
+DebugDemoPlugin function 'log_stdout' is not implemented
+DebugDemoPlugin function 'log_stderr' is not implemented
+DebugDemoPlugin function 'change_winsize' is not implemented
+DebugDemoPlugin function 'log_suspend' is not implemented
diff --git a/plugins/python/regress/testdata/check_example_debugging_sudo_cb@info.log b/plugins/python/regress/testdata/check_example_debugging_sudo_cb@info.log
new file mode 100644
index 0000000..908066b
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_debugging_sudo_cb@info.log
@@ -0,0 +1 @@
+Skipping close call, because there was no command run
diff --git a/plugins/python/regress/testdata/check_example_group_plugin_is_able_to_debug.log b/plugins/python/regress/testdata/check_example_group_plugin_is_able_to_debug.log
new file mode 100644
index 0000000..6def462
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_group_plugin_is_able_to_debug.log
@@ -0,0 +1,4 @@
+SudoGroupPlugin.__init__ was called with arguments: () [('args', ('ModulePath=SRC_DIR/example_group_plugin.py', 'ClassName=SudoGroupPlugin')), ('version', '1.0')]
+SudoGroupPlugin.__init__ returned result: <example_group_plugin.SudoGroupPlugin object>
+SudoGroupPlugin.query was called with arguments: ('user', 'group', ('pw_name', 'pw_passwd', 1001, 101, 'pw_gecos', 'pw_dir', 'pw_shell'))
+SudoGroupPlugin.query returned result: 0
diff --git a/plugins/python/regress/testdata/check_example_io_plugin_command_log.stderr b/plugins/python/regress/testdata/check_example_io_plugin_command_log.stderr
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_io_plugin_command_log.stderr
diff --git a/plugins/python/regress/testdata/check_example_io_plugin_command_log.stdout b/plugins/python/regress/testdata/check_example_io_plugin_command_log.stdout
new file mode 100644
index 0000000..7e94c91
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_io_plugin_command_log.stdout
@@ -0,0 +1 @@
+Example sudo python plugin will log to /tmp/sudo_check_python_exampleXXXXXX/sudo.log
diff --git a/plugins/python/regress/testdata/check_example_io_plugin_command_log.stored b/plugins/python/regress/testdata/check_example_io_plugin_command_log.stored
new file mode 100644
index 0000000..73fdc5d
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_io_plugin_command_log.stored
@@ -0,0 +1,16 @@
+ -- Plugin STARTED --
+EXEC id --help
+EXEC info [
+ "command=/bin/id",
+ "runas_uid=0"
+]
+STD IN some standard input
+STD OUT some standard output
+STD ERR some standard error
+SUSPEND SIGTSTP
+SUSPEND SIGCONT
+WINSIZE 200x100
+TTY IN some tty input
+TTY OUT some tty output
+CLOSE Command returned 1
+ -- Plugin DESTROYED --
diff --git a/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple.stderr b/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple.stderr
new file mode 100644
index 0000000..f519805
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple.stderr
@@ -0,0 +1 @@
+sudo: loading more than 8 sudo python IO plugins is not supported
diff --git a/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple.stdout b/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple.stdout
new file mode 100644
index 0000000..e9dbd67
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple.stdout
@@ -0,0 +1,2 @@
+Example sudo python plugin will log to /tmp/sudo_check_python_exampleXXXXXX/sudo.log
+Example sudo python plugin will log to /tmp/sudo_check_python_exampleXXXXXX2/sudo.log
diff --git a/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple1.stored b/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple1.stored
new file mode 100644
index 0000000..bc60c38
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple1.stored
@@ -0,0 +1,16 @@
+ -- Plugin STARTED --
+EXEC id --help
+EXEC info [
+ "command=/bin/id",
+ "runas_uid=0"
+]
+STD IN stdin for plugin 1
+STD OUT stdout for plugin 1
+STD ERR stderr for plugin 1
+SUSPEND SIGTSTP
+SUSPEND SIGCONT
+WINSIZE 20x10
+TTY IN tty input for plugin 1
+TTY OUT tty output for plugin 1
+CLOSE Command returned 1
+ -- Plugin DESTROYED --
diff --git a/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple2.stored b/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple2.stored
new file mode 100644
index 0000000..ed3fdc8
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_io_plugin_command_log_multiple2.stored
@@ -0,0 +1,16 @@
+ -- Plugin STARTED --
+EXEC whoami
+EXEC info [
+ "command=/bin/whoami",
+ "runas_uid=1"
+]
+STD IN stdin for plugin 2
+STD OUT stdout for plugin 2
+STD ERR stderr for plugin 2
+SUSPEND SIGSTOP
+SUSPEND SIGCONT
+WINSIZE 30x40
+TTY IN tty input for plugin 2
+TTY OUT tty output for plugin 2
+CLOSE Command returned 2
+ -- Plugin DESTROYED --
diff --git a/plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stderr b/plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stderr
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stderr
diff --git a/plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stdout b/plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stdout
new file mode 100644
index 0000000..7e94c91
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stdout
@@ -0,0 +1 @@
+Example sudo python plugin will log to /tmp/sudo_check_python_exampleXXXXXX/sudo.log
diff --git a/plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stored b/plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stored
new file mode 100644
index 0000000..1b99398
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_io_plugin_failed_to_start_command.stored
@@ -0,0 +1,8 @@
+ -- Plugin STARTED --
+EXEC cmd
+EXEC info [
+ "command=/usr/share/cmd",
+ "runas_uid=0"
+]
+CLOSE Failed to execute, execve returned 1 (EPERM)
+ -- Plugin DESTROYED --
diff --git a/plugins/python/regress/testdata/check_example_io_plugin_fails_with_python_backtrace.stderr b/plugins/python/regress/testdata/check_example_io_plugin_fails_with_python_backtrace.stderr
new file mode 100644
index 0000000..1dd42a6
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_io_plugin_fails_with_python_backtrace.stderr
@@ -0,0 +1 @@
+Failed to construct plugin instance: [Errno 2] No such file or directory: '/some/not/writable/directory/sudo.log'
diff --git a/plugins/python/regress/testdata/check_example_io_plugin_fails_with_python_backtrace.stdout b/plugins/python/regress/testdata/check_example_io_plugin_fails_with_python_backtrace.stdout
new file mode 100644
index 0000000..10b0e23
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_io_plugin_fails_with_python_backtrace.stdout
@@ -0,0 +1,7 @@
+Example sudo python plugin will log to /some/not/writable/directory/sudo.log
+Traceback:
+ File "SRC_DIR/example_io_plugin.py", line 64, in __init__
+ self._open_log_file(path.join(log_path, "sudo.log"))
+ File "SRC_DIR/example_io_plugin.py", line 134, in _open_log_file
+ self._log_file = open(log_path, "a")
+
diff --git a/plugins/python/regress/testdata/check_example_io_plugin_version_display.stderr b/plugins/python/regress/testdata/check_example_io_plugin_version_display.stderr
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_io_plugin_version_display.stderr
diff --git a/plugins/python/regress/testdata/check_example_io_plugin_version_display.stdout b/plugins/python/regress/testdata/check_example_io_plugin_version_display.stdout
new file mode 100644
index 0000000..07e998a
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_io_plugin_version_display.stdout
@@ -0,0 +1,2 @@
+Example sudo python plugin will log to /tmp/sudo_check_python_exampleXXXXXX/sudo.log
+Python Example IO Plugin version: 1.0
diff --git a/plugins/python/regress/testdata/check_example_io_plugin_version_display.stored b/plugins/python/regress/testdata/check_example_io_plugin_version_display.stored
new file mode 100644
index 0000000..45f9b9e
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_io_plugin_version_display.stored
@@ -0,0 +1,2 @@
+ -- Plugin STARTED --
+ -- Plugin DESTROYED --
diff --git a/plugins/python/regress/testdata/check_example_io_plugin_version_display_full.stdout b/plugins/python/regress/testdata/check_example_io_plugin_version_display_full.stdout
new file mode 100644
index 0000000..cfb3921
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_io_plugin_version_display_full.stdout
@@ -0,0 +1,3 @@
+Example sudo python plugin will log to /tmp/sudo_check_python_exampleXXXXXX/sudo.log
+Python io plugin (API 1.0): SudoIOPlugin (loaded from 'SRC_DIR/example_io_plugin.py')
+Python Example IO Plugin version: 1.0
diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_accepted_execution.stderr b/plugins/python/regress/testdata/check_example_policy_plugin_accepted_execution.stderr
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_policy_plugin_accepted_execution.stderr
diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_accepted_execution.stdout b/plugins/python/regress/testdata/check_example_policy_plugin_accepted_execution.stdout
new file mode 100644
index 0000000..c63a26e
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_policy_plugin_accepted_execution.stdout
@@ -0,0 +1 @@
+The command returned with exit_status 3
diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_denied_execution.stderr b/plugins/python/regress/testdata/check_example_policy_plugin_denied_execution.stderr
new file mode 100644
index 0000000..6db9b2c
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_policy_plugin_denied_execution.stderr
@@ -0,0 +1 @@
+You are not allowed to run this command!
diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_denied_execution.stdout b/plugins/python/regress/testdata/check_example_policy_plugin_denied_execution.stdout
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_policy_plugin_denied_execution.stdout
diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_failed_execution.stderr b/plugins/python/regress/testdata/check_example_policy_plugin_failed_execution.stderr
new file mode 100644
index 0000000..e8d7034
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_policy_plugin_failed_execution.stderr
@@ -0,0 +1 @@
+Failed to execute command, execve syscall returned 2 (ENOENT)
diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_failed_execution.stdout b/plugins/python/regress/testdata/check_example_policy_plugin_failed_execution.stdout
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_policy_plugin_failed_execution.stdout
diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_list.stderr b/plugins/python/regress/testdata/check_example_policy_plugin_list.stderr
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_policy_plugin_list.stderr
diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_list.stdout b/plugins/python/regress/testdata/check_example_policy_plugin_list.stdout
new file mode 100644
index 0000000..48c5baf
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_policy_plugin_list.stdout
@@ -0,0 +1,25 @@
+-- minimal --
+Only the following commands are allowed: id, whoami
+
+-- minimal (verbose) --
+Only the following commands are allowed: id, whoami
+
+-- with user --
+Only the following commands are allowed: id, whoami as user 'testuser'
+
+-- with user (verbose) --
+Only the following commands are allowed: id, whoami as user 'testuser'
+
+-- with allowed program --
+You are allowed to execute command '/bin/id'
+
+-- with allowed program (verbose) --
+You are allowed to execute command '/bin/id'
+Only the following commands are allowed: id, whoami
+
+-- with denied program --
+You are NOT allowed to execute command '/bin/passwd'
+
+-- with denied program (verbose) --
+You are NOT allowed to execute command '/bin/passwd'
+Only the following commands are allowed: id, whoami
diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_validate_invalidate.log b/plugins/python/regress/testdata/check_example_policy_plugin_validate_invalidate.log
new file mode 100644
index 0000000..6f1479a
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_policy_plugin_validate_invalidate.log
@@ -0,0 +1,8 @@
+SudoPolicyPlugin.__init__ was called with arguments: () [('plugin_options', ('ModulePath=SRC_DIR/example_policy_plugin.py', 'ClassName=SudoPolicyPlugin')), ('settings', ()), ('user_env', ()), ('user_info', ()), ('version', '1.0')]
+SudoPolicyPlugin.__init__ returned result: <example_policy_plugin.SudoPolicyPlugin object>
+SudoPolicyPlugin.validate was called with arguments: ()
+SudoPolicyPlugin.validate returned result: None
+SudoPolicyPlugin.invalidate was called with arguments: (1,)
+SudoPolicyPlugin.invalidate returned result: None
+SudoPolicyPlugin.invalidate was called with arguments: (0,)
+SudoPolicyPlugin.invalidate returned result: None
diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_version_display.stderr b/plugins/python/regress/testdata/check_example_policy_plugin_version_display.stderr
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_policy_plugin_version_display.stderr
diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_version_display.stdout b/plugins/python/regress/testdata/check_example_policy_plugin_version_display.stdout
new file mode 100644
index 0000000..1cc1edd
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_policy_plugin_version_display.stdout
@@ -0,0 +1 @@
+Python Example Policy Plugin version: 1.0
diff --git a/plugins/python/regress/testdata/check_example_policy_plugin_version_display_full.stdout b/plugins/python/regress/testdata/check_example_policy_plugin_version_display_full.stdout
new file mode 100644
index 0000000..a23cf12
--- /dev/null
+++ b/plugins/python/regress/testdata/check_example_policy_plugin_version_display_full.stdout
@@ -0,0 +1,2 @@
+Python policy plugin (API 1.0): SudoPolicyPlugin (loaded from 'SRC_DIR/example_policy_plugin.py')
+Python Example Policy Plugin version: 1.0
diff --git a/plugins/python/regress/testdata/check_loading_fails_missing_classname.stderr b/plugins/python/regress/testdata/check_loading_fails_missing_classname.stderr
new file mode 100644
index 0000000..c207e2f
--- /dev/null
+++ b/plugins/python/regress/testdata/check_loading_fails_missing_classname.stderr
@@ -0,0 +1,3 @@
+No plugin class is specified for python module 'SRC_DIR/regress/plugin_errorstr.py'. Use 'ClassName' configuration option in 'sudo.conf'
+Possible plugins: ConstructErrorPlugin, ErrorMsgPlugin
+Failed during loading plugin class
diff --git a/plugins/python/regress/testdata/check_loading_fails_missing_classname.stdout b/plugins/python/regress/testdata/check_loading_fails_missing_classname.stdout
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/python/regress/testdata/check_loading_fails_missing_classname.stdout
diff --git a/plugins/python/regress/testdata/check_loading_fails_missing_path.stderr b/plugins/python/regress/testdata/check_loading_fails_missing_path.stderr
new file mode 100644
index 0000000..05bc634
--- /dev/null
+++ b/plugins/python/regress/testdata/check_loading_fails_missing_path.stderr
@@ -0,0 +1,2 @@
+No python module path is specified. Use 'ModulePath' plugin config option in 'sudo.conf'
+Failed during loading plugin class
diff --git a/plugins/python/regress/testdata/check_loading_fails_missing_path.stdout b/plugins/python/regress/testdata/check_loading_fails_missing_path.stdout
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/python/regress/testdata/check_loading_fails_missing_path.stdout
diff --git a/plugins/python/regress/testdata/check_loading_fails_not_owned_by_root.stderr b/plugins/python/regress/testdata/check_loading_fails_not_owned_by_root.stderr
new file mode 100644
index 0000000..7ba1bc9
--- /dev/null
+++ b/plugins/python/regress/testdata/check_loading_fails_not_owned_by_root.stderr
@@ -0,0 +1 @@
+Failed during loading plugin class: File 'SRC_DIR/example_debugging.py' must be owned by uid 0
diff --git a/plugins/python/regress/testdata/check_loading_fails_not_owned_by_root.stdout b/plugins/python/regress/testdata/check_loading_fails_not_owned_by_root.stdout
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/python/regress/testdata/check_loading_fails_not_owned_by_root.stdout
diff --git a/plugins/python/regress/testdata/check_loading_fails_wrong_classname.stderr b/plugins/python/regress/testdata/check_loading_fails_wrong_classname.stderr
new file mode 100644
index 0000000..a4c519a
--- /dev/null
+++ b/plugins/python/regress/testdata/check_loading_fails_wrong_classname.stderr
@@ -0,0 +1,2 @@
+Failed to find plugin class 'MispelledPluginName'
+Failed during loading plugin class
diff --git a/plugins/python/regress/testdata/check_loading_fails_wrong_classname.stdout b/plugins/python/regress/testdata/check_loading_fails_wrong_classname.stdout
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/python/regress/testdata/check_loading_fails_wrong_classname.stdout
diff --git a/plugins/python/regress/testdata/check_loading_fails_wrong_path.stderr b/plugins/python/regress/testdata/check_loading_fails_wrong_path.stderr
new file mode 100644
index 0000000..3087ba8
--- /dev/null
+++ b/plugins/python/regress/testdata/check_loading_fails_wrong_path.stderr
@@ -0,0 +1 @@
+Failed during loading plugin class: No module named 'wrong_path'
diff --git a/plugins/python/regress/testdata/check_loading_fails_wrong_path.stdout b/plugins/python/regress/testdata/check_loading_fails_wrong_path.stdout
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/python/regress/testdata/check_loading_fails_wrong_path.stdout
diff --git a/plugins/python/regress/testdata/check_loading_succeeds_with_missing_classname.stdout b/plugins/python/regress/testdata/check_loading_succeeds_with_missing_classname.stdout
new file mode 100644
index 0000000..f7a1a6f
--- /dev/null
+++ b/plugins/python/regress/testdata/check_loading_succeeds_with_missing_classname.stdout
@@ -0,0 +1 @@
+Python io plugin (API 1.0): DebugDemoPlugin (loaded from 'SRC_DIR/example_debugging.py')
diff --git a/plugins/python/regress/testdata/check_multiple_approval_plugin_and_arguments.stderr b/plugins/python/regress/testdata/check_multiple_approval_plugin_and_arguments.stderr
new file mode 100644
index 0000000..6dfb141
--- /dev/null
+++ b/plugins/python/regress/testdata/check_multiple_approval_plugin_and_arguments.stderr
@@ -0,0 +1 @@
+sudo: loading more than 8 sudo python approval plugins is not supported
diff --git a/plugins/python/regress/testdata/check_multiple_approval_plugin_and_arguments.stdout b/plugins/python/regress/testdata/check_multiple_approval_plugin_and_arguments.stdout
new file mode 100644
index 0000000..2589025
--- /dev/null
+++ b/plugins/python/regress/testdata/check_multiple_approval_plugin_and_arguments.stdout
@@ -0,0 +1,67 @@
+(APPROVAL 1) Constructed:
+{
+ "_id": "(APPROVAL 1)",
+ "plugin_options": [
+ "ModulePath=SRC_DIR/regress/plugin_approval_test.py",
+ "ClassName=ApprovalTestPlugin",
+ "Id=1"
+ ],
+ "settings": [
+ "SETTING1=VALUE1",
+ "setting2=value2"
+ ],
+ "submit_argv": [
+ "sudo",
+ "-u",
+ "user",
+ "whoami",
+ "--help"
+ ],
+ "submit_optind": 3,
+ "user_env": [
+ "USER_ENV1=VALUE1",
+ "USER_ENV2=value2"
+ ],
+ "user_info": [
+ "INFO1=VALUE1",
+ "info2=value2"
+ ],
+ "version": "1.21"
+}
+(APPROVAL 2) Constructed:
+{
+ "_id": "(APPROVAL 2)",
+ "plugin_options": [
+ "ModulePath=SRC_DIR/regress/plugin_approval_test.py",
+ "ClassName=ApprovalTestPlugin",
+ "Id=2"
+ ],
+ "settings": [
+ "SETTING1=VALUE1",
+ "setting2=value2"
+ ],
+ "submit_argv": [
+ "sudo",
+ "-u",
+ "user",
+ "whoami",
+ "--help"
+ ],
+ "submit_optind": 3,
+ "user_env": [
+ "USER_ENV1=VALUE1",
+ "USER_ENV2=value2"
+ ],
+ "user_info": [
+ "INFO1=VALUE1",
+ "info2=value2"
+ ],
+ "version": "1.21"
+}
+(APPROVAL 1) Show version was called with arguments: (0,)
+Python approval plugin (API 1.0): ApprovalTestPlugin (loaded from 'SRC_DIR/regress/plugin_approval_test.py')
+(APPROVAL 2) Show version was called with arguments: (1,)
+(APPROVAL 1) Check was called with arguments: (('CMDINFO1=value1', 'CMDINFO2=VALUE2'), ('whoami', '--help'), ('USER_ENV1=VALUE1', 'USER_ENV2=value2'))
+(APPROVAL 2) Check was called with arguments: (('CMDINFO1=value1', 'CMDINFO2=VALUE2'), ('whoami', '--help'), ('USER_ENV1=VALUE1', 'USER_ENV2=value2'))
+(APPROVAL 1) Destructed successfully
+(APPROVAL 2) Destructed successfully
diff --git a/plugins/python/regress/testdata/check_python_plugins_do_not_affect_each_other.stdout b/plugins/python/regress/testdata/check_python_plugins_do_not_affect_each_other.stdout
new file mode 100644
index 0000000..cd5bef9
--- /dev/null
+++ b/plugins/python/regress/testdata/check_python_plugins_do_not_affect_each_other.stdout
@@ -0,0 +1,4 @@
+PATH before: [] (should be empty)
+PATH set: ['path_for_first_plugin']
+PATH before: [] (should be empty)
+PATH set: ['path_for_second_plugin']
diff --git a/plugins/python/regress/testhelpers.c b/plugins/python/regress/testhelpers.c
new file mode 100644
index 0000000..42971bb
--- /dev/null
+++ b/plugins/python/regress/testhelpers.c
@@ -0,0 +1,346 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2020 Robert Manner <robert.manner@oneidentity.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * This is an open source non-commercial project. Dear PVS-Studio, please check it.
+ * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
+ */
+
+#include "testhelpers.h"
+
+struct TestData data;
+
+/*
+ * Starting with Python 3.11, backtraces may contain a line with
+ * '^' characters to bring attention to the important part of the
+ * line.
+ */
+static void
+remove_underline(char *output)
+{
+ char *cp, *ep;
+
+ // Remove lines that only consist of '^' and white space.
+ cp = output;
+ ep = output + strlen(output);
+ for (;;) {
+ size_t len = strspn(cp, "^ \t");
+ if (len > 0 && cp[len] == '\n') {
+ /* Prune out lines that are "underlining". */
+ memmove(cp, cp + len + 1, ep - cp);
+ if (*cp == '\0')
+ break;
+ } else {
+ /* No match, move to the next line. */
+ cp = strchr(cp, '\n');
+ if (cp == NULL)
+ break;
+ cp++;
+ }
+ }
+}
+
+static void
+clean_output(char *output)
+{
+ // we replace some output which otherwise would be test run dependent
+ str_replace_in_place(output, MAX_OUTPUT, data.tmp_dir, TEMP_PATH_TEMPLATE);
+
+ if (data.tmp_dir2)
+ str_replace_in_place(output, MAX_OUTPUT, data.tmp_dir2, TEMP_PATH_TEMPLATE "2");
+
+ str_replace_in_place(output, MAX_OUTPUT, SRC_DIR, "SRC_DIR");
+
+ remove_underline(output);
+}
+
+const char *
+expected_path(const char *format, ...)
+{
+ static char expected_output_file[PATH_MAX];
+ size_t dirlen = strlcpy(expected_output_file, TESTDATA_DIR, sizeof(expected_output_file));
+
+ va_list args;
+ va_start(args, format);
+ vsnprintf(expected_output_file + dirlen, PATH_MAX - dirlen, format, args);
+ va_end(args);
+
+ return expected_output_file;
+}
+
+char **
+create_str_array(size_t count, ...)
+{
+ va_list args;
+
+ va_start(args, count);
+
+ char **result = calloc(count, sizeof(char *));
+ if (result != NULL) {
+ for (size_t i = 0; i < count; ++i) {
+ const char *str = va_arg(args, char *);
+ if (str != NULL) {
+ result[i] = strdup(str);
+ if (result[i] == NULL) {
+ while (i > 0) {
+ free(result[--i]);
+ }
+ free(result);
+ result = NULL;
+ break;
+ }
+ }
+ }
+ }
+
+ va_end(args);
+ return result;
+}
+
+int
+is_update(void)
+{
+ static int result = -1;
+ if (result < 0) {
+ const char *update = getenv("UPDATE_TESTDATA");
+ result = (update && strcmp(update, "1") == 0) ? 1 : 0;
+ }
+ return result;
+}
+
+int
+verify_content(char *actual_content, const char *reference_path)
+{
+ clean_output(actual_content);
+
+ if (is_update()) {
+ VERIFY_TRUE(fwriteall(reference_path, actual_content));
+ } else {
+ char expected_output[MAX_OUTPUT] = "";
+ if (!freadall(reference_path, expected_output, sizeof(expected_output))) {
+ printf("Error: Missing test data at '%s'\n", reference_path);
+ return false;
+ }
+ VERIFY_STR(actual_content, expected_output);
+ }
+
+ return true;
+}
+
+int
+verify_file(const char *actual_dir, const char *actual_file_name, const char *reference_path)
+{
+ char actual_path[PATH_MAX];
+ snprintf(actual_path, sizeof(actual_path), "%s/%s", actual_dir, actual_file_name);
+
+ char actual_str[MAX_OUTPUT];
+ if (!freadall(actual_path, actual_str, sizeof(actual_str))) {
+ printf("Expected that file '%s' gets created, but it was not\n", actual_path);
+ return false;
+ }
+
+ int rc = verify_content(actual_str, reference_path);
+ return rc;
+}
+
+int
+fake_conversation(int num_msgs, const struct sudo_conv_message msgs[],
+ struct sudo_conv_reply replies[], struct sudo_conv_callback *callback)
+{
+ (void) callback;
+ snprintf_append(data.conv_str, MAX_OUTPUT, "Question count: %d\n", num_msgs);
+ for (int i = 0; i < num_msgs; ++i) {
+ const struct sudo_conv_message *msg = &msgs[i];
+ snprintf_append(data.conv_str, MAX_OUTPUT, "Question %d: <<%s>> (timeout: %d, msg_type=%d)\n",
+ i, msg->msg, msg->timeout, msg->msg_type);
+
+ if (data.conv_replies[i] == NULL)
+ return 1; // simulates user interruption (conversation error)
+
+ replies[i].reply = strdup(data.conv_replies[i]);
+ if (replies[i].reply == NULL)
+ return 1; // memory allocation error
+ }
+
+ return 0; // simulate user answered just fine
+}
+
+int
+fake_conversation_with_suspend(int num_msgs, const struct sudo_conv_message msgs[],
+ struct sudo_conv_reply replies[], struct sudo_conv_callback *callback)
+{
+ if (callback != NULL) {
+ callback->on_suspend(SIGTSTP, callback->closure);
+ callback->on_resume(SIGCONT, callback->closure);
+ }
+
+ return fake_conversation(num_msgs, msgs, replies, callback);
+}
+
+int
+fake_printf(int msg_type, const char *fmt, ...)
+{
+ int rc = -1;
+ va_list args;
+ va_start(args, fmt);
+
+ char *output = NULL;
+ switch(msg_type) {
+ case SUDO_CONV_INFO_MSG:
+ output = data.stdout_str;
+ break;
+ case SUDO_CONV_ERROR_MSG:
+ output = data.stderr_str;
+ break;
+ default:
+ break;
+ }
+
+ if (output)
+ rc = vsnprintf_append(output, MAX_OUTPUT, fmt, args);
+
+ va_end(args);
+ return rc;
+}
+
+int
+verify_log_lines(const char *reference_path)
+{
+ char stored_path[PATH_MAX];
+ snprintf(stored_path, sizeof(stored_path), "%s/%s", data.tmp_dir, "debug.log");
+
+ FILE *file = fopen(stored_path, "rb");
+ if (file == NULL) {
+ printf("Failed to open file '%s'\n", stored_path);
+ return false;
+ }
+
+ char line[1024] = "";
+ char stored_str[MAX_OUTPUT] = "";
+ while (fgets(line, sizeof(line), file) != NULL) {
+ char *line_data = strstr(line, "] "); // this skips the timestamp and pid at the beginning
+ VERIFY_NOT_NULL(line_data); // malformed log line
+ line_data += 2;
+
+ char *line_end = strstr(line_data, " object at "); // this skips checking the pointer hex
+ if (line_end)
+ snprintf(line_end, sizeof(line) - (line_end - line), " object>\n");
+
+ if (strncmp(line_data, "handle @ /", sizeof("handle @ /") - 1) == 0) {
+ char *start = line_data + sizeof("handle @ ") - 1;
+
+ // normalize path to logging/__init__.py
+ char *logging = strstr(start, "logging/");
+ if (logging != NULL) {
+ memmove(start, logging, strlen(logging) + 1);
+ }
+
+ // remove line number
+ char *colon = strchr(start, ':');
+ if (colon != NULL) {
+ size_t len = strspn(colon + 1, "0123456789");
+ if (len != 0)
+ memmove(colon, colon + len + 1, strlen(colon + len + 1) + 1);
+ }
+ } else if (strncmp(line_data, "LogHandler.emit was called ", 27) == 0) {
+ // LogHandler.emit argument details vary based on python version
+ line_data[26] = '\n';
+ line_data[27] = '\0';
+ } else {
+ // Python 3.11 uses 0 instead of the symbolic REJECT in backtraces
+ char *cp = strstr(line_data, ": REJECT");
+ if (cp != NULL) {
+ // Convert ": REJECT" to ": 0" + rest of line
+ memcpy(cp, ": 0", 3);
+ memmove(cp + 3, cp + 8, strlen(cp + 8) + 1);
+ }
+ }
+
+ VERIFY_TRUE(strlcat(stored_str, line_data, sizeof(stored_str)) < sizeof(stored_str)); // we have enough space in buffer
+ }
+
+ clean_output(stored_str);
+
+ VERIFY_TRUE(verify_content(stored_str, reference_path));
+ return true;
+}
+
+int
+verify_str_set(char **actual_set, char **expected_set, const char *actual_variable_name)
+{
+ VERIFY_NOT_NULL(actual_set);
+ VERIFY_NOT_NULL(expected_set);
+
+ int actual_len = str_array_count(actual_set);
+ int expected_len = str_array_count(expected_set);
+
+ int matches = false;
+ if (actual_len == expected_len) {
+ int actual_pos = 0;
+ for (; actual_pos < actual_len; ++actual_pos) {
+ char *actual_item = actual_set[actual_pos];
+
+ int expected_pos = 0;
+ for (; expected_pos < expected_len; ++expected_pos) {
+ if (strcmp(actual_item, expected_set[expected_pos]) == 0)
+ break;
+ }
+
+ if (expected_pos == expected_len) {
+ // matching item was not found
+ break;
+ }
+ }
+
+ matches = (actual_pos == actual_len);
+ }
+
+ if (!matches) {
+ char actual_set_str[MAX_OUTPUT] = "";
+ char expected_set_str[MAX_OUTPUT] = "";
+ str_array_snprint(actual_set_str, MAX_OUTPUT, actual_set, actual_len);
+ str_array_snprint(expected_set_str, MAX_OUTPUT, expected_set, expected_len);
+
+ VERIFY_PRINT_MSG("%s", actual_variable_name, actual_set_str, "expected",
+ expected_set_str, "expected to contain the same elements as");
+ return false;
+ }
+
+ return true;
+}
+
+int
+mock_python_datetime_now(const char *plugin_name, const char *date_str)
+{
+ char *cmd = NULL;
+ int len;
+ len = asprintf(&cmd,
+ "import %s\n" // the plugin has its own submodule
+ "from datetime import datetime\n" // store the real datetime
+ "import time\n"
+ "from unittest.mock import Mock\n"
+ "%s.datetime = Mock()\n" // replace plugin's datetime
+ "%s.datetime.now = lambda: datetime.strptime('%s', '%%Y-%%m-%%dT%%H:%%M:%%S')\n",
+ plugin_name, plugin_name, plugin_name, date_str);
+ if (len == -1)
+ return false;
+ VERIFY_PTR_NE(cmd, NULL);
+ VERIFY_INT(PyRun_SimpleString(cmd), 0);
+ free(cmd);
+ return true;
+}
diff --git a/plugins/python/regress/testhelpers.h b/plugins/python/regress/testhelpers.h
new file mode 100644
index 0000000..2dd1d54
--- /dev/null
+++ b/plugins/python/regress/testhelpers.h
@@ -0,0 +1,175 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2020 Robert Manner <robert.manner@oneidentity.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef PYTHON_TESTHELPERS
+#define PYTHON_TESTHELPERS
+
+#include "iohelpers.h"
+
+#include "../pyhelpers.h"
+
+#include "sudo_conf.h"
+
+// just for the IDE
+#ifndef SRC_DIR
+#define SRC_DIR ""
+#endif
+#define TESTDATA_DIR SRC_DIR "/regress/testdata/"
+
+#define TEMP_PATH_TEMPLATE "/tmp/sudo_check_python_exampleXXXXXX"
+
+extern struct TestData {
+ char *tmp_dir;
+ char *tmp_dir2;
+ char stdout_str[MAX_OUTPUT];
+ char stderr_str[MAX_OUTPUT];
+
+ char conv_str[MAX_OUTPUT];
+ const char *conv_replies[8];
+
+ // some example test data used by multiple test cases:
+ char ** settings;
+ char ** user_info;
+ char ** command_info;
+ char ** plugin_argv;
+ int plugin_argc;
+ char ** user_env;
+ char ** plugin_options;
+} data;
+
+const char * expected_path(const char *format, ...);
+
+char ** create_str_array(size_t count, ...);
+
+#define RUN_TEST(testcase) \
+ do { \
+ int success = 1; \
+ ntests++; \
+ if (verbose) { \
+ printf("Running test " #testcase " ... \n"); \
+ } \
+ if (!init()) { \
+ printf("FAILED: initialization of testcase %s at %s:%d\n", #testcase, __FILE__, __LINE__); \
+ success = 0; \
+ } else \
+ if (!testcase) { \
+ printf("FAILED: testcase %s at %s:%d\n", #testcase, __FILE__, __LINE__); \
+ success = 0; \
+ } \
+ if (!cleanup(success)) { \
+ printf("FAILED: deinitialization of testcase %s at %s:%d\n", #testcase, __FILE__, __LINE__); \
+ success = 0; \
+ } \
+ if (!success) { \
+ errors++; \
+ } \
+ } while(false)
+
+#define VERIFY_PRINT_MSG(fmt, actual_str, actual, expected_str, expected, expected_to_be_message) \
+ printf("Expectation failed at %s:%d:\n actual is <<" fmt ">>: %s\n %s <<" fmt ">>: %s\n", \
+ __FILE__, __LINE__, actual, actual_str, expected_to_be_message, expected, expected_str)
+
+#define VERIFY_CUSTOM(fmt, type, actual, expected, invert) \
+ do { \
+ type actual_value = (type)(actual); \
+ int failed = (actual_value != expected); \
+ if (invert) \
+ failed = !failed; \
+ if (failed) { \
+ VERIFY_PRINT_MSG(fmt, #actual, actual_value, #expected, expected, invert ? "not expected to be" : "expected to be"); \
+ return false; \
+ } \
+ } while(false)
+
+#define VERIFY_EQ(fmt, type, actual, expected) VERIFY_CUSTOM(fmt, type, actual, expected, false)
+#define VERIFY_NE(fmt, type, actual, not_expected) VERIFY_CUSTOM(fmt, type, actual, not_expected, true)
+
+#define VERIFY_INT(actual, expected) VERIFY_EQ("%d", int, actual, expected)
+
+#define VERIFY_PTR(actual, expected) VERIFY_EQ("%p", const void *, (const void *)actual, (const void *)expected)
+#define VERIFY_PTR_NE(actual, not_expected) VERIFY_NE("%p", const void *, (const void *)actual, (const void *)not_expected)
+
+#define VERIFY_TRUE(actual) VERIFY_NE("%d", int, actual, 0)
+#define VERIFY_FALSE(actual) VERIFY_INT(actual, false)
+
+#define VERIFY_NOT_NULL(actual) VERIFY_NE("%p", const void *, actual, NULL)
+
+#define VERIFY_STR(actual, expected) \
+ do { \
+ const char *actual_str = actual; \
+ if (!actual_str || strcmp(actual_str, expected) != 0) { \
+ VERIFY_PRINT_MSG("%s", #actual, actual_str ? actual_str : "(null)", #expected, expected, "expected to be"); \
+ return false; \
+ } \
+ } while(false)
+
+#define VERIFY_STR_CONTAINS(actual, expected) \
+ do { \
+ const char *actual_str = actual; \
+ if (!actual_str || strstr(actual_str, expected) == NULL) { \
+ VERIFY_PRINT_MSG("%s", #actual, actual_str ? actual_str : "(null)", #expected, expected, "expected to contain the string"); \
+ return false; \
+ } \
+ } while(false)
+
+int is_update(void);
+
+int verify_content(char *actual_content, const char *reference_path);
+
+#define VERIFY_CONTENT(actual_output, reference_path) \
+ VERIFY_TRUE(verify_content(actual_output, reference_path))
+
+#define VERIFY_STDOUT(reference_path) \
+ VERIFY_CONTENT(data.stdout_str, reference_path)
+
+#define VERIFY_STDERR(reference_path) \
+ VERIFY_CONTENT(data.stderr_str, reference_path)
+
+#define VERIFY_CONV(reference_name) \
+ VERIFY_CONTENT(data.conv_str, reference_name)
+
+int verify_file(const char *actual_dir, const char *actual_file_name, const char *reference_path);
+
+#define VERIFY_FILE(actual_file_name, reference_path) \
+ VERIFY_TRUE(verify_file(data.tmp_dir, actual_file_name, reference_path))
+
+int fake_conversation(int num_msgs, const struct sudo_conv_message msgs[],
+ struct sudo_conv_reply replies[], struct sudo_conv_callback *callback);
+
+int fake_conversation_with_suspend(int num_msgs, const struct sudo_conv_message msgs[],
+ struct sudo_conv_reply replies[], struct sudo_conv_callback *callback);
+
+int fake_printf(int msg_type, const char *fmt, ...);
+
+int verify_log_lines(const char *reference_path);
+
+int mock_python_datetime_now(const char *plugin_name, const char *date_str);
+
+#define VERIFY_LOG_LINES(reference_path) \
+ VERIFY_TRUE(verify_log_lines(reference_path))
+
+int verify_str_set(char **actual_set, char **expected_set, const char *actual_variable_name);
+
+#define VERIFY_STR_SET(actual_set, ...) \
+ do { \
+ char **expected_set = create_str_array(__VA_ARGS__); \
+ VERIFY_TRUE(verify_str_set(actual_set, expected_set, #actual_set)); \
+ str_array_free(&expected_set); \
+ } while(false)
+
+#endif // PYTHON_TESTHELPERS
diff --git a/plugins/python/sudo_python_debug.c b/plugins/python/sudo_python_debug.c
new file mode 100644
index 0000000..83e6845
--- /dev/null
+++ b/plugins/python/sudo_python_debug.c
@@ -0,0 +1,129 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 Todd C. Miller <Todd.Miller@sudo.ws>
+ *
+ * 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 <config.h>
+#include <string.h>
+
+#include <ctype.h>
+#include <stdlib.h>
+
+#include "sudo_gettext.h"
+#include "sudo_compat.h"
+#include "sudo_python_debug.h"
+#include "sudo_queue.h"
+#include "sudo_conf.h"
+#include "sudo_fatal.h"
+
+
+static int python_debug_instance = SUDO_DEBUG_INSTANCE_INITIALIZER;
+static unsigned int python_debug_refcnt;
+
+static const char *const python_subsystem_names[] = {
+ "py_calls", // logs c -> py calls
+ "c_calls", // logs py -> c calls
+ "load", // logs python plugin loading / unloading
+ "sudo_cb", // logs sudo callback calls
+ "internal", // logs internal functions of the language wrapper plugin
+ "plugin", // logs whatever log the python module would like to log through sudo.debug API
+ NULL
+};
+
+#define NUM_SUBSYSTEMS sizeof(python_subsystem_names) / sizeof(*python_subsystem_names) - 1
+
+/* Subsystem IDs assigned at registration time. */
+int python_subsystem_ids[NUM_SUBSYSTEMS];
+
+/*
+ * Parse the "filename flags,..." debug_flags entry and insert a new
+ * sudo_debug_file struct into debug_files.
+ */
+bool
+python_debug_parse_flags(struct sudo_conf_debug_file_list *debug_files,
+ const char *entry)
+{
+ /* Already initialized? */
+ if (python_debug_instance != SUDO_DEBUG_INSTANCE_INITIALIZER)
+ return true;
+
+ return sudo_debug_parse_flags(debug_files, entry) != -1;
+}
+
+/*
+ * Register the specified debug files and program with the
+ * debug subsystem, freeing the debug list when done.
+ * Sets the active debug instance as a side effect.
+ */
+bool
+python_debug_register(const char *program,
+ struct sudo_conf_debug_file_list *debug_files)
+{
+ int instance = python_debug_instance;
+ struct sudo_debug_file *debug_file, *debug_next;
+
+ /* Setup debugging if indicated. */
+ if (debug_files != NULL && !TAILQ_EMPTY(debug_files)) {
+ if (program != NULL) {
+ instance = sudo_debug_register(program, python_subsystem_names,
+ (unsigned int *)python_subsystem_ids, debug_files, -1);
+ }
+ TAILQ_FOREACH_SAFE(debug_file, debug_files, entries, debug_next) {
+ TAILQ_REMOVE(debug_files, debug_file, entries);
+ free(debug_file->debug_file);
+ free(debug_file->debug_flags);
+ free(debug_file);
+ }
+ }
+
+ switch (instance) {
+ case SUDO_DEBUG_INSTANCE_ERROR:
+ return false;
+ case SUDO_DEBUG_INSTANCE_INITIALIZER:
+ /* Nothing to do */
+ break;
+ default:
+ /* New debug instance or additional reference on existing one. */
+ python_debug_instance = instance;
+ sudo_debug_set_active_instance(python_debug_instance);
+ python_debug_refcnt++;
+ break;
+ }
+
+ return true;
+}
+
+/*
+ * Deregister python_debug_instance if it is registered.
+ */
+void
+python_debug_deregister(void)
+{
+ debug_decl(python_debug_deregister, PYTHON_DEBUG_INTERNAL);
+
+ if (python_debug_refcnt != 0) {
+ sudo_debug_exit(__func__, __FILE__, __LINE__, sudo_debug_subsys);
+ if (--python_debug_refcnt == 0) {
+ if (sudo_debug_deregister(python_debug_instance) < 1)
+ python_debug_instance = SUDO_DEBUG_INSTANCE_INITIALIZER;
+ }
+ }
+}
diff --git a/plugins/python/sudo_python_debug.h b/plugins/python/sudo_python_debug.h
new file mode 100644
index 0000000..02a715a
--- /dev/null
+++ b/plugins/python/sudo_python_debug.h
@@ -0,0 +1,46 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2014 Todd C. Miller <Todd.Miller@sudo.ws>
+ *
+ * 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 SUDO_PYTHON_DEBUG_H
+#define SUDO_PYTHON_DEBUG_H
+
+#include "sudo_debug.h"
+
+/*
+ * Sudo python plugin debug subsystems.
+ * Note that python_subsystem_ids[] is filled in at debug registration time.
+ */
+extern int python_subsystem_ids[];
+#define PYTHON_DEBUG_PY_CALLS (python_subsystem_ids[0])
+#define PYTHON_DEBUG_C_CALLS (python_subsystem_ids[1])
+#define PYTHON_DEBUG_PLUGIN_LOAD (python_subsystem_ids[2])
+#define PYTHON_DEBUG_CALLBACKS (python_subsystem_ids[3])
+#define PYTHON_DEBUG_INTERNAL (python_subsystem_ids[4])
+#define PYTHON_DEBUG_PLUGIN (python_subsystem_ids[5])
+
+bool python_debug_parse_flags(struct sudo_conf_debug_file_list *debug_files, const char *entry);
+bool python_debug_register(const char *program, struct sudo_conf_debug_file_list *debug_files);
+void python_debug_deregister(void);
+
+#define debug_return_ptr_pynone \
+ do { \
+ Py_INCREF(Py_None); \
+ debug_return_ptr(Py_None); \
+ } while(0)
+
+#endif /* SUDO_PYTHON_DEBUG_H */
diff --git a/plugins/python/sudo_python_module.c b/plugins/python/sudo_python_module.c
new file mode 100644
index 0000000..ee94101
--- /dev/null
+++ b/plugins/python/sudo_python_module.c
@@ -0,0 +1,612 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 Robert Manner <robert.manner@oneidentity.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * This is an open source non-commercial project. Dear PVS-Studio, please check it.
+ * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
+ */
+
+#include "sudo_python_module.h"
+
+#define EXC_VAR(exception_name) sudo_exc_ ## exception_name
+#define TYPE_VAR(type_name) &sudo_type_ ## type_name
+
+// exceptions:
+PyObject *sudo_exc_SudoException;
+PyObject *sudo_exc_PluginException;
+PyObject *sudo_exc_PluginError;
+PyObject *sudo_exc_PluginReject;
+static PyObject *sudo_exc_ConversationInterrupted;
+
+// the methods exposed in the "sudo" python module
+// "args" is a tuple (~= const list) containing all the unnamed arguments
+// "kwargs" is a dict of the keyword arguments or NULL if there are none
+static PyObject *python_sudo_log_info(PyObject *py_self, PyObject *py_args, PyObject *py_kwargs);
+static PyObject *python_sudo_log_error(PyObject *py_self, PyObject *py_args, PyObject *py_kwargs);
+static PyObject *python_sudo_conversation(PyObject *py_self, PyObject *py_args, PyObject *py_kwargs);
+static PyObject *python_sudo_options_as_dict(PyObject *py_self, PyObject *py_args);
+static PyObject *python_sudo_options_from_dict(PyObject *py_self, PyObject *py_args);
+
+static PyMethodDef sudo_methods[] = {
+ {"debug", (PyCFunction)python_sudo_debug, METH_VARARGS, "Debug messages which can be saved to file in sudo.conf."},
+ {"log_info", (PyCFunction)python_sudo_log_info, METH_VARARGS | METH_KEYWORDS, "Display informational messages."},
+ {"log_error", (PyCFunction)python_sudo_log_error, METH_VARARGS | METH_KEYWORDS, "Display error messages."},
+ {"conv", (PyCFunction)python_sudo_conversation, METH_VARARGS | METH_KEYWORDS, "Interact with the user"},
+ {"options_as_dict", python_sudo_options_as_dict, METH_VARARGS, "Convert a string tuple in key=value format to a dictionary."},
+ {"options_from_dict", python_sudo_options_from_dict, METH_VARARGS, "Convert a dictionary to a tuple of strings in key=value format."},
+ {NULL, NULL, 0, NULL} /* Sentinel */
+};
+
+static struct PyModuleDef sudo_module = {
+ PyModuleDef_HEAD_INIT,
+ "sudo", /* name of module */
+ NULL, /* module documentation, may be NULL */
+ -1, /* size of per-interpreter state of the module,
+ or -1 if the module keeps state in global variables. */
+ sudo_methods,
+ NULL, /* slots */
+ NULL, /* traverse */
+ NULL, /* clear */
+ NULL /* free */
+};
+
+CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
+static int
+_parse_log_function_args(PyObject *py_args, PyObject *py_kwargs, char **args_joined, const char ** end)
+{
+ debug_decl(python_sudo_log, PYTHON_DEBUG_INTERNAL);
+
+ int rc = SUDO_RC_ERROR;
+ PyObject *py_empty = NULL;
+
+ const char *sep = NULL;
+ py_empty = PyTuple_New(0);
+ if (py_empty == NULL)
+ goto cleanup;
+
+ static const char *keywords[] = { "sep", "end", NULL };
+ if (py_kwargs != NULL && !PyArg_ParseTupleAndKeywords(py_empty, py_kwargs, "|zz:sudo.log", (char **)keywords, &sep, end))
+ goto cleanup;
+
+ if (sep == NULL)
+ sep = " ";
+
+ if (*end == NULL)
+ *end = "\n";
+
+ // this is to mimic the behaviour of python "print" / "log"
+ *args_joined = py_join_str_list(py_args, sep);
+ if (!PyErr_Occurred()) // == (*args_joined != NULL), but cpychecker does not understand that
+ rc = SUDO_RC_OK;
+
+cleanup:
+ Py_CLEAR(py_empty);
+ debug_return_int(rc);
+}
+
+static PyObject *
+python_sudo_log(int msg_type, PyObject *Py_UNUSED(py_self), PyObject *py_args, PyObject *py_kwargs)
+{
+ debug_decl(python_sudo_log, PYTHON_DEBUG_C_CALLS);
+ py_debug_python_call("sudo", "log", py_args, py_kwargs, PYTHON_DEBUG_C_CALLS);
+
+ int rc = SUDO_RC_ERROR;
+
+ char *args_joined = NULL;
+ const char *end = NULL;
+ if (_parse_log_function_args(py_args, py_kwargs, &args_joined, &end) != SUDO_RC_OK)
+ goto cleanup;
+
+ rc = py_ctx.sudo_log(msg_type, "%s%s", args_joined, end);
+ if (rc < 0) {
+ PyErr_Format(sudo_exc_SudoException, "sudo.log: Error displaying message");
+ goto cleanup;
+ }
+
+cleanup:
+ free(args_joined);
+
+ PyObject *py_result = PyErr_Occurred() ? NULL : PyLong_FromLong(rc);
+
+ py_debug_python_result("sudo", "log", py_result, PYTHON_DEBUG_C_CALLS);
+ debug_return_ptr(py_result);
+}
+
+static PyObject *
+python_sudo_options_as_dict(PyObject *py_self, PyObject *py_args)
+{
+ (void) py_self;
+
+ debug_decl(python_sudo_options_as_dict, PYTHON_DEBUG_C_CALLS);
+ py_debug_python_call("sudo", "options_as_dict", py_args, NULL, PYTHON_DEBUG_C_CALLS);
+
+ PyObject *py_config_tuple = NULL,
+ *py_result = NULL,
+ *py_config_tuple_iterator = NULL,
+ *py_config = NULL,
+ *py_splitted = NULL,
+ *py_separator = NULL;
+
+ if (!PyArg_ParseTuple(py_args, "O:sudo.options_as_dict", &py_config_tuple))
+ goto cleanup;
+
+ py_config_tuple_iterator = PyObject_GetIter(py_config_tuple);
+ if (py_config_tuple_iterator == NULL)
+ goto cleanup;
+
+ py_result = PyDict_New();
+ if (py_result == NULL)
+ goto cleanup;
+
+ py_separator = PyUnicode_FromString("=");
+ if (py_separator == NULL)
+ goto cleanup;
+
+ while ((py_config = PyIter_Next(py_config_tuple_iterator)) != NULL) {
+ py_splitted = PyUnicode_Split(py_config, py_separator, 1);
+ if (py_splitted == NULL)
+ goto cleanup;
+
+ PyObject *py_key = PyList_GetItem(py_splitted, 0); // borrowed ref
+ if (py_key == NULL)
+ goto cleanup;
+
+ PyObject *py_value = PyList_GetItem(py_splitted, 1);
+ if (py_value == NULL) { // skip values without a key
+ Py_CLEAR(py_config);
+ Py_CLEAR(py_splitted);
+ PyErr_Clear();
+ continue;
+ }
+
+ if (PyDict_SetItem(py_result, py_key, py_value) != 0) {
+ goto cleanup;
+ }
+
+ Py_CLEAR(py_config);
+ Py_CLEAR(py_splitted);
+ }
+
+cleanup:
+ Py_CLEAR(py_config_tuple_iterator);
+ Py_CLEAR(py_config);
+ Py_CLEAR(py_splitted);
+ Py_CLEAR(py_separator);
+
+ if (PyErr_Occurred()) {
+ Py_CLEAR(py_result);
+ }
+
+ py_debug_python_result("sudo", "options_as_dict", py_result, PYTHON_DEBUG_C_CALLS);
+ debug_return_ptr(py_result);
+}
+
+static PyObject *
+python_sudo_options_from_dict(PyObject *py_self, PyObject *py_args)
+{
+ (void) py_self;
+ debug_decl(python_sudo_options_from_dict, PYTHON_DEBUG_C_CALLS);
+ py_debug_python_call("sudo", "options_from_dict", py_args, NULL, PYTHON_DEBUG_C_CALLS);
+
+ PyObject *py_config_dict = NULL,
+ *py_result = NULL;
+
+ if (!PyArg_ParseTuple(py_args, "O!:sudo.options_from_dict", &PyDict_Type, &py_config_dict))
+ goto cleanup;
+
+ Py_ssize_t dict_size = PyDict_Size(py_config_dict);
+ py_result = PyTuple_New(dict_size);
+ if (py_result == NULL)
+ goto cleanup;
+
+ PyObject *py_key = NULL, *py_value = NULL; // -> borrowed references
+ Py_ssize_t i, pos = 0;
+ for (i = 0; PyDict_Next(py_config_dict, &pos, &py_key, &py_value); i++) {
+ PyObject *py_config = PyUnicode_FromFormat("%S%s%S", py_key, "=", py_value);
+ if (py_config == NULL)
+ goto cleanup;
+
+ /* Dictionaries are sparse so we cannot use pos as an index. */
+ if (PyTuple_SetItem(py_result, i, py_config) != 0) { // this steals a reference, even on error
+ goto cleanup;
+ }
+ }
+
+cleanup:
+ if (PyErr_Occurred()) {
+ Py_CLEAR(py_result);
+ }
+
+ py_debug_python_result("sudo", "options_from_dict", py_result, PYTHON_DEBUG_C_CALLS);
+ debug_return_ptr(py_result);
+}
+
+static PyObject *
+python_sudo_log_info(PyObject *py_self, PyObject *py_args, PyObject *py_kwargs)
+{
+ return python_sudo_log(SUDO_CONV_INFO_MSG, py_self, py_args, py_kwargs);
+}
+
+static PyObject *
+python_sudo_log_error(PyObject *py_self, PyObject *py_args, PyObject *py_kwargs)
+{
+ return python_sudo_log(SUDO_CONV_ERROR_MSG, py_self, py_args, py_kwargs);
+}
+
+CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
+static int py_expect_arg_callable(PyObject *py_callable,
+ const char *func_name, const char *arg_name)
+{
+ debug_decl(py_expect_arg_callable, PYTHON_DEBUG_INTERNAL);
+
+ if (!PyCallable_Check(py_callable)) {
+ PyErr_Format(PyExc_ValueError, "%s: %s argument must be python callable (got %s) ",
+ func_name, arg_name, Py_TYPENAME(py_callable));
+ debug_return_int(-1);
+ }
+
+ debug_return_int(0);
+}
+
+struct py_conv_callback_closure
+{
+ PyObject *py_on_suspend;
+ PyObject *py_on_resume;
+};
+
+static int
+_call_conversation_callback(PyObject *py_callback, int signo)
+{
+ debug_decl(_call_conversation_callback, PYTHON_DEBUG_INTERNAL);
+
+ if (py_callback == NULL || py_callback == Py_None)
+ debug_return_int(0); // nothing to do
+
+ PyObject *py_result = PyObject_CallFunction(py_callback, "(i)", signo);
+
+ int rc = -1;
+
+ // We treat sudo.RC_OK (1) and None (no exception occurred) as success as well to avoid confusion
+ if (py_result && (py_result == Py_None || PyLong_AsLong(py_result) >= 0))
+ rc = 0;
+
+ Py_CLEAR(py_result);
+
+ if (rc != 0)
+ py_log_last_error("Error during conversation callback");
+
+ debug_return_int(rc);
+}
+
+static int
+python_sudo_conversation_suspend_cb(int signo, struct py_conv_callback_closure *closure)
+{
+ return _call_conversation_callback(closure->py_on_suspend, signo);
+}
+
+static int
+python_sudo_conversation_resume_cb(int signo, struct py_conv_callback_closure *closure)
+{
+ return _call_conversation_callback(closure->py_on_resume, signo);
+}
+
+static PyObject *
+python_sudo_conversation(PyObject *Py_UNUSED(self), PyObject *py_args, PyObject *py_kwargs)
+{
+ debug_decl(python_sudo_conversation, PYTHON_DEBUG_C_CALLS);
+ py_debug_python_call("sudo", "conv", py_args, py_kwargs, PYTHON_DEBUG_C_CALLS);
+
+ PyObject *py_result = NULL, *py_empty = NULL;
+ Py_ssize_t num_msgs = 0;
+ struct sudo_conv_message *msgs = NULL;
+ struct sudo_conv_reply *replies = NULL;
+
+ // Note, they are both borrowed references of py_kwargs
+ struct py_conv_callback_closure callback_closure = { NULL, NULL };
+
+ struct sudo_conv_callback callback = {
+ SUDO_CONV_CALLBACK_VERSION,
+ &callback_closure,
+ (sudo_conv_callback_fn_t)python_sudo_conversation_suspend_cb,
+ (sudo_conv_callback_fn_t)python_sudo_conversation_resume_cb
+ };
+
+ py_empty = PyTuple_New(0);
+ if (py_empty == NULL)
+ goto cleanup;
+
+ static const char *keywords[] = { "on_suspend", "on_resume", NULL };
+ if (py_kwargs != NULL && !PyArg_ParseTupleAndKeywords(py_empty, py_kwargs, "|OO:sudo.conv", (char **)keywords,
+ &callback_closure.py_on_suspend,
+ &callback_closure.py_on_resume))
+ goto cleanup;
+
+ if (callback_closure.py_on_suspend != NULL &&
+ py_expect_arg_callable(callback_closure.py_on_suspend, "sudo.conv", "on_suspend") < 0) {
+ goto cleanup;
+ }
+
+ if (callback_closure.py_on_resume != NULL &&
+ py_expect_arg_callable(callback_closure.py_on_resume, "sudo.conv", "on_resume") < 0) {
+ goto cleanup;
+ }
+
+ /* sudo_module_ConvMessages_to_c() returns error if no messages. */
+ if (sudo_module_ConvMessages_to_c(py_args, &num_msgs, &msgs) < 0) {
+ goto cleanup;
+ }
+
+ replies = calloc(num_msgs, sizeof(struct sudo_conv_reply));
+ if (replies == NULL)
+ goto cleanup;
+ py_result = PyTuple_New(num_msgs);
+ if (py_result == NULL)
+ goto cleanup;
+
+ if (py_ctx.sudo_conv == NULL) {
+ PyErr_Format(sudo_exc_SudoException, "%s: conversation is unavailable",
+ __func__);
+ goto cleanup;
+ }
+
+ int rc = py_sudo_conv((int)num_msgs, msgs, replies, &callback);
+ if (rc != 0) {
+ PyErr_Format(sudo_exc_ConversationInterrupted,
+ "%s: conversation was interrupted", __func__, rc);
+ goto cleanup;
+ }
+
+ for (Py_ssize_t i = 0; i < num_msgs; ++i) {
+ char *reply = replies[i].reply;
+ if (reply != NULL) {
+ PyObject *py_reply = PyUnicode_FromString(reply);
+ if (py_reply == NULL) {
+ goto cleanup;
+ }
+
+ if (PyTuple_SetItem(py_result, i, py_reply) != 0) { // this steals a reference even on error
+ PyErr_Format(sudo_exc_SudoException, "%s: failed to set tuple item", __func__);
+ goto cleanup;
+ }
+
+ sudo_debug_printf(SUDO_DEBUG_DIAG, "user reply for conversation: '%s'\n", reply);
+ }
+ }
+
+cleanup:
+ Py_CLEAR(py_empty);
+ if (replies != NULL) {
+ for (int i = 0; i < num_msgs; ++i)
+ free(replies[i].reply);
+ }
+ free(msgs);
+ free(replies);
+
+ if (PyErr_Occurred()) {
+ Py_CLEAR(py_result); // we return NULL
+ }
+
+ py_debug_python_result("sudo", "conv", py_result, PYTHON_DEBUG_C_CALLS);
+
+ debug_return_ptr(py_result);
+}
+
+/*
+ * Create a python class.
+ * Class name must be a full name including module, eg. "sudo.MyFavouriteClass".
+ * The resulting class object can be added to a module using PyModule_AddObject.
+ */
+PyObject *
+sudo_module_create_class(const char *class_name, PyMethodDef *class_methods,
+ PyObject *base_class)
+{
+ debug_decl(sudo_module_create_class, PYTHON_DEBUG_INTERNAL);
+
+ PyObject *py_base_classes = NULL, *py_class = NULL, *py_member_dict = NULL;
+
+ if (base_class == NULL) {
+ py_base_classes = PyTuple_New(0);
+ } else {
+ py_base_classes = Py_BuildValue("(O)", base_class);
+ }
+
+ if (py_base_classes == NULL)
+ goto cleanup;
+
+ py_member_dict = PyDict_New();
+ if (py_member_dict == NULL)
+ goto cleanup;
+
+ for (PyMethodDef *py_def = class_methods; py_def->ml_name != NULL; ++py_def) {
+ PyObject *py_func = PyCFunction_New(py_def, NULL);
+ if (py_func == NULL) {
+ goto cleanup;
+ }
+
+ // this wrapping makes the function get the 'self' as argument
+ PyObject *py_method = PyInstanceMethod_New(py_func);
+ if (py_method == NULL) {
+ Py_DECREF(py_func);
+ goto cleanup;
+ }
+
+ int rc = PyDict_SetItemString(py_member_dict, py_def->ml_name, py_method);
+
+ Py_XDECREF(py_func);
+ Py_XDECREF(py_method);
+
+ if (rc != 0)
+ goto cleanup;
+ }
+
+ py_class = PyObject_CallFunction((PyObject *)&PyType_Type, "(sOO)",
+ class_name,
+ py_base_classes,
+ py_member_dict);
+
+cleanup:
+ Py_CLEAR(py_base_classes);
+ Py_CLEAR(py_member_dict);
+
+ debug_return_ptr(py_class);
+}
+
+CPYCHECKER_STEALS_REFERENCE_TO_ARG(3)
+static void
+sudo_module_register_enum(PyObject *py_module, const char *enum_name, PyObject *py_constants_dict)
+{
+ // pseudo code:
+ // return enum.IntEnum('MyEnum', {'DEFINITION_NAME': DEFINITION_VALUE, ...})
+
+ debug_decl(sudo_module_register_enum, PYTHON_DEBUG_INTERNAL);
+
+ if (py_constants_dict == NULL)
+ return;
+
+ PyObject *py_enum_class = NULL;
+ {
+ PyObject *py_enum_module = PyImport_ImportModule("enum");
+ if (py_enum_module == NULL) {
+ Py_CLEAR(py_constants_dict);
+ debug_return;
+ }
+
+ py_enum_class = PyObject_CallMethod(py_enum_module,
+ "IntEnum", "sO", enum_name,
+ py_constants_dict);
+
+ Py_CLEAR(py_constants_dict);
+ Py_CLEAR(py_enum_module);
+ }
+
+ if (py_enum_class == NULL) {
+ debug_return;
+ }
+
+ if (PyModule_AddObject(py_module, enum_name, py_enum_class) < 0) {
+ Py_CLEAR(py_enum_class);
+ debug_return;
+ }
+
+ debug_return;
+}
+
+PyMODINIT_FUNC
+sudo_module_init(void)
+{
+ debug_decl(sudo_module_init, PYTHON_DEBUG_C_CALLS);
+
+ PyObject *py_module = PyModule_Create(&sudo_module);
+
+ if (py_module == NULL)
+ debug_return_ptr(NULL);
+
+ // Note: "PyModule_AddObject()" decrements the refcount only on success
+
+ // exceptions
+ #define MODULE_ADD_EXCEPTION(exception_name, base_exception) \
+ do { \
+ EXC_VAR(exception_name) = PyErr_NewException("sudo." # exception_name, base_exception, NULL); \
+ if (EXC_VAR(exception_name) == NULL || PyModule_AddObject(py_module, # exception_name, EXC_VAR(exception_name)) < 0) { \
+ Py_CLEAR(EXC_VAR(exception_name)); \
+ goto cleanup; \
+ } \
+ Py_INCREF(EXC_VAR(exception_name)); \
+ } while(0);
+
+ MODULE_ADD_EXCEPTION(SudoException, NULL);
+
+ MODULE_ADD_EXCEPTION(PluginException, NULL);
+ MODULE_ADD_EXCEPTION(PluginError, EXC_VAR(PluginException));
+ MODULE_ADD_EXCEPTION(PluginReject, EXC_VAR(PluginException));
+
+ MODULE_ADD_EXCEPTION(ConversationInterrupted, EXC_VAR(SudoException));
+
+ #define MODULE_REGISTER_ENUM(name, key_values) \
+ sudo_module_register_enum(py_module, name, py_dict_create_string_int(\
+ sizeof(key_values) / sizeof(struct key_value_str_int), key_values))
+
+ // constants
+ struct key_value_str_int constants_rc[] = {
+ {"OK", SUDO_RC_OK},
+ {"ACCEPT", SUDO_RC_ACCEPT},
+ {"REJECT", SUDO_RC_REJECT},
+ {"ERROR", SUDO_RC_ERROR},
+ {"USAGE_ERROR", SUDO_RC_USAGE_ERROR}
+ };
+ MODULE_REGISTER_ENUM("RC", constants_rc);
+
+ struct key_value_str_int constants_conv[] = {
+ {"PROMPT_ECHO_OFF", SUDO_CONV_PROMPT_ECHO_OFF},
+ {"PROMPT_ECHO_ON", SUDO_CONV_PROMPT_ECHO_ON},
+ {"INFO_MSG", SUDO_CONV_INFO_MSG},
+ {"PROMPT_MASK", SUDO_CONV_PROMPT_MASK},
+ {"PROMPT_ECHO_OK", SUDO_CONV_PROMPT_ECHO_OK},
+ {"PREFER_TTY", SUDO_CONV_PREFER_TTY}
+ };
+ MODULE_REGISTER_ENUM("CONV", constants_conv);
+
+ struct key_value_str_int constants_debug[] = {
+ {"CRIT", SUDO_DEBUG_CRIT},
+ {"ERROR", SUDO_DEBUG_ERROR},
+ {"WARN", SUDO_DEBUG_WARN},
+ {"NOTICE", SUDO_DEBUG_NOTICE},
+ {"DIAG", SUDO_DEBUG_DIAG},
+ {"INFO", SUDO_DEBUG_INFO},
+ {"TRACE", SUDO_DEBUG_TRACE},
+ {"DEBUG", SUDO_DEBUG_DEBUG}
+ };
+ MODULE_REGISTER_ENUM("DEBUG", constants_debug);
+
+ struct key_value_str_int constants_exit_reason[] = {
+ {"NO_STATUS", SUDO_PLUGIN_NO_STATUS},
+ {"WAIT_STATUS", SUDO_PLUGIN_WAIT_STATUS},
+ {"EXEC_ERROR", SUDO_PLUGIN_EXEC_ERROR},
+ {"SUDO_ERROR", SUDO_PLUGIN_SUDO_ERROR}
+ };
+ MODULE_REGISTER_ENUM("EXIT_REASON", constants_exit_reason);
+
+ struct key_value_str_int constants_plugin_types[] = {
+ {"POLICY", SUDO_POLICY_PLUGIN},
+ {"AUDIT", SUDO_AUDIT_PLUGIN},
+ {"IO", SUDO_IO_PLUGIN},
+ {"APPROVAL", SUDO_APPROVAL_PLUGIN},
+ {"SUDO", SUDO_FRONT_END}
+ };
+ MODULE_REGISTER_ENUM("PLUGIN_TYPE", constants_plugin_types);
+
+ // classes
+ if (sudo_module_register_conv_message(py_module) != SUDO_RC_OK)
+ goto cleanup;
+
+ if (sudo_module_register_baseplugin(py_module) != SUDO_RC_OK)
+ goto cleanup;
+
+ if (sudo_module_register_loghandler(py_module) != SUDO_RC_OK)
+ goto cleanup;
+
+cleanup:
+ if (PyErr_Occurred()) {
+ Py_CLEAR(py_module);
+ Py_CLEAR(sudo_exc_SudoException);
+ Py_CLEAR(sudo_exc_PluginError);
+ Py_CLEAR(sudo_exc_ConversationInterrupted);
+ }
+
+ debug_return_ptr(py_module);
+}
diff --git a/plugins/python/sudo_python_module.h b/plugins/python/sudo_python_module.h
new file mode 100644
index 0000000..dea8b83
--- /dev/null
+++ b/plugins/python/sudo_python_module.h
@@ -0,0 +1,61 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 Robert Manner <robert.manner@oneidentity.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef SUDO_PYTHON_MODULE_H
+#define SUDO_PYTHON_MODULE_H
+
+#include "pyhelpers.h"
+
+extern PyObject *sudo_exc_SudoException; // Base exception for the sudo module problems
+
+// This is for the python plugins to report error messages for us
+extern PyObject *sudo_exc_PluginException; // base exception of the following:
+extern PyObject *sudo_exc_PluginReject; // a reject with message
+extern PyObject *sudo_exc_PluginError; // an error with message
+
+extern PyTypeObject *sudo_type_Plugin;
+extern PyTypeObject *sudo_type_ConvMessage;
+
+extern PyObject *sudo_type_LogHandler;
+
+PyObject *sudo_module_create_class(const char *class_name, PyMethodDef *class_methods,
+ PyObject *base_class);
+
+CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
+int sudo_module_register_conv_message(PyObject *py_module);
+
+CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
+int sudo_module_ConvMessage_to_c(PyObject *py_conv_message, struct sudo_conv_message *conv_message);
+
+CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
+int sudo_module_ConvMessages_to_c(PyObject *py_tuple, Py_ssize_t *num_msgs, struct sudo_conv_message **msgs);
+
+CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
+int sudo_module_register_baseplugin(PyObject *py_module);
+
+CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
+int sudo_module_register_loghandler(PyObject *py_module);
+
+CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
+int sudo_module_set_default_loghandler(void);
+
+PyObject *python_sudo_debug(PyObject *py_self, PyObject *py_args);
+
+PyMODINIT_FUNC sudo_module_init(void);
+
+#endif // SUDO_PYTHON_MODULE_H