5ac19a42bd29b8757df652eb98d2ca0c2ee50473
[debian/sudo] / plugins / sample / sample_plugin.c
1 /*
2  * Copyright (c) 2010-2011 Todd C. Miller <Todd.Miller@courtesan.com>
3  *
4  * Permission to use, copy, modify, and distribute this software for any
5  * purpose with or without fee is hereby granted, provided that the above
6  * copyright notice and this permission notice appear in all copies.
7  *
8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15  */
16
17 #include <config.h>
18
19 #include <sys/types.h>
20 #include <sys/param.h>
21 #include <sys/stat.h>
22 #include <sys/wait.h>
23
24 #include <stdio.h>
25 #ifdef STDC_HEADERS
26 # include <stdlib.h>
27 # include <stddef.h>
28 #else
29 # ifdef HAVE_STDLIB_H
30 #  include <stdlib.h>
31 # endif
32 #endif /* STDC_HEADERS */
33 #ifdef HAVE_STRING_H
34 # if defined(HAVE_MEMORY_H) && !defined(STDC_HEADERS)
35 #  include <memory.h>
36 # endif
37 # include <string.h>
38 #endif /* HAVE_STRING_H */
39 #ifdef HAVE_STRINGS_H
40 # include <strings.h>
41 #endif /* HAVE_STRINGS_H */
42 #ifdef HAVE_UNISTD_H
43 # include <unistd.h>
44 #endif /* HAVE_UNISTD_H */
45 #include <ctype.h>
46 #include <fcntl.h>
47 #include <limits.h>
48 #include <grp.h>
49 #include <pwd.h>
50 #include <stdarg.h>
51
52 #include <pathnames.h>
53 #include "sudo_plugin.h"
54 #include "missing.h"
55
56 /*
57  * Sample plugin module that allows any user who knows the password
58  * ("test") to run any command as root.  Since there is no credential
59  * caching the validate and invalidate functions are NULL.
60  */
61
62 #ifdef __TANDEM
63 # define ROOT_UID       65535
64 #else
65 # define ROOT_UID       0
66 #endif
67
68 #undef TRUE
69 #define TRUE 1
70 #undef FALSE
71 #define FALSE 0
72 #undef ERROR
73 #define ERROR -1
74
75 static struct plugin_state {
76     char **envp;
77     char * const *settings;
78     char * const *user_info;
79 } plugin_state;
80 static sudo_conv_t sudo_conv;
81 static sudo_printf_t sudo_log;
82 static FILE *input, *output;
83 static uid_t runas_uid = ROOT_UID;
84 static gid_t runas_gid = -1;
85 static int use_sudoedit = FALSE;
86
87 /*
88  * Allocate storage for a name=value string and return it.
89  */
90 static char *
91 fmt_string(const char *var, const char *val)
92 {
93     size_t var_len = strlen(var);
94     size_t val_len = strlen(val);
95     char *cp, *str;
96
97     cp = str = malloc(var_len + 1 + val_len + 1);
98     if (str != NULL) {
99         memcpy(cp, var, var_len);
100         cp += var_len;
101         *cp++ = '=';
102         memcpy(cp, val, val_len);
103         cp += val_len;
104         *cp = '\0';
105     }
106
107     return str;
108 }
109
110 /*
111  * Plugin policy open function.
112  */
113 static int
114 policy_open(unsigned int version, sudo_conv_t conversation,
115     sudo_printf_t sudo_printf, char * const settings[],
116     char * const user_info[], char * const user_env[])
117 {
118     char * const *ui;
119     struct passwd *pw;
120     const char *runas_user = NULL;
121     struct group *gr;
122     const char *runas_group = NULL;
123
124     if (!sudo_conv)
125         sudo_conv = conversation;
126     if (!sudo_log)
127         sudo_log = sudo_printf;
128
129     if (SUDO_API_VERSION_GET_MAJOR(version) != SUDO_API_VERSION_MAJOR) {
130         sudo_log(SUDO_CONV_ERROR_MSG,
131             "the sample plugin requires API version %d.x\n",
132             SUDO_API_VERSION_MAJOR);
133         return ERROR;
134     }
135
136     /* Only allow commands to be run as root. */
137     for (ui = settings; *ui != NULL; ui++) {
138         if (strncmp(*ui, "runas_user=", sizeof("runas_user=") - 1) == 0) {
139             runas_user = *ui + sizeof("runas_user=") - 1;
140         }
141         if (strncmp(*ui, "runas_group=", sizeof("runas_group=") - 1) == 0) {
142             runas_group = *ui + sizeof("runas_group=") - 1;
143         }
144 #if !defined(HAVE_GETPROGNAME) && !defined(HAVE___PROGNAME)
145         if (strncmp(*ui, "progname=", sizeof("progname=") - 1) == 0) {
146             setprogname(*ui + sizeof("progname=") - 1);
147         }
148 #endif
149         /* Check to see if sudo was called as sudoedit or with -e flag. */
150         if (strncmp(*ui, "sudoedit=", sizeof("sudoedit=") - 1) == 0) {
151             if (strcasecmp(*ui + sizeof("sudoedit=") - 1, "true") == 0)
152                 use_sudoedit = TRUE;
153         }
154         /* This plugin doesn't support running sudo with no arguments. */
155         if (strncmp(*ui, "implied_shell=", sizeof("implied_shell=") - 1) == 0) {
156             if (strcasecmp(*ui + sizeof("implied_shell=") - 1, "true") == 0)
157                 return -2; /* usage error */
158         }
159     }
160     if (runas_user != NULL) {
161         if ((pw = getpwnam(runas_user)) == NULL) {
162             sudo_log(SUDO_CONV_ERROR_MSG, "unknown user %s\n", runas_user);
163             return 0;
164         }
165         runas_uid = pw->pw_uid;
166     }
167     if (runas_group != NULL) {
168         if ((gr = getgrnam(runas_group)) == NULL) {
169             sudo_log(SUDO_CONV_ERROR_MSG, "unknown group %s\n", runas_group);
170             return 0;
171         }
172         runas_gid = gr->gr_gid;
173     }
174
175     /* Plugin state. */
176     plugin_state.envp = (char **)user_env;
177     plugin_state.settings = settings;
178     plugin_state.user_info = user_info;
179
180     return 1;
181 }
182
183 static char *
184 find_in_path(char *command, char **envp)
185 {
186     struct stat sb;
187     char *path, *path0, **ep, *cp;
188     char pathbuf[PATH_MAX], *qualified = NULL;
189
190     if (strchr(command, '/') != NULL)
191         return command;
192
193     path = _PATH_DEFPATH;
194     for (ep = plugin_state.envp; *ep != NULL; ep++) {
195         if (strncmp(*ep, "PATH=", 5) == 0) {
196             path = *ep + 5;
197             break;
198         }
199     }
200     path = path0 = strdup(path);
201     do {
202         if ((cp = strchr(path, ':')))
203             *cp = '\0';
204         snprintf(pathbuf, sizeof(pathbuf), "%s/%s", *path ? path : ".",
205             command);
206         if (stat(pathbuf, &sb) == 0) {
207             if (S_ISREG(sb.st_mode) && (sb.st_mode & 0000111)) {
208                 qualified = pathbuf;
209                 break;
210             }
211         }
212         path = cp + 1;
213     } while (cp != NULL);
214     free(path0);
215     return qualified ? strdup(qualified) : NULL;
216 }
217
218 static int
219 check_passwd(void)
220 {
221     struct sudo_conv_message msg;
222     struct sudo_conv_reply repl;
223
224     /* Prompt user for password via conversation function. */
225     memset(&msg, 0, sizeof(msg));
226     msg.msg_type = SUDO_CONV_PROMPT_ECHO_OFF;
227     msg.msg = "Password: ";
228     memset(&repl, 0, sizeof(repl));
229     sudo_conv(1, &msg, &repl);
230     if (repl.reply == NULL) {
231         sudo_log(SUDO_CONV_ERROR_MSG, "missing password\n");
232         return FALSE;
233     }
234     if (strcmp(repl.reply, "test") != 0) {
235         sudo_log(SUDO_CONV_ERROR_MSG, "incorrect password\n");
236         return FALSE;
237     }
238     return TRUE;
239 }
240
241 static char **
242 build_command_info(char *command)
243 {
244     static char **command_info;
245     int i = 0;
246
247     /* Setup command info. */
248     command_info = calloc(32, sizeof(char *));
249     if (command_info == NULL)
250         return NULL;
251     if ((command_info[i++] = fmt_string("command", command)) == NULL ||
252         asprintf(&command_info[i++], "runas_euid=%ld", (long)runas_uid) == -1 ||
253         asprintf(&command_info[i++], "runas_uid=%ld", (long)runas_uid) == -1) {
254         return NULL;
255     }
256     if (runas_gid != -1) {
257         if (asprintf(&command_info[i++], "runas_gid=%ld", (long)runas_gid) == -1 ||
258             asprintf(&command_info[i++], "runas_egid=%ld", (long)runas_gid) == -1) {
259             return NULL;
260         }
261     }
262     if (use_sudoedit) {
263         command_info[i] = strdup("sudoedit=true");
264         if (command_info[i++] == NULL)
265                 return NULL;
266     }
267 #ifdef USE_TIMEOUT
268     command_info[i++] = "timeout=30";
269 #endif
270     return command_info;
271 }
272
273 static char *
274 find_editor(int nfiles, char * const files[], char **argv_out[])
275 {
276     char *cp, **ep, **nargv, *editor, *editor_path;
277     int ac, i, nargc, wasblank;
278
279     /* Lookup EDITOR in user's environment. */
280     editor = _PATH_VI;
281     for (ep = plugin_state.envp; *ep != NULL; ep++) {
282         if (strncmp(*ep, "EDITOR=", 7) == 0) {
283             editor = *ep + 7;
284             break;
285         }
286     }
287     editor = strdup(editor);
288     if (editor == NULL) {
289         sudo_log(SUDO_CONV_ERROR_MSG, "unable to allocate memory\n");
290         return NULL;
291     }
292
293     /*
294      * Split editor into an argument vector; editor is reused (do not free).
295      * The EDITOR environment variables may contain command
296      * line args so look for those and alloc space for them too.
297      */
298     nargc = 1;
299     for (wasblank = 0, cp = editor; *cp != '\0'; cp++) {
300         if (isblank((unsigned char) *cp))
301             wasblank = 1;
302         else if (wasblank) {
303             wasblank = 0;
304             nargc++;
305         }
306     }
307     /* If we can't find the editor in the user's PATH, give up. */
308     cp = strtok(editor, " \t");
309     if (cp == NULL ||
310         (editor_path = find_in_path(editor, plugin_state.envp)) == NULL) {
311         return NULL;
312     }
313     nargv = (char **) malloc((nargc + 1 + nfiles + 1) * sizeof(char *));
314     if (nargv == NULL) {
315         sudo_log(SUDO_CONV_ERROR_MSG, "unable to allocate memory\n");
316         return NULL;
317     }
318     for (ac = 0; cp != NULL && ac < nargc; ac++) {
319         nargv[ac] = cp;
320         cp = strtok(NULL, " \t");
321     }
322     nargv[ac++] = "--";
323     for (i = 0; i < nfiles; )
324         nargv[ac++] = files[i++];
325     nargv[ac] = NULL;
326
327     *argv_out = nargv;
328     return editor_path;
329 }
330
331 /*
332  * Plugin policy check function.
333  * Simple example that prompts for a password, hard-coded to "test".
334  */
335 static int 
336 policy_check(int argc, char * const argv[],
337     char *env_add[], char **command_info_out[],
338     char **argv_out[], char **user_env_out[])
339 {
340     char *command;
341
342     if (!argc || argv[0] == NULL) {
343         sudo_log(SUDO_CONV_ERROR_MSG, "no command specified\n");
344         return FALSE;
345     }
346
347     if (!check_passwd())
348         return FALSE;
349
350     command = find_in_path(argv[0], plugin_state.envp);
351     if (command == NULL) {
352         sudo_log(SUDO_CONV_ERROR_MSG, "%s: command not found\n", argv[0]);
353         return FALSE;
354     }
355
356     /* If "sudo vi" is run, auto-convert to sudoedit.  */
357     if (strcmp(command, _PATH_VI) == 0)
358         use_sudoedit = TRUE;
359
360     if (use_sudoedit) {
361         /* Rebuild argv using editor */
362         command = find_editor(argc - 1, argv + 1, argv_out);
363         if (command == NULL) {
364             sudo_log(SUDO_CONV_ERROR_MSG, "unable to find valid editor\n");
365             return ERROR;
366         }
367         use_sudoedit = TRUE;
368     } else {
369         /* No changes needd to argv */
370         *argv_out = (char **)argv;
371     }
372
373     /* No changes to envp */
374     *user_env_out = plugin_state.envp;
375
376     /* Setup command info. */
377     *command_info_out = build_command_info(command);
378     if (*command_info_out == NULL) {
379         sudo_log(SUDO_CONV_ERROR_MSG, "out of memory\n");
380         return ERROR;
381     }
382
383     return TRUE;
384 }
385
386 static int
387 policy_list(int argc, char * const argv[], int verbose, const char *list_user)
388 {
389     /*
390      * List user's capabilities.
391      */
392     sudo_log(SUDO_CONV_INFO_MSG, "Validated users may run any command\n");
393     return TRUE;
394 }
395
396 static int
397 policy_version(int verbose)
398 {
399     sudo_log(SUDO_CONV_INFO_MSG, "Sample policy plugin version %s\n", PACKAGE_VERSION);
400     return TRUE;
401 }
402
403 static void
404 policy_close(int exit_status, int error)
405 {
406     /*
407      * The policy might log the command exit status here.
408      * In this example, we just print a message.
409      */
410     if (error) {
411         sudo_log(SUDO_CONV_ERROR_MSG, "Command error: %s\n", strerror(error));
412     } else {
413         if (WIFEXITED(exit_status)) {
414             sudo_log(SUDO_CONV_INFO_MSG, "Command exited with status %d\n",
415                 WEXITSTATUS(exit_status));
416         } else if (WIFSIGNALED(exit_status)) {
417             sudo_log(SUDO_CONV_INFO_MSG, "Command killed by signal %d\n",
418                 WTERMSIG(exit_status));
419         }
420     }
421 }
422
423 static int
424 io_open(unsigned int version, sudo_conv_t conversation,
425     sudo_printf_t sudo_printf, char * const settings[],
426     char * const user_info[], char * const command_info[],
427     int argc, char * const argv[], char * const user_env[])
428 {
429     int fd;
430     char path[PATH_MAX];
431
432     if (!sudo_conv)
433         sudo_conv = conversation;
434     if (!sudo_log)
435         sudo_log = sudo_printf;
436
437     /* Open input and output files. */
438     snprintf(path, sizeof(path), "/var/tmp/sample-%u.output",
439         (unsigned int)getpid());
440     fd = open(path, O_WRONLY|O_CREAT|O_EXCL, 0644);
441     if (fd == -1)
442         return FALSE;
443     output = fdopen(fd, "w");
444
445     snprintf(path, sizeof(path), "/var/tmp/sample-%u.input",
446         (unsigned int)getpid());
447     fd = open(path, O_WRONLY|O_CREAT|O_EXCL, 0644);
448     if (fd == -1)
449         return FALSE;
450     input = fdopen(fd, "w");
451
452     return TRUE;
453 }
454
455 static void
456 io_close(int exit_status, int error)
457 {
458     fclose(input);
459     fclose(output);
460 }
461
462 static int
463 io_version(int verbose)
464 {
465     sudo_log(SUDO_CONV_INFO_MSG, "Sample I/O plugin version %s\n",
466         PACKAGE_VERSION);
467     return TRUE;
468 }
469
470 static int
471 io_log_input(const char *buf, unsigned int len)
472 {
473     fwrite(buf, len, 1, input);
474     return TRUE;
475 }
476
477 static int
478 io_log_output(const char *buf, unsigned int len)
479 {
480     fwrite(buf, len, 1, output);
481     return TRUE;
482 }
483
484 struct policy_plugin sample_policy = {
485     SUDO_POLICY_PLUGIN,
486     SUDO_API_VERSION,
487     policy_open,
488     policy_close,
489     policy_version,
490     policy_check,
491     policy_list,
492     NULL, /* validate */
493     NULL /* invalidate */
494 };
495
496 /*
497  * Note: This plugin does not differentiate between tty and pipe I/O.
498  *       It all gets logged to the same file.
499  */
500 struct io_plugin sample_io = {
501     SUDO_IO_PLUGIN,
502     SUDO_API_VERSION,
503     io_open,
504     io_close,
505     io_version,
506     io_log_input,       /* tty input */
507     io_log_output,      /* tty output */
508     io_log_input,       /* command stdin if not tty */
509     io_log_output,      /* command stdout if not tty */
510     io_log_output       /* command stderr if not tty */
511 };