1
0
Fork 0

Adding upstream version 1.9.16p2.

Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
This commit is contained in:
Daniel Baumann 2025-06-22 09:52:37 +02:00
parent ebbaee52bc
commit 182f151a13
Signed by: daniel.baumann
GPG key ID: BCC918A2ABD66424
1342 changed files with 621215 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,182 @@
/*
* SPDX-License-Identifier: ISC
*
* Copyright (c) 2020 Robert Manner <robert.manner@oneidentity.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
* This is an open source non-commercial project. Dear PVS-Studio, please check it.
* PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
*/
#include "iohelpers.h"
#include <sudo_fatal.h>
int
rmdir_recursive(const char *path)
{
char *cmd = NULL;
int success = false;
if (asprintf(&cmd, "rm -rf \"%s\"", path) < 0)
return false;
if (system(cmd) == 0)
success = true;
free(cmd);
return success;
}
int
fwriteall(const char *file_path, const char *string)
{
int success = false;
FILE *file = fopen(file_path, "w+");
if (file == NULL)
goto cleanup;
size_t size = strlen(string);
if (fwrite(string, 1, size, file) < size) {
goto cleanup;
}
success = true;
cleanup:
if (file)
fclose(file);
return success;
}
int
freadall(const char *file_path, char *output, size_t max_len)
{
int rc = false;
FILE *file = fopen(file_path, "rb");
if (file == NULL) {
sudo_warn_nodebug("failed to open file '%s'", file_path);
goto cleanup;
}
size_t len = fread(output, 1, max_len - 1, file);
output[len] = '\0';
if (ferror(file) != 0) {
sudo_warn_nodebug("failed to read file '%s'", file_path);
goto cleanup;
}
if (!feof(file)) {
sudo_warn_nodebug("file '%s' was bigger than allocated buffer %zu",
file_path, max_len);
goto cleanup;
}
rc = true;
cleanup:
if (file)
fclose(file);
return rc;
}
int
vsnprintf_append(char * restrict output, size_t max_output_len, const char * restrict fmt, va_list args)
{
va_list args2;
va_copy(args2, args);
size_t output_len = strlen(output);
int rc = vsnprintf(output + output_len, max_output_len - output_len, fmt, args2);
va_end(args2);
return rc;
}
int
snprintf_append(char * restrict output, size_t max_output_len, const char * restrict fmt, ...)
{
va_list args;
va_start(args, fmt);
int rc = vsnprintf_append(output, max_output_len, fmt, args);
va_end(args);
return rc;
}
int
str_array_count(char **str_array)
{
int result = 0;
for (; str_array[result] != NULL; ++result) {}
return result;
}
void
str_array_snprint(char *out_str, size_t max_len, char **str_array, int array_len)
{
if (array_len < 0)
array_len = str_array_count(str_array);
for (int pos = 0; pos < array_len; ++pos) {
snprintf_append(out_str, max_len, "%s%s", pos > 0 ? ", " : "", str_array[pos]);
}
}
char *
str_replaced(const char *source, size_t dest_len, const char *old, const char *new)
{
char *result = malloc(dest_len);
char *dest = result;
char *pos = NULL;
size_t old_len = strlen(old);
if (result == NULL)
return NULL;
while ((pos = strstr(source, old)) != NULL) {
size_t len = (size_t)snprintf(dest, dest_len,
"%.*s%s", (int)(pos - source), source, new);
if (len >= dest_len)
goto fail;
dest_len -= len;
dest += len;
source = pos + old_len;
}
if (strlcpy(dest, source, dest_len) >= dest_len)
goto fail;
return result;
fail:
free(result);
return strdup("str_replace_all failed, string too long");
}
void
str_replace_in_place(char *string, size_t max_length, const char *old, const char *new)
{
char *replaced = str_replaced(string, max_length, old, new);
if (replaced != NULL) {
strlcpy(string, replaced, max_length);
free(replaced);
}
}

View file

@ -0,0 +1,58 @@
/*
* SPDX-License-Identifier: ISC
*
* Copyright (c) 2020 Robert Manner <robert.manner@oneidentity.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#ifndef PYTHON_IO_HELPERS
#define PYTHON_IO_HELPERS
#include <config.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef HAVE_STDBOOL_H
# include <stdbool.h>
#else
# include <compat/stdbool.h>
#endif /* HAVE_STDBOOL_H */
#include <string.h>
#include <stdarg.h>
#include <signal.h>
#include <pwd.h>
#include <sudo_compat.h>
#define MAX_OUTPUT (2 << 16)
int rmdir_recursive(const char *path);
int fwriteall(const char *file_path, const char *string);
int freadall(const char *file_path, char *output, size_t max_len);
// allocates new string with the content of 'string' but 'old' replaced to 'new'
// The allocated array will be dest_length size and null terminated correctly.
char *str_replaced(const char *string, size_t dest_length, const char *old, const char *new);
// same, but "string" must be able to store 'max_length' number of characters including the null terminator
void str_replace_in_place(char *string, size_t max_length, const char *old, const char *new);
int vsnprintf_append(char * restrict output, size_t max_output_len, const char * restrict fmt, va_list args);
int snprintf_append(char * restrict output, size_t max_output_len, const char * restrict fmt, ...);
int str_array_count(char **str_array);
void str_array_snprint(char *out_str, size_t max_len, char **str_array, int array_len);
#endif

View file

@ -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))

View file

@ -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))

View file

@ -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")

View file

@ -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 --

View file

@ -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 --

View file

@ -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 --

View file

@ -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 --

View file

@ -0,0 +1 @@
sudo: loading more than 8 sudo python audit plugins is not supported

View file

@ -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 --

View file

@ -0,0 +1,3 @@
Question count: 2
Question 0: <<Reason: >> (timeout: 120, msg_type=2)
Question 1: <<Secret reason: >> (timeout: 120, msg_type=5)

View file

@ -0,0 +1,3 @@
Please provide your reason for executing ('/bin/whoami',)
conversation suspend: signal SIGTSTP
conversation resume: signal was SIGCONT

View file

@ -0,0 +1,3 @@
Executed /bin/whoami
Reason: my fake reason
Hidden reason: my real secret reason

View file

@ -0,0 +1,3 @@
Question count: 2
Question 0: <<Reason: >> (timeout: 120, msg_type=2)
Question 1: <<Secret reason: >> (timeout: 120, msg_type=5)

View file

@ -0,0 +1 @@
Please provide your reason for executing ('/bin/whoami',)

View file

@ -0,0 +1,3 @@
Executed /bin/whoami
Reason: my fake reason
Hidden reason: my real secret reason

View file

@ -0,0 +1,2 @@
Question count: 2
Question 0: <<Reason: >> (timeout: 120, msg_type=2)

View file

@ -0,0 +1,2 @@
Question count: 2
Question 0: <<Reason: >> (timeout: 120, msg_type=2)

View file

@ -0,0 +1 @@
You did not answer in time

View file

@ -0,0 +1 @@
Please provide your reason for executing ('/bin/whoami',)

View file

@ -0,0 +1,6 @@
sudo.debug was called with arguments: (DEBUG.ERROR, 'My demo purpose plugin shows this ERROR level debug message')
sudo.debug was called with arguments: (DEBUG.INFO, 'My demo purpose plugin shows this INFO level debug message')
LogHandler.emit was called
LogHandler.emit was called
sudo.options_as_dict was called with arguments: (('ModulePath=SRC_DIR/example_debugging.py', 'ClassName=DebugDemoPlugin'),)
sudo.options_as_dict returned result: [('ClassName', 'DebugDemoPlugin'), ('ModulePath', 'SRC_DIR/example_debugging.py')]

View file

@ -0,0 +1,11 @@
__init__ @ SRC_DIR/example_debugging.py:58 calls C function:
sudo.debug was called with arguments: (DEBUG.ERROR, 'My demo purpose plugin shows this ERROR level debug message')
__init__ @ SRC_DIR/example_debugging.py:63 calls C function:
sudo.debug was called with arguments: (DEBUG.INFO, 'My demo purpose plugin shows this INFO level debug message')
handle @ logging/__init__.py calls C function:
LogHandler.emit was called
handle @ logging/__init__.py calls C function:
LogHandler.emit was called
__init__ @ SRC_DIR/example_debugging.py:85 calls C function:
sudo.options_as_dict was called with arguments: (('ModulePath=SRC_DIR/example_debugging.py', 'ClassName=DebugDemoPlugin'),)
sudo.options_as_dict returned result: [('ClassName', 'DebugDemoPlugin'), ('ModulePath', 'SRC_DIR/example_debugging.py')]

View file

@ -0,0 +1,3 @@
importing module: SRC_DIR/example_debugging.py
Extending python 'path' with 'SRC_DIR'
Deinit was called for a python plugin

View file

@ -0,0 +1,2 @@
My demo purpose plugin shows this ERROR level debug message
Python log system shows this ERROR level debug message

View file

@ -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

View file

@ -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>

View file

@ -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

View file

@ -0,0 +1 @@
Skipping close call, because there was no command run

View file

@ -0,0 +1,4 @@
SudoGroupPlugin.__init__ was called with arguments: () [('args', ('ModulePath=SRC_DIR/example_group_plugin.py', 'ClassName=SudoGroupPlugin')), ('version', '1.0')]
SudoGroupPlugin.__init__ returned result: <example_group_plugin.SudoGroupPlugin object>
SudoGroupPlugin.query was called with arguments: ('user', 'group', ('pw_name', 'pw_passwd', 1001, 101, 'pw_gecos', 'pw_dir', 'pw_shell'))
SudoGroupPlugin.query returned result: 0

View file

@ -0,0 +1 @@
Example sudo python plugin will log to /tmp/sudo_check_python_exampleXXXXXX/sudo.log

View file

@ -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 --

View file

@ -0,0 +1 @@
sudo: loading more than 8 sudo python IO plugins is not supported

View file

@ -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

View file

@ -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 --

View file

@ -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 --

View file

@ -0,0 +1 @@
Example sudo python plugin will log to /tmp/sudo_check_python_exampleXXXXXX/sudo.log

View file

@ -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 --

View file

@ -0,0 +1 @@
Failed to construct plugin instance: [Errno 2] No such file or directory: '/some/not/writable/directory/sudo.log'

View file

@ -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")

View file

@ -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

View file

@ -0,0 +1,2 @@
-- Plugin STARTED --
-- Plugin DESTROYED --

View file

@ -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

View file

@ -0,0 +1 @@
The command returned with exit_status 3

View file

@ -0,0 +1 @@
You are not allowed to run this command!

View file

@ -0,0 +1 @@
Failed to execute command, execve syscall returned 2 (ENOENT)

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
Python Example Policy Plugin version: 1.0

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,2 @@
No python module path is specified. Use 'ModulePath' plugin config option in 'sudo.conf'
Failed during loading plugin class

View file

@ -0,0 +1 @@
Failed during loading plugin class: File 'SRC_DIR/example_debugging.py' must be owned by uid 0

View file

@ -0,0 +1,2 @@
Failed to find plugin class 'MispelledPluginName'
Failed during loading plugin class

View file

@ -0,0 +1 @@
Failed during loading plugin class: No module named 'wrong_path'

View file

@ -0,0 +1 @@
Python io plugin (API 1.0): DebugDemoPlugin (loaded from 'SRC_DIR/example_debugging.py')

View file

@ -0,0 +1 @@
sudo: loading more than 8 sudo python approval plugins is not supported

View file

@ -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.22"
}
(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.22"
}
(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

View file

@ -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']

View file

@ -0,0 +1,356 @@
/*
* SPDX-License-Identifier: ISC
*
* Copyright (c) 2020 Robert Manner <robert.manner@oneidentity.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
* This is an open source non-commercial project. Dear PVS-Studio, please check it.
* PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
*/
#include "testhelpers.h"
struct TestData data;
/*
* Starting with Python 3.11, backtraces may contain a line with
* '~' and '^' characters to bring attention to the important part
* of the line.
*/
static void
remove_underline(char *output)
{
char *cp, *ep;
// Remove lines that only consist of '~', '^' and white space.
cp = output;
ep = output + strlen(output);
for (;;) {
size_t len = strspn(cp, "~^ \t");
if (len > 0 && cp[len] == '\n') {
/* Prune out lines that are "underlining". */
memmove(cp, cp + len + 1, (size_t)(ep - cp));
if (*cp == '\0')
break;
} else {
/* No match, move to the next line. */
cp = strchr(cp, '\n');
if (cp == NULL)
break;
cp++;
}
}
}
static void
clean_output(char *output)
{
// we replace some output which otherwise would be test run dependent
str_replace_in_place(output, MAX_OUTPUT, data.tmp_dir, TEMP_PATH_TEMPLATE);
if (data.tmp_dir2)
str_replace_in_place(output, MAX_OUTPUT, data.tmp_dir2, TEMP_PATH_TEMPLATE "2");
str_replace_in_place(output, MAX_OUTPUT, SRC_DIR, "SRC_DIR");
remove_underline(output);
}
const char *
expected_path(const char *format, ...)
{
static char expected_output_file[PATH_MAX];
size_t dirlen = strlcpy(expected_output_file, TESTDATA_DIR, sizeof(expected_output_file));
va_list args;
va_start(args, format);
vsnprintf(expected_output_file + dirlen, PATH_MAX - dirlen, format, args);
va_end(args);
return expected_output_file;
}
char **
create_str_array(size_t count, ...)
{
va_list args;
va_start(args, count);
char **result = calloc(count, sizeof(char *));
if (result != NULL) {
for (size_t i = 0; i < count; ++i) {
const char *str = va_arg(args, char *);
if (str != NULL) {
result[i] = strdup(str);
if (result[i] == NULL) {
while (i > 0) {
free(result[--i]);
}
free(result);
result = NULL;
break;
}
}
}
}
va_end(args);
return result;
}
int
is_update(void)
{
static int result = -1;
if (result < 0) {
const char *update = getenv("UPDATE_TESTDATA");
result = (update && strcmp(update, "1") == 0) ? 1 : 0;
}
return result;
}
int
verify_content(char *actual_content, const char *reference_path)
{
clean_output(actual_content);
if (is_update()) {
VERIFY_TRUE(fwriteall(reference_path, actual_content));
} else {
char expected_output[MAX_OUTPUT] = "";
if (!freadall(reference_path, expected_output, sizeof(expected_output))) {
printf("Error: Missing test data at '%s'\n", reference_path);
return false;
}
VERIFY_STR(actual_content, expected_output);
}
return true;
}
int
verify_file(const char *actual_dir, const char *actual_file_name, const char *reference_path)
{
char actual_path[PATH_MAX];
snprintf(actual_path, sizeof(actual_path), "%s/%s", actual_dir, actual_file_name);
char actual_str[MAX_OUTPUT];
if (!freadall(actual_path, actual_str, sizeof(actual_str))) {
printf("Expected that file '%s' gets created, but it was not\n", actual_path);
return false;
}
int rc = verify_content(actual_str, reference_path);
return rc;
}
int
fake_conversation(int num_msgs, const struct sudo_conv_message msgs[],
struct sudo_conv_reply replies[], struct sudo_conv_callback *callback)
{
(void) callback;
snprintf_append(data.conv_str, MAX_OUTPUT, "Question count: %d\n", num_msgs);
for (int i = 0; i < num_msgs; ++i) {
const struct sudo_conv_message *msg = &msgs[i];
snprintf_append(data.conv_str, MAX_OUTPUT, "Question %d: <<%s>> (timeout: %d, msg_type=%d)\n",
i, msg->msg, msg->timeout, msg->msg_type);
if (data.conv_replies[i] == NULL)
return 1; // simulates user interruption (conversation error)
replies[i].reply = strdup(data.conv_replies[i]);
if (replies[i].reply == NULL)
return 1; // memory allocation error
}
return 0; // simulate user answered just fine
}
int
fake_conversation_with_suspend(int num_msgs, const struct sudo_conv_message msgs[],
struct sudo_conv_reply replies[], struct sudo_conv_callback *callback)
{
if (callback != NULL) {
callback->on_suspend(SIGTSTP, callback->closure);
callback->on_resume(SIGCONT, callback->closure);
}
return fake_conversation(num_msgs, msgs, replies, callback);
}
int
fake_printf(int msg_type, const char * restrict fmt, ...)
{
int rc = -1;
va_list args;
va_start(args, fmt);
char *output = NULL;
switch(msg_type) {
case SUDO_CONV_INFO_MSG:
output = data.stdout_str;
break;
case SUDO_CONV_ERROR_MSG:
output = data.stderr_str;
break;
default:
break;
}
if (output)
rc = vsnprintf_append(output, MAX_OUTPUT, fmt, args);
va_end(args);
return rc;
}
int
verify_log_lines(const char *reference_path)
{
char stored_path[PATH_MAX];
snprintf(stored_path, sizeof(stored_path), "%s/%s", data.tmp_dir, "debug.log");
FILE *file = fopen(stored_path, "rb");
if (file == NULL) {
printf("Failed to open file '%s'\n", stored_path);
return false;
}
char line[1024] = "";
char stored_str[MAX_OUTPUT] = "";
while (fgets(line, sizeof(line), file) != NULL) {
char *line_data = strstr(line, "] "); // this skips the timestamp and pid at the beginning
VERIFY_NOT_NULL(line_data); // malformed log line
line_data += 2;
char *line_end = strstr(line_data, " object at "); // this skips checking the pointer hex
if (line_end) {
snprintf(line_end, sizeof(line) - (size_t)(line_end - line),
" object>\n");
}
if (strncmp(line_data, "handle @ /", sizeof("handle @ /") - 1) == 0) {
char *start = line_data + sizeof("handle @ ") - 1;
// normalize path to logging/__init__.py
char *logging = strstr(start, "logging/");
if (logging != NULL) {
memmove(start, logging, strlen(logging) + 1);
}
// remove line number
char *colon = strchr(start, ':');
if (colon != NULL) {
size_t len = strspn(colon + 1, "0123456789");
if (len != 0)
memmove(colon, colon + len + 1, strlen(colon + len + 1) + 1);
}
} else if (strncmp(line_data, "LogHandler.emit was called ", 27) == 0) {
// LogHandler.emit argument details vary based on python version
line_data[26] = '\n';
line_data[27] = '\0';
} else {
// Python 3.11 uses 0 instead of the symbolic REJECT in backtraces
char *cp = strstr(line_data, ": REJECT");
if (cp != NULL) {
// Convert ": REJECT" to ": 0" + rest of line
memcpy(cp, ": 0", 3);
memmove(cp + 3, cp + 8, strlen(cp + 8) + 1);
} else {
// Python 3.12 may use <RC.REJECT: 0> instead of 0
cp = strstr(line_data, "<RC.REJECT: 0>");
if (cp != NULL) {
*cp = '0';
memmove(cp + 1, cp + 14, strlen(cp + 14) + 1);
}
}
}
VERIFY_TRUE(strlcat(stored_str, line_data, sizeof(stored_str)) < sizeof(stored_str)); // we have enough space in buffer
}
clean_output(stored_str);
VERIFY_TRUE(verify_content(stored_str, reference_path));
return true;
}
int
verify_str_set(char **actual_set, char **expected_set, const char *actual_variable_name)
{
VERIFY_NOT_NULL(actual_set);
VERIFY_NOT_NULL(expected_set);
int actual_len = str_array_count(actual_set);
int expected_len = str_array_count(expected_set);
int matches = false;
if (actual_len == expected_len) {
int actual_pos = 0;
for (; actual_pos < actual_len; ++actual_pos) {
char *actual_item = actual_set[actual_pos];
int expected_pos = 0;
for (; expected_pos < expected_len; ++expected_pos) {
if (strcmp(actual_item, expected_set[expected_pos]) == 0)
break;
}
if (expected_pos == expected_len) {
// matching item was not found
break;
}
}
matches = (actual_pos == actual_len);
}
if (!matches) {
char actual_set_str[MAX_OUTPUT] = "";
char expected_set_str[MAX_OUTPUT] = "";
str_array_snprint(actual_set_str, MAX_OUTPUT, actual_set, actual_len);
str_array_snprint(expected_set_str, MAX_OUTPUT, expected_set, expected_len);
VERIFY_PRINT_MSG("%s", actual_variable_name, actual_set_str, "expected",
expected_set_str, "expected to contain the same elements as");
return false;
}
return true;
}
int
mock_python_datetime_now(const char *plugin_name, const char *date_str)
{
char *cmd = NULL;
int len;
len = asprintf(&cmd,
"import %s\n" // the plugin has its own submodule
"from datetime import datetime\n" // store the real datetime
"import time\n"
"from unittest.mock import Mock\n"
"%s.datetime = Mock()\n" // replace plugin's datetime
"%s.datetime.now = lambda: datetime.strptime('%s', '%%Y-%%m-%%dT%%H:%%M:%%S')\n",
plugin_name, plugin_name, plugin_name, date_str);
if (len == -1)
return false;
VERIFY_PTR_NE(cmd, NULL);
VERIFY_INT(PyRun_SimpleString(cmd), 0);
free(cmd);
return true;
}

View file

@ -0,0 +1,175 @@
/*
* SPDX-License-Identifier: ISC
*
* Copyright (c) 2020 Robert Manner <robert.manner@oneidentity.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#ifndef PYTHON_TESTHELPERS
#define PYTHON_TESTHELPERS
#include "iohelpers.h"
#include "../pyhelpers.h"
#include <sudo_conf.h>
// just for the IDE
#ifndef SRC_DIR
#define SRC_DIR ""
#endif
#define TESTDATA_DIR SRC_DIR "/regress/testdata/"
#define TEMP_PATH_TEMPLATE "/tmp/sudo_check_python_exampleXXXXXX"
extern struct TestData {
char *tmp_dir;
char *tmp_dir2;
char stdout_str[MAX_OUTPUT];
char stderr_str[MAX_OUTPUT];
char conv_str[MAX_OUTPUT];
const char *conv_replies[8];
// some example test data used by multiple test cases:
char ** settings;
char ** user_info;
char ** command_info;
char ** plugin_argv;
int plugin_argc;
char ** user_env;
char ** plugin_options;
} data;
const char * expected_path(const char *format, ...);
char ** create_str_array(size_t count, ...);
#define RUN_TEST(testcase) \
do { \
int success = 1; \
ntests++; \
if (verbose) { \
printf("Running test " #testcase " ... \n"); \
} \
if (!init()) { \
printf("FAILED: initialization of testcase %s at %s:%d\n", #testcase, __FILE__, __LINE__); \
success = 0; \
} else \
if (!testcase) { \
printf("FAILED: testcase %s at %s:%d\n", #testcase, __FILE__, __LINE__); \
success = 0; \
} \
if (!cleanup(success)) { \
printf("FAILED: deinitialization of testcase %s at %s:%d\n", #testcase, __FILE__, __LINE__); \
success = 0; \
} \
if (!success) { \
errors++; \
} \
} while(false)
#define VERIFY_PRINT_MSG(fmt, actual_str, actual, expected_str, expected, expected_to_be_message) \
printf("Expectation failed at %s:%d:\n actual is <<" fmt ">>: %s\n %s <<" fmt ">>: %s\n", \
__FILE__, __LINE__, actual, actual_str, expected_to_be_message, expected, expected_str)
#define VERIFY_CUSTOM(fmt, type, actual, expected, invert) \
do { \
type actual_value = (type)(actual); \
int failed = (actual_value != expected); \
if (invert) \
failed = !failed; \
if (failed) { \
VERIFY_PRINT_MSG(fmt, #actual, actual_value, #expected, expected, invert ? "not expected to be" : "expected to be"); \
return false; \
} \
} while(false)
#define VERIFY_EQ(fmt, type, actual, expected) VERIFY_CUSTOM(fmt, type, actual, expected, false)
#define VERIFY_NE(fmt, type, actual, not_expected) VERIFY_CUSTOM(fmt, type, actual, not_expected, true)
#define VERIFY_INT(actual, expected) VERIFY_EQ("%d", int, actual, expected)
#define VERIFY_PTR(actual, expected) VERIFY_EQ("%p", const void *, (const void *)actual, (const void *)expected)
#define VERIFY_PTR_NE(actual, not_expected) VERIFY_NE("%p", const void *, (const void *)actual, (const void *)not_expected)
#define VERIFY_TRUE(actual) VERIFY_NE("%d", int, actual, 0)
#define VERIFY_FALSE(actual) VERIFY_INT(actual, false)
#define VERIFY_NOT_NULL(actual) VERIFY_NE("%p", const void *, actual, NULL)
#define VERIFY_STR(actual, expected) \
do { \
const char *actual_str = actual; \
if (!actual_str || strcmp(actual_str, expected) != 0) { \
VERIFY_PRINT_MSG("%s", #actual, actual_str ? actual_str : "(null)", #expected, expected, "expected to be"); \
return false; \
} \
} while(false)
#define VERIFY_STR_CONTAINS(actual, expected) \
do { \
const char *actual_str = actual; \
if (!actual_str || strstr(actual_str, expected) == NULL) { \
VERIFY_PRINT_MSG("%s", #actual, actual_str ? actual_str : "(null)", #expected, expected, "expected to contain the string"); \
return false; \
} \
} while(false)
int is_update(void);
int verify_content(char *actual_content, const char *reference_path);
#define VERIFY_CONTENT(actual_output, reference_path) \
VERIFY_TRUE(verify_content(actual_output, reference_path))
#define VERIFY_STDOUT(reference_path) \
VERIFY_CONTENT(data.stdout_str, reference_path)
#define VERIFY_STDERR(reference_path) \
VERIFY_CONTENT(data.stderr_str, reference_path)
#define VERIFY_CONV(reference_name) \
VERIFY_CONTENT(data.conv_str, reference_name)
int verify_file(const char *actual_dir, const char *actual_file_name, const char *reference_path);
#define VERIFY_FILE(actual_file_name, reference_path) \
VERIFY_TRUE(verify_file(data.tmp_dir, actual_file_name, reference_path))
int fake_conversation(int num_msgs, const struct sudo_conv_message msgs[],
struct sudo_conv_reply replies[], struct sudo_conv_callback *callback);
int fake_conversation_with_suspend(int num_msgs, const struct sudo_conv_message msgs[],
struct sudo_conv_reply replies[], struct sudo_conv_callback *callback);
int fake_printf(int msg_type, const char * restrict fmt, ...);
int verify_log_lines(const char *reference_path);
int mock_python_datetime_now(const char *plugin_name, const char *date_str);
#define VERIFY_LOG_LINES(reference_path) \
VERIFY_TRUE(verify_log_lines(reference_path))
int verify_str_set(char **actual_set, char **expected_set, const char *actual_variable_name);
#define VERIFY_STR_SET(actual_set, ...) \
do { \
char **expected_set = create_str_array(__VA_ARGS__); \
VERIFY_TRUE(verify_str_set(actual_set, expected_set, #actual_set)); \
str_array_free(&expected_set); \
} while(false)
#endif // PYTHON_TESTHELPERS