diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 12:52:13 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 12:52:13 +0000 |
commit | f8e5c55a036f0e2e2a958e30456270f3f9eba933 (patch) | |
tree | 4a06ff510774a7a3373e492df4e2984d7b0664b1 /plugins/python | |
parent | Initial commit. (diff) | |
download | sudo-f8e5c55a036f0e2e2a958e30456270f3f9eba933.tar.xz sudo-f8e5c55a036f0e2e2a958e30456270f3f9eba933.zip |
Adding upstream version 1.9.5p2.upstream/1.9.5p2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'plugins/python')
109 files changed, 8269 insertions, 0 deletions
diff --git a/plugins/python/Makefile.in b/plugins/python/Makefile.in new file mode 100644 index 0000000..5315ff0 --- /dev/null +++ b/plugins/python/Makefile.in @@ -0,0 +1,520 @@ +# +# 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@ +SED = @SED@ +AWK = @AWK@ + +# Our install program supports extra flags... +INSTALL = $(SHELL) $(top_srcdir)/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 +SSP_CFLAGS = @SSP_CFLAGS@ +SSP_LDFLAGS = @SSP_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_conversation.py example_debugging.py example_group_plugin.py example_io_plugin.py example_policy_plugin.py \ + example_audit_plugin.py example_approval_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_importblocker.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 + +CHECK_PYTHON_EXAMPLES_OBJS = check_python_examples.o iohelpers.o testhelpers.o pyhelpers.lo sudo_python_debug.lo + +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) $(SSP_CFLAGS) $< + +.c.lo: + $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_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) $(SSP_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 + +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 + +check: $(TEST_PROGS) + @if test X"$(cross_compiling)" != X"yes"; then \ + ./check_python_examples ".libs/python_plugin.so"; \ + fi + +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) $(SSP_LDFLAGS) $(LIBS) + +# 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) $(SSP_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)/sudo_compat.h \ + $(srcdir)/regress/iohelpers.h $(top_builddir)/config.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/regress/iohelpers.c +iohelpers.i: $(srcdir)/regress/iohelpers.c $(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) $(SSP_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 $@ +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) $(SSP_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) $(SSP_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_importblocker.lo: $(srcdir)/python_importblocker.c \ + $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_plugin.h \ + $(incdir)/sudo_queue.h $(incdir)/sudo_util.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) $(SSP_CFLAGS) $(srcdir)/python_importblocker.c +python_importblocker.i: $(srcdir)/python_importblocker.c \ + $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_plugin.h \ + $(incdir)/sudo_queue.h $(incdir)/sudo_util.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_importblocker.plog: python_importblocker.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/python_importblocker.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) $(SSP_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) $(SSP_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) $(SSP_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) $(SSP_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) $(SSP_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) $(SSP_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) $(SSP_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) $(SSP_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_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) $(SSP_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) $(SSP_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/pyhelpers.c b/plugins/python/pyhelpers.c new file mode 100644 index 0000000..6b235f3 --- /dev/null +++ b/plugins/python/pyhelpers.c @@ -0,0 +1,568 @@ +/* + * 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, +}; + + +int +py_is_sudo_log_available(void) +{ + debug_decl(py_is_sudo_log_available, PYTHON_DEBUG_INTERNAL); + debug_return_int(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); +} + +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(Py_SSIZE2SIZE(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 index, PyTypeObject *expected_type) +{ + debug_decl(py_tuple_get, PYTHON_DEBUG_INTERNAL); + + PyObject *py_item = PyTuple_GetItem(py_tuple, index); + 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')", + index, 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) { + result = strdup(bytes); + } + } + + 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 (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..01843d7 --- /dev/null +++ b/plugins/python/pyhelpers.h @@ -0,0 +1,105 @@ +/* + * 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_SSIZE2SIZE(value) ((value) < 0 ? 0 : (size_t)(value)) + +#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..67e6d96 --- /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 char *keywords[] = { "self", "msg_type", "msg", "timeout", NULL }; + if (!PyArg_ParseTupleAndKeywords(py_args ? py_args : py_empty, py_kwargs, "Ois|i:sudo.ConvMessage", 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(Py_SSIZE2SIZE(*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_importblocker.c b/plugins/python/python_importblocker.c new file mode 100644 index 0000000..2bce36c --- /dev/null +++ b/plugins/python/python_importblocker.c @@ -0,0 +1,213 @@ +/* + * 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" + +#include "sudo_util.h" + +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> + + +static int +_verify_import(const char *file_path) +{ + debug_decl(_verify_import, PYTHON_DEBUG_INTERNAL); + + // Check mode and owner similar to what we do in open_sudoers(). + // This is to help avoid loading a potentially insecure module. + struct stat sb; + if (stat(file_path, &sb) != 0) { + PyErr_Format(PyExc_ImportError, "Failed to stat file '%s'", file_path); + debug_return_int(SUDO_RC_ERROR); + } + + if (sb.st_uid != ROOT_UID) { + PyErr_Format(PyExc_ImportError, "File '%s' must be owned by uid %d", file_path, ROOT_UID); + debug_return_int(SUDO_RC_ERROR); + } + + if ((sb.st_mode & (S_IWGRP|S_IWOTH)) != 0) { + PyErr_Format(PyExc_ImportError, "File '%s' must be only be writable by owner", file_path); + debug_return_int(SUDO_RC_ERROR); + } + + debug_return_int(SUDO_RC_OK); +} + +static PyObject * +_sudo_ImportBlocker__Init(PyObject *py_self, PyObject *py_args) +{ + debug_decl(_sudo_ImportBlocker__Init, PYTHON_DEBUG_C_CALLS); + + py_debug_python_call("ImportBlocker", "__init__", py_args, NULL, PYTHON_DEBUG_C_CALLS); + + PyObject *py_meta_path = NULL; + if (!PyArg_UnpackTuple(py_args, "sudo.ImportBlocker.__init__", 2, 2, &py_self, &py_meta_path)) + goto cleanup; + + if (PyObject_SetAttrString(py_self, "meta_path", py_meta_path) != 0) + goto cleanup; + +cleanup: + if (PyErr_Occurred()) + debug_return_ptr(NULL); + + debug_return_ptr_pynone; +} + +static PyObject * +_sudo_ImportBlocker__find_module(PyObject *py_self, PyObject *py_args) +{ + debug_decl(_sudo_ImportBlocker__find_module, PYTHON_DEBUG_C_CALLS); + + PyObject *py_fullname = NULL, *py_path = NULL, *py_meta_path = NULL, + *py_meta_path_iterator = NULL, *py_finder = NULL, + *py_importer = NULL, *py_import_path = NULL; + + py_debug_python_call("ImportBlocker", "find_module", py_args, NULL, PYTHON_DEBUG_C_CALLS); + + if (!PyArg_UnpackTuple(py_args, "sudo.ImportBlocker.find_module", 2, 3, &py_self, &py_fullname, &py_path)) + goto cleanup; + + py_meta_path = PyObject_GetAttrString(py_self, "meta_path"); + if (py_meta_path == NULL) + goto cleanup; + + py_meta_path_iterator = PyObject_GetIter(py_meta_path); + if (py_meta_path_iterator == NULL) + goto cleanup; + + while ((py_finder = PyIter_Next(py_meta_path_iterator)) != NULL) { + py_importer = PyObject_CallMethod(py_finder, "find_module", "(OO)", + py_fullname, py_path); + if (py_importer == NULL) { + goto cleanup; + } + + if (py_importer != Py_None) { // the import could be resolved + if (PyObject_HasAttrString(py_importer, "get_filename")) { + // there is a file associated with the import (.py, .so, etc) + py_import_path = PyObject_CallMethod(py_importer, "get_filename", ""); + const char *import_path = PyUnicode_AsUTF8(py_import_path); + + sudo_debug_printf(SUDO_DEBUG_DIAG, "ImportBlocker: verifying permissions " + "on file '%s'\n", import_path); + if (_verify_import(import_path) != SUDO_RC_OK) + goto cleanup; + + Py_CLEAR(py_import_path); + + } else { + sudo_debug_printf(SUDO_DEBUG_DIAG, "ImportBlocker: internal module import '%s'\n", + PyUnicode_AsUTF8(py_fullname)); + } + + goto cleanup; + } + + Py_CLEAR(py_importer); + Py_CLEAR(py_finder); + } + + Py_CLEAR(py_importer); + py_importer = Py_None; + Py_INCREF(py_importer); + +cleanup: + Py_CLEAR(py_meta_path_iterator); + Py_CLEAR(py_meta_path); + Py_CLEAR(py_finder); + Py_CLEAR(py_import_path); + + if (PyErr_Occurred()) { + Py_CLEAR(py_importer); + debug_return_ptr(NULL); + } + + debug_return_ptr(py_importer); +} + +static PyMethodDef _sudo_ImportBlocker_class_methods[] = +{ + {"__init__", _sudo_ImportBlocker__Init, METH_VARARGS, ""}, + {"find_module", _sudo_ImportBlocker__find_module, METH_VARARGS, ""}, + {NULL, NULL, 0, NULL} +}; + +// This possibly can be replaced with PySys_AddAuditHook for python >= 3.8 +// +// This function is equivalent of the python call: +// sys.meta_path = [sudo.ImportBlocker(sys.meta_path)] +int +sudo_module_register_importblocker(void) +{ + debug_decl(sudo_module_register_importblocker, PYTHON_DEBUG_C_CALLS); + + int rc = SUDO_RC_ERROR; + + PyObject *py_meta_path = NULL, *py_import_blocker_cls = NULL, + *py_import_blocker = NULL; + + py_meta_path = PySys_GetObject("meta_path"); // note: borrowed reference + if (py_meta_path == NULL) { + PyErr_Format(sudo_exc_SudoException, "'sys.meta_path' is not available. " + "Unable to register import blocker hook which is meant to " + "verify that no such module get loaded by the sudo python plugins" + "which are writable by others than root."); + goto cleanup; + } + Py_INCREF(py_meta_path); + + py_import_blocker_cls = sudo_module_create_class("sudo.ImportBlocker", _sudo_ImportBlocker_class_methods, NULL); + if (py_import_blocker_cls == NULL) + goto cleanup; + + // call the constructor + py_import_blocker = PyObject_CallFunctionObjArgs(py_import_blocker_cls, py_meta_path, NULL); + if (py_import_blocker == NULL) + goto cleanup; + + Py_DECREF(py_meta_path); + py_meta_path = PyList_New(1); + if (py_meta_path == NULL) + goto cleanup; + + if (PyList_SetItem(py_meta_path, 0, py_import_blocker) != 0) + goto cleanup; + py_import_blocker = NULL; // list has stolen it + + if (PySys_SetObject("meta_path", py_meta_path) != 0) { + goto cleanup; + } + + rc = SUDO_RC_OK; + +cleanup: + Py_CLEAR(py_meta_path); + Py_CLEAR(py_import_blocker); + Py_CLEAR(py_import_blocker_cls); + + debug_return_int(rc); +} 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..31c479f --- /dev/null +++ b/plugins/python/python_plugin_approval.c @@ -0,0 +1,195 @@ +/* + * 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) + + +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); +} + +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 +}; + +sudo_dso_public 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..6ee16be --- /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 = { {}, &APPROVAL_SYMBOL_NAME(python_approval) }; + + +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); +} + +void +CALLBACK_CFUNC(close)(void) +{ + python_plugin_approval_close(&PLUGIN_CTX); +} + +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); +} + +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..ac40934 --- /dev/null +++ b/plugins/python/python_plugin_audit.c @@ -0,0 +1,282 @@ +/* + * 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) + + +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)); + } else { + rc = SUDO_RC_ERROR; + } + + 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; +} + +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); +} + +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); +} + +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); +} + +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 +}; + +sudo_dso_public 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..bb65f9e --- /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 = { {}, &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); +} + +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); +} + +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); +} + +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); +} + +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..b6e6c4e --- /dev/null +++ b/plugins/python/python_plugin_common.c @@ -0,0 +1,743 @@ +/* + * 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 + +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 +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) +{ + 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); + + 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); + + debug_return_ptr(PyImport_ImportModule(module_name)); +} + +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; +} + +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"); + rc = SUDO_RC_ERROR; + } 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); +} + +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_conf_developer_mode() && sudo_module_register_importblocker() < 0) { + goto cleanup; + } + + 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); + for (size_t i = 0; i < py_ctx.interpreter_count; ++i) { + PyThreadState *py_interpreter = py_ctx.py_subinterpreters[i]; + 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..d0ea3c1 --- /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) + + +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); +} + +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); +} + +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..b124808 --- /dev/null +++ b/plugins/python/python_plugin_io.c @@ -0,0 +1,277 @@ +/* + * 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) + + +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)); + } else { + rc = SUDO_RC_ERROR; + } + + if (rc != SUDO_RC_OK) + plugin_ctx->call_close = 0; + + Py_XDECREF(py_argv); + Py_XDECREF(py_command_info); + debug_return_int(rc); +} + +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); +} + +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; +} + +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")); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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 +}; + +sudo_dso_public 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..bf776f3 --- /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 = { {}, &IO_SYMBOL_NAME(python_io) }; + +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); +} + +void +CALLBACK_CFUNC(close)(int exit_status, int error) +{ + python_plugin_io_close(&PLUGIN_CTX, exit_status, error); +} + +int +CALLBACK_CFUNC(show_version)(int verbose) +{ + return python_plugin_io_show_version(&PLUGIN_CTX, verbose); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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..82b9a18 --- /dev/null +++ b/plugins/python/python_plugin_policy.c @@ -0,0 +1,290 @@ +/* + * 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)) + { + rc = SUDO_RC_ERROR; + 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")); +} + +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); +} + +void +python_plugin_policy_invalidate(int remove) +{ + 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)", remove)); + debug_return; +} + +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..ad35592 --- /dev/null +++ b/plugins/python/regress/check_python_examples.c @@ -0,0 +1,1599 @@ +/* + * 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 "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 int _init_symbols(void); +static int _unlink_symbols(void); + +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); +} + +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); +} + +void +create_debugging_plugin_options(void) +{ + create_plugin_options("example_debugging", "DebugDemoPlugin", NULL); +} + +void +create_audit_plugin_options(const char *extra_argument) +{ + create_plugin_options("example_audit_plugin", "SudoAuditPlugin", extra_argument); +} + +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); +} + +void +create_policy_plugin_options(void) +{ + create_plugin_options("example_policy_plugin", "SudoPolicyPlugin", NULL); +} + +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 = "pw_name"; + example_pwd.pw_passwd = "pw_passwd"; + example_pwd.pw_gecos = "pw_gecos"; + example_pwd.pw_shell ="pw_shell"; + example_pwd.pw_dir = "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)); + + // by default we test in developer mode, so the python plugin can be loaded + sudo_conf_clear_paths(); + VERIFY_INT(sudo_conf_read(sudo_conf_developer_mode, SUDO_CONF_ALL), true); + VERIFY_TRUE(sudo_conf_developer_mode()); + + // 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; +} + +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; +} + +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; +} + +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); + +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; +} + +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; +} + +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; +} + +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; +} + +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; +} + +const char * +create_debug_config(const char *debug_spec) +{ + char *result = NULL; + + static char config_path[PATH_MAX] = "/"; + snprintf(config_path, sizeof(config_path), "%s/sudo.conf", data.tmp_dir); + + char *content = NULL; + if (asprintf(&content, "Set developer_mode true\n" + "Debug %s %s/debug.log %s\n", + "python_plugin.so", data.tmp_dir, debug_spec) < 0) + { + printf("Failed to allocate string\n"); + goto cleanup; + } + + if (fwriteall(config_path, content) != true) { + printf("Failed to write '%s'\n", config_path); + goto cleanup; + } + + result = config_path; + +cleanup: + free(content); + + return result; +} + +int +check_example_group_plugin_is_able_to_debug(void) +{ + const char *config_path = create_debug_config("py_calls@diag"); + VERIFY_NOT_NULL(config_path); + VERIFY_INT(sudo_conf_read(config_path, SUDO_CONF_ALL), true); + + create_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; +} + +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; +} + +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; +} + +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; +} + +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"); +} + +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; +} + +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"); +} + +int +check_loading_fails_with_wrong_classname(void) +{ + create_plugin_options("example_debugging", "MispelledPluginName", NULL); + return check_loading_fails("wrong_classname"); +} + +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"); +} + +int +check_loading_fails_plugin_is_not_owned_by_root(void) +{ + sudo_conf_clear_paths(); + VERIFY_INT(sudo_conf_read(sudo_conf_normal_mode, SUDO_CONF_ALL), true); + + create_debugging_plugin_options(); + return check_loading_fails("not_owned_by_root"); +} + +int +check_example_conversation_plugin_reason_log(int simulate_suspend, const char *description) +{ + 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; +} + +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; +} + +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; +} + +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; +} + +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; +} + +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; +} + +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; +} + +int +check_example_policy_plugin_validate_invalidate(void) +{ + const char *errstr = NULL; + + // the plugin does not do any meaningful for these, so using log to validate instead + const char *config_path = create_debug_config("py_calls@diag"); + VERIFY_NOT_NULL(config_path); + VERIFY_INT(sudo_conf_read(config_path, SUDO_CONF_ALL), true); + + create_policy_plugin_options(); + + VERIFY_INT(python_policy->open(SUDO_API_VERSION, fake_conversation, fake_printf, data.settings, + data.user_info, data.user_env, data.plugin_options, &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 + + VERIFY_LOG_LINES(expected_path("check_example_policy_plugin_validate_invalidate.log")); + VERIFY_STR(data.stderr_str, ""); + VERIFY_STR(data.stdout_str, ""); + return true; +} + +int +check_policy_plugin_callbacks_are_optional(void) +{ + 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; +} + +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; +} + +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; +} + +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; +} + +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; +} + +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; +} + +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); + +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; +} + +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; +} + +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; +} + +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 errors = 0; + + if (argc != 2) { + printf("Please specify the python_plugin.so as argument!\n"); + return EXIT_FAILURE; + } + python_plugin_so_path = argv[1]; + + 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()); + RUN_TEST(check_example_group_plugin_is_able_to_debug()); + 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_loading_fails_plugin_is_not_owned_by_root()); + 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()); + + 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()); + + return errors; +} diff --git a/plugins/python/regress/iohelpers.c b/plugins/python/regress/iohelpers.c new file mode 100644 index 0000000..9c71dfc --- /dev/null +++ b/plugins/python/regress/iohelpers.c @@ -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. + */ + +/* + * 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 = calloc(1, dest_len); + char *dest = result; + char *pos = NULL; + size_t old_len = strlen(old); + + 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); + 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..b2383af --- /dev/null +++ b/plugins/python/regress/iohelpers.h @@ -0,0 +1,54 @@ +/* + * 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 "sudo_compat.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stdarg.h> +#include <signal.h> +#include <pwd.h> + +#include <stdbool.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..1d2918a --- /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: 2>, 'My demo purpose plugin shows this ERROR level debug message') +sudo.debug was called with arguments: (<DEBUG.INFO: 6>, '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..755b3f5 --- /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: 2>, '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: 6>, '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..bb2fc4f --- /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: RC.REJECT 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..0500bec --- /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.17" +} +(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.17" +} +(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/testdata/sudo.conf.developer_mode b/plugins/python/regress/testdata/sudo.conf.developer_mode new file mode 100644 index 0000000..4da2ad9 --- /dev/null +++ b/plugins/python/regress/testdata/sudo.conf.developer_mode @@ -0,0 +1 @@ +Set developer_mode true diff --git a/plugins/python/regress/testdata/sudo.conf.normal_mode b/plugins/python/regress/testdata/sudo.conf.normal_mode new file mode 100644 index 0000000..b972a6a --- /dev/null +++ b/plugins/python/regress/testdata/sudo.conf.normal_mode @@ -0,0 +1 @@ +Set developer_mode false diff --git a/plugins/python/regress/testhelpers.c b/plugins/python/regress/testhelpers.c new file mode 100644 index 0000000..1a861bd --- /dev/null +++ b/plugins/python/regress/testhelpers.c @@ -0,0 +1,295 @@ +/* + * 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" + +const char *sudo_conf_developer_mode = TESTDATA_DIR "sudo.conf.developer_mode"; +const char *sudo_conf_normal_mode = TESTDATA_DIR "sudo.conf.normal_mode"; + +struct TestData data; + +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"); +} + +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 *)); + for (size_t i = 0; i < count; ++i) { + const char *str = va_arg(args, char *); + result[i] = (str == NULL ? NULL : strdup(str)); + } + + 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]); + } + + 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'; + } + + 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..b5abb99 --- /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/" + +extern const char *sudo_conf_developer_mode; +extern const char *sudo_conf_normal_mode; + +#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; \ + 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: deitialization 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..18cb756 --- /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); + } + 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..bd189df --- /dev/null +++ b/plugins/python/sudo_python_module.c @@ -0,0 +1,611 @@ +/* + * 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 char *keywords[] = { "sep", "end", NULL }; + if (py_kwargs != NULL && !PyArg_ParseTupleAndKeywords(py_empty, py_kwargs, "|zz:sudo.log", 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); +} + +int +python_sudo_conversation_suspend_cb(int signo, struct py_conv_callback_closure *closure) +{ + return _call_conversation_callback(closure->py_on_suspend, signo); +} + +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 char *keywords[] = { "on_suspend", "on_resume", NULL }; + if (py_kwargs != NULL && !PyArg_ParseTupleAndKeywords(py_empty, py_kwargs, "|OO:sudo.conv", 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; + } + + if (sudo_module_ConvMessages_to_c(py_args, &num_msgs, &msgs) < 0) { + goto cleanup; + } + + replies = calloc(Py_SSIZE2SIZE(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) +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..a6c4f04 --- /dev/null +++ b/plugins/python/sudo_python_module.h @@ -0,0 +1,64 @@ +/* + * 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_importblocker(void); + +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 |