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)):
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.
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
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:
@@ -36,8 +36,8 @@ class ReasonLoggerIOPlugin(sudo.Plugin):
# 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)
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)
@@ -49,7 +49,7 @@ class ReasonLoggerIOPlugin(sudo.Plugin):
except sudo.ConversationInterrupted:
sudo.log_error("You did not answer in time")
return sudo.RC_REJECT
return sudo.RC.REJECT
def on_conversation_suspend(self, signum):
# 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
Log levels
crit sudo.DEBUG_CRIT --> only cricital 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
crit sudo.DEBUG.CRIT --> only cricital 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").
@@ -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
# 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)
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.
# 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
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
sudo.RC.ACCEPT / sudo.RC.OK 1
sudo.RC.REJECT 0
sudo.RC.ERROR -1
sudo.RC.USAGE_ERROR -2
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
shown to the user and the plugin function returns sudo.RC_ERROR. If that is
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
not acceptable, catch it.
"""
@@ -39,4 +39,4 @@ class SudoGroupPlugin(sudo.Plugin):
}
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
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
sudo.RC.ACCEPT / sudo.RC.OK 1
sudo.RC.REJECT 0
sudo.RC.ERROR -1
sudo.RC.USAGE_ERROR -2
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
shown to the user and the plugin function returns sudo.RC_ERROR. If that is
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
not acceptable, catch it.
"""
@@ -78,7 +78,7 @@ class SudoIOPlugin(sudo.Plugin):
self._log("EXEC", " ".join(argv))
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:
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
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:
self._log("CLOSE", "Command returned {}".format(exit_status))
@@ -133,4 +133,4 @@ class SudoIOPlugin(sudo.Plugin):
def _log(self, type, message):
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
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
sudo.RC.ACCEPT / sudo.RC.OK 1
sudo.RC.REJECT 0
sudo.RC.ERROR -1
sudo.RC.USAGE_ERROR -2
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
shown to the user and the plugin function returns sudo.RC_ERROR. If that is
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
not acceptable, catch it.
"""
@@ -70,7 +70,7 @@ class SudoPolicyPlugin(sudo.Plugin):
# 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
return sudo.RC.REJECT
# The environment the command will be executed with (we allow any here)
user_env_out = sudo.options_from_dict(self.user_env) + env_add
@@ -83,9 +83,9 @@ class SudoPolicyPlugin(sudo.Plugin):
})
except SudoPluginError as 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, ...]):
"""Perform session setup
@@ -97,10 +97,10 @@ class SudoPolicyPlugin(sudo.Plugin):
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",))
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):
# return sudo.RC_OK
# return sudo.RC.OK
def list(self, argv: Tuple[str, ...], is_verbose: int, user: str):
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);
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);
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[]);

View File

@@ -243,6 +243,15 @@ _python_plugin_register_plugin_in_py_ctx(void)
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);
}

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: (6, 'My demo purpose plugin shows this INFO 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: (<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 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:
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:
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:
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'}

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__ 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
SudoGroupPlugin.query returned result: RC.REJECT

View File

@@ -498,6 +498,44 @@ cleanup:
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
sudo_module_init(void)
@@ -525,35 +563,41 @@ sudo_module_init(void)
MODULE_ADD_EXCEPTION(SudoException, NULL);
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
#define MODULE_ADD_INT_CONSTANT(constant) \
do { \
if (PyModule_AddIntConstant(py_module, #constant, SUDO_ ## constant) != 0) \
goto cleanup; \
} while(0)
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);
MODULE_ADD_INT_CONSTANT(RC_OK);
MODULE_ADD_INT_CONSTANT(RC_ACCEPT);
MODULE_ADD_INT_CONSTANT(RC_REJECT);
MODULE_ADD_INT_CONSTANT(RC_ERROR);
MODULE_ADD_INT_CONSTANT(RC_USAGE_ERROR);
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);
MODULE_ADD_INT_CONSTANT(CONV_PROMPT_ECHO_OFF);
MODULE_ADD_INT_CONSTANT(CONV_PROMPT_ECHO_ON);
MODULE_ADD_INT_CONSTANT(CONV_ERROR_MSG);
MODULE_ADD_INT_CONSTANT(CONV_INFO_MSG);
MODULE_ADD_INT_CONSTANT(CONV_PROMPT_MASK);
MODULE_ADD_INT_CONSTANT(CONV_PROMPT_ECHO_OK);
MODULE_ADD_INT_CONSTANT(CONV_PREFER_TTY);
MODULE_ADD_INT_CONSTANT(DEBUG_CRIT);
MODULE_ADD_INT_CONSTANT(DEBUG_ERROR);
MODULE_ADD_INT_CONSTANT(DEBUG_WARN);
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);
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);
// classes
if (sudo_module_register_conv_message(py_module) != SUDO_RC_OK)