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:

committed by
Todd C. Miller

parent
67ab6fd5d6
commit
21c02e1732
@@ -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.
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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[]);
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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'}
|
||||
|
@@ -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'}
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user