1import sudo
2import signal
3from os import path
4
5
6class ReasonLoggerIOPlugin(sudo.Plugin):
7    """
8    An example sudo plugin demonstrating how to use the sudo conversation API.
9
10    From the python plugin, you can ask something from the user using the
11    "sudo.conv" function. It expects one or more "sudo.ConvMessage" instances
12    which specifies how the interaction has to look like.
13
14    sudo.ConvMessage has the following fields (see help(sudo.ConvMessage)):
15        msg_type: int  Specifies the type of the conversation.
16                       See sudo.CONV.* constants below.
17        timeout: int   The maximum amount of time for the conversation
18                       in seconds. After the timeout exceeds, the "sudo.conv"
19                       function will raise sudo.ConversationInterrupted
20                       exception.
21        msg: str       The message to display for the user.
22
23    To specify the conversion type you can use the following constants:
24        sudo.CONV.PROMPT_ECHO_OFF
25        sudo.CONV.PROMPT_ECHO_ON
26        sudo.CONV.ERROR_MSG
27        sudo.CONV.INFO_MSG
28        sudo.CONV.PROMPT_MASK
29        sudo.CONV.PROMPT_ECHO_OK
30        sudo.CONV.PREFER_TTY
31    """
32
33    def open(self, argv, command_info):
34        try:
35            conv_timeout = 120  # in seconds
36            sudo.log_info("Please provide your reason "
37                          "for executing {}".format(argv))
38
39            # We ask two questions, the second is not visible on screen,
40            # so the user can hide a hidden message in case of criminals are
41            # forcing him for running the command.
42            # You can either specify the arguments in strict order (timeout
43            # being optional), or use named arguments.
44            message1 = sudo.ConvMessage(sudo.CONV.PROMPT_ECHO_ON,
45                                        "Reason: ",
46                                        conv_timeout)
47            message2 = sudo.ConvMessage(msg="Secret reason: ",
48                                        timeout=conv_timeout,
49                                        msg_type=sudo.CONV.PROMPT_MASK)
50            reply1, reply2 = sudo.conv(message1, message2,
51                                       on_suspend=self.on_conversation_suspend,
52                                       on_resume=self.on_conversation_resume)
53
54            with open(self._log_file_path(), "a") as file:
55                print("Executed", ' '.join(argv), file=file)
56                print("Reason:", reply1, file=file)
57                print("Hidden reason:", reply2, file=file)
58
59        except sudo.ConversationInterrupted:
60            sudo.log_error("You did not answer in time")
61            return sudo.RC.REJECT
62
63    def on_conversation_suspend(self, signum):
64        # This is just an example of how to do something on conversation
65        # suspend. You can skip specifying 'on_suspend' argument if there
66        # is no need
67        sudo.log_info("conversation suspend: signal",
68                      self._signal_name(signum))
69
70    def on_conversation_resume(self, signum):
71        # This is just an example of how to do something on conversation
72        # resume. You can skip specifying 'on_resume' argument if there
73        # is no need
74        sudo.log_info("conversation resume: signal was",
75                      self._signal_name(signum))
76
77    # helper functions:
78    if hasattr(signal, "Signals"):
79        @classmethod
80        def _signal_name(cls, signum: int):
81            try:
82                return signal.Signals(signum).name
83            except Exception:
84                return "{}".format(signum)
85    else:
86        @classmethod
87        def _signal_name(cls, signum: int):
88            for n, v in sorted(signal.__dict__.items()):
89                if v != signum:
90                    continue
91                if n.startswith("SIG") and not n.startswith("SIG_"):
92                    return n
93            return "{}".format(signum)
94
95    def _log_file_path(self):
96        options_dict = sudo.options_as_dict(self.plugin_options)
97        log_path = options_dict.get("LogPath", "/tmp")
98        return path.join(log_path, "sudo_reasons.txt")
99