plugins/python/sudo_python_module.c: use IntEnums instead of constants

It is a bit more code, but it is more "pythonic" and easier to debug
as the enum values also know their names.

It is also an API break, eg. sudo.RC_OK becomes sudo.RC.OK as sudo.RC will
be the "type" of the enum, but I guess that is acceptable before the
initial release.
This commit is contained in:
Robert Manner
2020-02-04 13:50:26 +01:00
committed by Todd C. Miller
parent 67ab6fd5d6
commit 21c02e1732
12 changed files with 170 additions and 79 deletions

View File

@@ -12,20 +12,20 @@ class ReasonLoggerIOPlugin(sudo.Plugin):
sudo.ConvMessage has the following fields (see help(sudo.ConvMessage)): sudo.ConvMessage has the following fields (see help(sudo.ConvMessage)):
msg_type: int Specifies the type of the conversation. msg_type: int Specifies the type of the conversation.
See sudo.CONV_* constants below. See sudo.CONV.* constants below.
timeout: int The maximum amount of time for the conversation in seconds. timeout: int The maximum amount of time for the conversation in seconds.
After the timeout exceeds, the "sudo.conv" function will After the timeout exceeds, the "sudo.conv" function will
raise sudo.ConversationInterrupted exception. raise sudo.ConversationInterrupted exception.
msg: str The message to display for the user. msg: str The message to display for the user.
To specify the conversion type you can use the following constants: To specify the conversion type you can use the following constants:
sudo.CONV_PROMPT_ECHO_OFF sudo.CONV.PROMPT_ECHO_OFF
sudo.CONV_PROMPT_ECHO_ON sudo.CONV.PROMPT_ECHO_ON
sudo.CONV_ERROR_MSG sudo.CONV.ERROR_MSG
sudo.CONV_INFO_MSG sudo.CONV.INFO_MSG
sudo.CONV_PROMPT_MASK sudo.CONV.PROMPT_MASK
sudo.CONV_PROMPT_ECHO_OK sudo.CONV.PROMPT_ECHO_OK
sudo.CONV_PREFER_TTY sudo.CONV.PREFER_TTY
""" """
def open(self, argv, command_info): def open(self, argv, command_info):
try: try:
@@ -36,8 +36,8 @@ class ReasonLoggerIOPlugin(sudo.Plugin):
# can hide a hidden message in case of criminals are forcing him for # can hide a hidden message in case of criminals are forcing him for
# running the command. # running the command.
# You can either specify the arguments in strict order (timeout being optional), or use named arguments. # 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) 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) message2 = sudo.ConvMessage(msg="Secret reason: ", timeout=conv_timeout, msg_type=sudo.CONV.PROMPT_MASK)
reply1, reply2 = sudo.conv(message1, message2, reply1, reply2 = sudo.conv(message1, message2,
on_suspend=self.on_conversation_suspend, on_suspend=self.on_conversation_suspend,
on_resume=self.on_conversation_resume) on_resume=self.on_conversation_resume)
@@ -49,7 +49,7 @@ class ReasonLoggerIOPlugin(sudo.Plugin):
except sudo.ConversationInterrupted: except sudo.ConversationInterrupted:
sudo.log_error("You did not answer in time") sudo.log_error("You did not answer in time")
return sudo.RC_REJECT return sudo.RC.REJECT
def on_conversation_suspend(self, signum): def on_conversation_suspend(self, signum):
# This is just an example of how to do something on conversation suspend. # This is just an example of how to do something on conversation suspend.

View File

@@ -26,14 +26,14 @@ class DebugDemoPlugin(sudo.Plugin):
load logs python plugin loading / unloading load logs python plugin loading / unloading
Log levels Log levels
crit sudo.DEBUG_CRIT --> only cricital messages crit sudo.DEBUG.CRIT --> only cricital messages
err sudo.DEBUG_ERROR err sudo.DEBUG.ERROR
warn sudo.DEBUG_WARN warn sudo.DEBUG.WARN
notice sudo.DEBUG_NOTICE notice sudo.DEBUG.NOTICE
diag sudo.DEBUG_DIAG diag sudo.DEBUG.DIAG
info sudo.DEBUG_INFO info sudo.DEBUG.INFO
trace sudo.DEBUG_TRACE trace sudo.DEBUG.TRACE
debug sudo.DEBUG_DEBUG --> very extreme verbose debugging debug sudo.DEBUG.DEBUG --> very extreme verbose debugging
See the sudo.conf manual for more details ("man sudo.conf"). See the sudo.conf manual for more details ("man sudo.conf").
@@ -42,10 +42,10 @@ class DebugDemoPlugin(sudo.Plugin):
# Specify: "py_calls@info" debug option to show the call to this constructor and the arguments passed in # 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) # 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") 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) # 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") sudo.debug(sudo.DEBUG.INFO, "My demo purpose plugin shows this INFO level debug message")
# If you raise the level to info or below, the call of the debug will also be logged. # 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: # An example output you will see in the debug log file:

View File

@@ -15,14 +15,14 @@ class SudoGroupPlugin(sudo.Plugin):
Most functions can express error or reject through their "int" return value 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: as documented in the manual. The sudo module also has constants for these:
sudo.RC_ACCEPT / sudo.RC_OK 1 sudo.RC.ACCEPT / sudo.RC.OK 1
sudo.RC_REJECT 0 sudo.RC.REJECT 0
sudo.RC_ERROR -1 sudo.RC.ERROR -1
sudo.RC_USAGE_ERROR -2 sudo.RC.USAGE_ERROR -2
If the function returns "None" (for example does not call return), it will If the function returns "None" (for example does not call return), it will
be considered sudo.RC_OK. If an exception is raised, its backtrace will be be considered sudo.RC.OK. If an exception is raised, its backtrace will be
shown to the user and the plugin function returns sudo.RC_ERROR. If that is shown to the user and the plugin function returns sudo.RC.ERROR. If that is
not acceptable, catch it. not acceptable, catch it.
""" """
@@ -39,4 +39,4 @@ class SudoGroupPlugin(sudo.Plugin):
} }
group_has_user = user in hardcoded_user_groups.get(group, []) group_has_user = user in hardcoded_user_groups.get(group, [])
return sudo.RC_ACCEPT if group_has_user else sudo.RC_REJECT return sudo.RC.ACCEPT if group_has_user else sudo.RC.REJECT

View File

@@ -24,14 +24,14 @@ class SudoIOPlugin(sudo.Plugin):
Most functions can express error or reject through their "int" return value 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: as documented in the manual. The sudo module also has constants for these:
sudo.RC_ACCEPT / sudo.RC_OK 1 sudo.RC.ACCEPT / sudo.RC.OK 1
sudo.RC_REJECT 0 sudo.RC.REJECT 0
sudo.RC_ERROR -1 sudo.RC.ERROR -1
sudo.RC_USAGE_ERROR -2 sudo.RC.USAGE_ERROR -2
If the function returns "None" (for example does not call return), it will If the function returns "None" (for example does not call return), it will
be considered sudo.RC_OK. If an exception is raised, its backtrace will be be considered sudo.RC.OK. If an exception is raised, its backtrace will be
shown to the user and the plugin function returns sudo.RC_ERROR. If that is shown to the user and the plugin function returns sudo.RC.ERROR. If that is
not acceptable, catch it. not acceptable, catch it.
""" """
@@ -78,7 +78,7 @@ class SudoIOPlugin(sudo.Plugin):
self._log("EXEC", " ".join(argv)) self._log("EXEC", " ".join(argv))
self._log("EXEC info", json.dumps(command_info, indent=4)) self._log("EXEC info", json.dumps(command_info, indent=4))
return sudo.RC_ACCEPT return sudo.RC.ACCEPT
def log_ttyout(self, buf: str) -> int: def log_ttyout(self, buf: str) -> int:
return self._log("TTY OUT", buf.strip()) return self._log("TTY OUT", buf.strip())
@@ -116,7 +116,7 @@ class SudoIOPlugin(sudo.Plugin):
Works the same as close() from C API (see sudo_plugin manual), except 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() that it only gets called if there was a command execution trial (open()
returned with sudo.RC_ACCEPT). returned with sudo.RC.ACCEPT).
""" """
if error == 0: if error == 0:
self._log("CLOSE", "Command returned {}".format(exit_status)) self._log("CLOSE", "Command returned {}".format(exit_status))
@@ -133,4 +133,4 @@ class SudoIOPlugin(sudo.Plugin):
def _log(self, type, message): def _log(self, type, message):
print(type, message, file=self._log_file) print(type, message, file=self._log_file)
return sudo.RC_ACCEPT return sudo.RC.ACCEPT

View File

@@ -30,14 +30,14 @@ class SudoPolicyPlugin(sudo.Plugin):
Most functions can express error or reject through their "int" return value 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: as documented in the manual. The sudo module also has constants for these:
sudo.RC_ACCEPT / sudo.RC_OK 1 sudo.RC.ACCEPT / sudo.RC.OK 1
sudo.RC_REJECT 0 sudo.RC.REJECT 0
sudo.RC_ERROR -1 sudo.RC.ERROR -1
sudo.RC_USAGE_ERROR -2 sudo.RC.USAGE_ERROR -2
If the function returns "None" (for example does not call return), it will If the function returns "None" (for example does not call return), it will
be considered sudo.RC_OK. If an exception is raised, its backtrace will be be considered sudo.RC.OK. If an exception is raised, its backtrace will be
shown to the user and the plugin function returns sudo.RC_ERROR. If that is shown to the user and the plugin function returns sudo.RC.ERROR. If that is
not acceptable, catch it. not acceptable, catch it.
""" """
@@ -70,7 +70,7 @@ class SudoPolicyPlugin(sudo.Plugin):
# Example for a simple reject: # Example for a simple reject:
if not self._is_command_allowed(cmd): if not self._is_command_allowed(cmd):
sudo.log_error("You are not allowed to run this command!") sudo.log_error("You are not allowed to run this command!")
return sudo.RC_REJECT return sudo.RC.REJECT
# The environment the command will be executed with (we allow any here) # The environment the command will be executed with (we allow any here)
user_env_out = sudo.options_from_dict(self.user_env) + env_add user_env_out = sudo.options_from_dict(self.user_env) + env_add
@@ -83,9 +83,9 @@ class SudoPolicyPlugin(sudo.Plugin):
}) })
except SudoPluginError as error: except SudoPluginError as error:
sudo.log_error(str(error)) sudo.log_error(str(error))
return sudo.RC_ERROR return sudo.RC.ERROR
return (sudo.RC_ACCEPT, command_info_out, argv, user_env_out) return (sudo.RC.ACCEPT, command_info_out, argv, user_env_out)
def init_session(self, user_pwd: Tuple, user_env: Tuple[str, ...]): def init_session(self, user_pwd: Tuple, user_env: Tuple[str, ...]):
"""Perform session setup """Perform session setup
@@ -97,10 +97,10 @@ class SudoPolicyPlugin(sudo.Plugin):
user_pwd = pwd.struct_passwd(user_pwd) if user_pwd else None user_pwd = pwd.struct_passwd(user_pwd) if user_pwd else None
# This is how you change the user_env: # This is how you change the user_env:
return (sudo.RC_OK, user_env + ("PLUGIN_EXAMPLE_ENV=1",)) return (sudo.RC.OK, user_env + ("PLUGIN_EXAMPLE_ENV=1",))
# If you do not want to change user_env, you can also just return (or None): # If you do not want to change user_env, you can also just return (or None):
# return sudo.RC_OK # return sudo.RC.OK
def list(self, argv: Tuple[str, ...], is_verbose: int, user: str): def list(self, argv: Tuple[str, ...], is_verbose: int, user: str):
cmd = argv[0] if argv else None cmd = argv[0] if argv else None

View File

@@ -518,3 +518,33 @@ py_object_set_attr_string(PyObject *py_object, const char *attr_name, const char
PyObject_SetAttrString(py_object, attr_name, py_value); PyObject_SetAttrString(py_object, attr_name, py_value);
Py_CLEAR(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);
}

View File

@@ -62,6 +62,14 @@ char *py_create_string_rep(PyObject *py_object);
char *py_join_str_list(PyObject *py_str_list, const char *separator); 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_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_with_count(Py_ssize_t count, char * const strings[]);

View File

@@ -243,6 +243,15 @@ _python_plugin_register_plugin_in_py_ctx(void)
PyImport_AppendInittab("sudo", sudo_module_init); PyImport_AppendInittab("sudo", sudo_module_init);
Py_InitializeEx(0); Py_InitializeEx(0);
py_ctx.py_main_interpreter = PyThreadState_Get(); 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 { } else {
PyThreadState_Swap(py_ctx.py_main_interpreter); PyThreadState_Swap(py_ctx.py_main_interpreter);
} }

View File

@@ -1,4 +1,4 @@
sudo.debug was called with arguments: (2, 'My demo purpose plugin shows this ERROR level debug message') 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: (6, 'My demo purpose plugin shows this INFO level debug message') sudo.debug was called with arguments: (<DEBUG.INFO: 6>, 'My demo purpose plugin shows this INFO level debug message')
sudo.options_as_dict was called with arguments: (('ModulePath=SRC_DIR/example_debugging.py', 'ClassName=DebugDemoPlugin'),) sudo.options_as_dict was called with arguments: (('ModulePath=SRC_DIR/example_debugging.py', 'ClassName=DebugDemoPlugin'),)
sudo.options_as_dict returned result: {'ModulePath': 'SRC_DIR/example_debugging.py', 'ClassName': 'DebugDemoPlugin'} sudo.options_as_dict returned result: {'ModulePath': 'SRC_DIR/example_debugging.py', 'ClassName': 'DebugDemoPlugin'}

View File

@@ -1,7 +1,7 @@
__init__ @ SRC_DIR/example_debugging.py:45 calls C function: __init__ @ SRC_DIR/example_debugging.py:45 calls C function:
sudo.debug was called with arguments: (2, 'My demo purpose plugin shows this ERROR level debug message') 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:48 calls C function: __init__ @ SRC_DIR/example_debugging.py:48 calls C function:
sudo.debug was called with arguments: (6, 'My demo purpose plugin shows this INFO level debug message') sudo.debug was called with arguments: (<DEBUG.INFO: 6>, 'My demo purpose plugin shows this INFO level debug message')
__init__ @ SRC_DIR/example_debugging.py:58 calls C function: __init__ @ SRC_DIR/example_debugging.py:58 calls C function:
sudo.options_as_dict was called with arguments: (('ModulePath=SRC_DIR/example_debugging.py', 'ClassName=DebugDemoPlugin'),) sudo.options_as_dict was called with arguments: (('ModulePath=SRC_DIR/example_debugging.py', 'ClassName=DebugDemoPlugin'),)
sudo.options_as_dict returned result: {'ModulePath': 'SRC_DIR/example_debugging.py', 'ClassName': 'DebugDemoPlugin'} sudo.options_as_dict returned result: {'ModulePath': 'SRC_DIR/example_debugging.py', 'ClassName': 'DebugDemoPlugin'}

View File

@@ -1,4 +1,4 @@
SudoGroupPlugin.__init__ was called with arguments: () {'args': ('ModulePath=SRC_DIR/example_group_plugin.py', 'ClassName=SudoGroupPlugin'), 'version': '1.0'} 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.__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 was called with arguments: ('user', 'group', ('pw_name', 'pw_passwd', 1001, 101, 'pw_gecos', 'pw_dir', 'pw_shell'))
SudoGroupPlugin.query returned result: 0 SudoGroupPlugin.query returned result: RC.REJECT

View File

@@ -498,6 +498,44 @@ cleanup:
debug_return_ptr(py_class); debug_return_ptr(py_class);
} }
void
sudo_module_register_enum(PyObject *py_module, const char *enum_name, PyObject *py_constants_dict)
{
// pseudo code:
// return 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 PyMODINIT_FUNC
sudo_module_init(void) sudo_module_init(void)
@@ -525,35 +563,41 @@ sudo_module_init(void)
MODULE_ADD_EXCEPTION(SudoException, NULL); MODULE_ADD_EXCEPTION(SudoException, NULL);
MODULE_ADD_EXCEPTION(ConversationInterrupted, EXC_VAR(SudoException)); 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 // constants
#define MODULE_ADD_INT_CONSTANT(constant) \ struct key_value_str_int constants_rc[] = {
do { \ {"OK", SUDO_RC_OK},
if (PyModule_AddIntConstant(py_module, #constant, SUDO_ ## constant) != 0) \ {"ACCEPT", SUDO_RC_ACCEPT},
goto cleanup; \ {"REJECT", SUDO_RC_REJECT},
} while(0) {"ERROR", SUDO_RC_ERROR},
{"USAGE_ERROR", SUDO_RC_USAGE_ERROR}
};
MODULE_REGISTER_ENUM("RC", constants_rc);
MODULE_ADD_INT_CONSTANT(RC_OK); struct key_value_str_int constants_conv[] = {
MODULE_ADD_INT_CONSTANT(RC_ACCEPT); {"PROMPT_ECHO_OFF", SUDO_CONV_PROMPT_ECHO_OFF},
MODULE_ADD_INT_CONSTANT(RC_REJECT); {"PROMPT_ECHO_ON", SUDO_CONV_PROMPT_ECHO_ON},
MODULE_ADD_INT_CONSTANT(RC_ERROR); {"INFO_MSG", SUDO_CONV_INFO_MSG},
MODULE_ADD_INT_CONSTANT(RC_USAGE_ERROR); {"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);
MODULE_ADD_INT_CONSTANT(CONV_PROMPT_ECHO_OFF); struct key_value_str_int constants_debug[] = {
MODULE_ADD_INT_CONSTANT(CONV_PROMPT_ECHO_ON); {"CRIT", SUDO_DEBUG_CRIT},
MODULE_ADD_INT_CONSTANT(CONV_ERROR_MSG); {"ERROR", SUDO_DEBUG_ERROR},
MODULE_ADD_INT_CONSTANT(CONV_INFO_MSG); {"WARN", SUDO_DEBUG_WARN},
MODULE_ADD_INT_CONSTANT(CONV_PROMPT_MASK); {"NOTICE", SUDO_DEBUG_NOTICE},
MODULE_ADD_INT_CONSTANT(CONV_PROMPT_ECHO_OK); {"DIAG", SUDO_DEBUG_DIAG},
MODULE_ADD_INT_CONSTANT(CONV_PREFER_TTY); {"INFO", SUDO_DEBUG_INFO},
{"TRACE", SUDO_DEBUG_TRACE},
MODULE_ADD_INT_CONSTANT(DEBUG_CRIT); {"DEBUG", SUDO_DEBUG_DEBUG}
MODULE_ADD_INT_CONSTANT(DEBUG_ERROR); };
MODULE_ADD_INT_CONSTANT(DEBUG_WARN); MODULE_REGISTER_ENUM("DEBUG", constants_debug);
MODULE_ADD_INT_CONSTANT(DEBUG_NOTICE);
MODULE_ADD_INT_CONSTANT(DEBUG_DIAG);
MODULE_ADD_INT_CONSTANT(DEBUG_INFO);
MODULE_ADD_INT_CONSTANT(DEBUG_TRACE);
MODULE_ADD_INT_CONSTANT(DEBUG_DEBUG);
// classes // classes
if (sudo_module_register_conv_message(py_module) != SUDO_RC_OK) if (sudo_module_register_conv_message(py_module) != SUDO_RC_OK)