]> granicus.if.org Git - sudo/commitdiff
Add a new flag, -e, that makes it possible to give users the ability
authorTodd C. Miller <Todd.Miller@courtesan.com>
Wed, 21 Jan 2004 22:25:10 +0000 (22:25 +0000)
committerTodd C. Miller <Todd.Miller@courtesan.com>
Wed, 21 Jan 2004 22:25:10 +0000 (22:25 +0000)
to edit files with the editor of their choice as the invoking user,
not the runas user.  Temporary files are used for the actual edit
and the temp file is copied over the original after the editor is done.

Makefile.in
parse.c
parse.lex
sudo.c
sudo_edit.c [new file with mode: 0644]

index 23402b235b1e6858dfc34c7562d5e1ac73bf5e8c..13f26daaac3d88d8feaf6625ff2567b1152b4cc3 100644 (file)
@@ -120,8 +120,8 @@ SRCS = alloc.c alloca.c check.c closefrom.c def_data.c defaults.c env.c err.c \
        fileops.c find_path.c fnmatch.c getcwd.c getprogname.c getspwuid.c \
        goodpath.c interfaces.c lex.yy.c lsearch.c logging.c parse.c parse.lex \
        parse.yacc set_perms.c sigaction.c snprintf.c strcasecmp.c strerror.c \
-       strlcat.c strlcpy.c sudo.c sudo.tab.c testsudoers.c tgetpass.c utime.c \
-       visudo.c zero_bytes.c $(AUTH_SRCS)
+       strlcat.c strlcpy.c sudo.c sudo.tab.c sudo_edit.c testsudoers.c \
+       tgetpass.c utime.c visudo.c zero_bytes.c $(AUTH_SRCS)
 
 AUTH_SRCS = auth/afs.c auth/aix_auth.c auth/bsdauth.c auth/dce.c auth/fwtk.c \
            auth/kerb4.c auth/kerb5.c auth/pam.c auth/passwd.c auth/rfc1938.c \
@@ -138,8 +138,8 @@ AUTH_OBJS = sudo_auth.o @AUTH_OBJS@
 PARSEOBJS = sudo.tab.o lex.yy.o alloc.o defaults.o
 
 SUDOBJS = check.o env.o getspwuid.o goodpath.o fileops.o find_path.o \
-         interfaces.o logging.o parse.o set_perms.o sudo.o tgetpass.o \
-         zero_bytes.o $(AUTH_OBJS) $(PARSEOBJS)
+         interfaces.o logging.o parse.o set_perms.o sudo.o sudo_edit.o \
+         tgetpass.o zero_bytes.o $(AUTH_OBJS) $(PARSEOBJS)
 
 VISUDOBJS = visudo.o fileops.o goodpath.o find_path.o $(PARSEOBJS)
 
diff --git a/parse.c b/parse.c
index bcfdc668a5a4766bf323ed943a1591477f448edd..2f653f91991652f9a90a04859ddb771d4b7d6083 100644 (file)
--- a/parse.c
+++ b/parse.c
@@ -260,9 +260,27 @@ command_matches(cmnd, cmnd_args, path, sudoers_args)
     char buf[MAXPATHLEN];
     static char *cmnd_base;
 
-    /* Don't bother with pseudo commands like "validate" */
-    if (strchr(cmnd, '/') == NULL)
-       return(FALSE);
+    /* Check for pseudo-commands */
+    if (*cmnd != '/') {
+       /*
+        * Return true if cmnd is "sudoedit" AND
+        *  a) there are no args in sudoers OR
+        *  b) there are no args on command line and none req by sudoers OR
+        *  c) there are args in sudoers and on command line and they match
+        */
+       if (strcmp(cmnd, "sudoedit") != 0)
+           return(FALSE);
+       if (!sudoers_args ||
+           (!cmnd_args && sudoers_args && !strcmp("\"\"", sudoers_args)) ||
+           (sudoers_args &&
+            fnmatch(sudoers_args, cmnd_args ? cmnd_args : "", 0) == 0)) {
+           if (safe_cmnd)
+               free(safe_cmnd);
+           safe_cmnd = estrdup(path);
+           return(TRUE);
+       } else
+           return(FALSE);
+    }
 
     plen = strlen(path);
 
@@ -299,8 +317,8 @@ command_matches(cmnd, cmnd_args, path, sudoers_args)
            return(FALSE);
        if (!sudoers_args ||
            (!cmnd_args && sudoers_args && !strcmp("\"\"", sudoers_args)) ||
-           (sudoers_args && fnmatch(sudoers_args, cmnd_args ? cmnd_args : "",
-           0) == 0)) {
+           (sudoers_args &&
+            fnmatch(sudoers_args, cmnd_args ? cmnd_args : "", 0) == 0)) {
            if (safe_cmnd)
                free(safe_cmnd);
            safe_cmnd = estrdup(user_cmnd);
index 2c5600867199593173f83851fb3566babba56f6f..599d33636a2e381c5948d1cc9248cb27b8b62ee8 100644 (file)
--- a/parse.lex
+++ b/parse.lex
@@ -292,6 +292,12 @@ EXEC[[:blank:]]*:  {
                            BEGIN INITIAL;
                        }
 
+sudoedit               {
+                           BEGIN GOTCMND;
+                           LEXTRACE("COMMAND ");
+                           fill_cmnd(yytext, yyleng);
+                       }                       /* sudo -e */
+
 \/(\\[\,:= \t#]|[^\,:=\\ \t\n#])+      {
                            /* directories can't have args... */
                            if (yytext[yyleng - 1] == '/') {
diff --git a/sudo.c b/sudo.c
index e8e0d0a8bb07d77dbb5b3a34f4c1576082d5f237..0b287952ca9444af382efbd491f17588083d1b7b 100644 (file)
--- a/sudo.c
+++ b/sudo.c
@@ -121,6 +121,7 @@ static void set_loginclass          __P((struct passwd *));
 static void usage                      __P((int));
 static void usage_excl                 __P((int));
 static struct passwd *get_authpw       __P((void));
+extern int sudo_edit                   __P((int, char **));
 extern void list_matches               __P((void));
 extern char **rebuild_env              __P((char **, int, int));
 extern char **zero_env                 __P((char **));
@@ -220,6 +221,8 @@ main(argc, argv, envp)
     pwflag = 0;
     if (sudo_mode & MODE_SHELL)
        user_cmnd = "shell";
+    else if (sudo_mode & MODE_EDIT)
+       user_cmnd = "sudoedit";
     else
        switch (sudo_mode) {
            case MODE_VERSION:
@@ -344,8 +347,11 @@ main(argc, argv, envp)
     if (!(validated & FLAG_NOPASS))
        check_user(validated & FLAG_CHECK_USER);
 
-    /* Build up custom environment that avoids any nasty bits. */
-    new_environ = rebuild_env(envp, sudo_mode, (validated & FLAG_NOEXEC));
+    /* Build a new environment that avoids any nasty bits if we have a cmnd. */
+    if (sudo_mode & MODE_RUN)
+       new_environ = rebuild_env(envp, sudo_mode, (validated & FLAG_NOEXEC));
+    else
+       new_environ = envp;
 
     if (validated & VALIDATE_OK) {
        /* Finally tell the user if the command did not exist. */
@@ -382,17 +388,18 @@ main(argc, argv, envp)
        (void) setrlimit(RLIMIT_CORE, &corelimit);
 #endif /* RLIMIT_CORE && !SUDO_DEVEL */
 
-       /* Become specified user or root. */
-       set_perms(PERM_FULL_RUNAS);
+       /* Become specified user or root if executing a command. */
+       if (sudo_mode & MODE_RUN)
+           set_perms(PERM_FULL_RUNAS);
 
        /* Close the password and group files */
        endpwent();
        endgrent();
 
-       /* Install the new environment. */
+       /* Install the real environment. */
        environ = new_environ;
 
-       if ((sudo_mode & MODE_LOGIN_SHELL)) {
+       if (sudo_mode & MODE_LOGIN_SHELL) {
            char *p;
 
            /* Convert /bin/sh -> -sh so shell knows it is a login shell */
@@ -412,6 +419,9 @@ main(argc, argv, envp)
        (void) sigaction(SIGTSTP, &saved_sa_tstp, NULL);
        (void) sigaction(SIGCHLD, &saved_sa_chld, NULL);
 
+       if (sudo_mode & MODE_EDIT)
+           exit(sudo_edit(NewArgc, NewArgv));
+
 #ifndef PROFILING
        if ((sudo_mode & MODE_BACKGROUND) && fork() > 0)
            exit(0);
@@ -571,14 +581,16 @@ init_vars(sudo_mode)
        set_perms(PERM_ROOT);
 
     /*
-     * If we were given the '-i' or '-s' options (run shell) we need to redo
+     * If we were given the '-e', '-i' or '-s' options we need to redo
      * NewArgv and NewArgc.
      */
-    if ((sudo_mode & MODE_SHELL)) {
+    if ((sudo_mode & (MODE_SHELL | MODE_EDIT))) {
        char **dst, **src = NewArgv;
 
        NewArgv = (char **) emalloc2((++NewArgc + 1), sizeof(char *));
-       if (sudo_mode & MODE_LOGIN_SHELL)
+       if (sudo_mode & MODE_EDIT)
+           NewArgv[0] = "sudoedit";
+       else if (sudo_mode & MODE_LOGIN_SHELL)
            NewArgv[0] = runas_pw->pw_shell;
        else if (user_shell && *user_shell)
            NewArgv[0] = user_shell;
@@ -594,16 +606,19 @@ init_vars(sudo_mode)
     set_loginclass(sudo_user.pw);
 
     /* Resolve the path and return. */
-    if ((sudo_mode & MODE_RUN)) {
-       /* XXX - default_runas may be modified during parsing of sudoers */
-       set_perms(PERM_RUNAS);
-       rval = find_path(NewArgv[0], &user_cmnd, user_path);
-       set_perms(PERM_ROOT);
-       if (rval != FOUND) {
-           /* Failed as root, try as invoking user. */
-           set_perms(PERM_USER);
+    rval = FOUND;
+    if (sudo_mode & (MODE_RUN | MODE_EDIT)) {
+       if (sudo_mode & MODE_RUN) {
+           /* XXX - default_runas may be modified during parsing of sudoers */
+           set_perms(PERM_RUNAS);
            rval = find_path(NewArgv[0], &user_cmnd, user_path);
            set_perms(PERM_ROOT);
+           if (rval != FOUND) {
+               /* Failed as root, try as invoking user. */
+               set_perms(PERM_USER);
+               rval = find_path(NewArgv[0], &user_cmnd, user_path);
+               set_perms(PERM_ROOT);
+           }
        }
 
        /* set user_args */
@@ -611,8 +626,8 @@ init_vars(sudo_mode)
            char *to, **from;
            size_t size, n;
 
-           /* If MODE_SHELL not set then NewArgv is contiguous so just count */
-           if (!(sudo_mode & MODE_SHELL)) {
+           /* If we didn't realloc NewArgv it is contiguous so just count. */
+           if (!(sudo_mode & (MODE_SHELL | MODE_EDIT))) {
                size = (size_t) (NewArgv[NewArgc-1] - NewArgv[1]) +
                        strlen(NewArgv[NewArgc-1]) + 1;
            } else {
@@ -620,7 +635,7 @@ init_vars(sudo_mode)
                    size += strlen(*from) + 1;
            }
 
-           /* alloc and copy. */
+           /* Alloc and build up user_args. */
            user_args = (char *) emalloc(size);
            for (to = user_args, from = NewArgv + 1; *from; from++) {
                n = strlcpy(to, *from, size - (to - user_args));
@@ -631,8 +646,7 @@ init_vars(sudo_mode)
            }
            *--to = '\0';
        }
-    } else
-       rval = FOUND;
+    }
 
     return(rval);
 }
@@ -709,6 +723,11 @@ parse_args(argc, argv)
            case 'b':
                rval |= MODE_BACKGROUND;
                break;
+           case 'e':
+               rval = MODE_EDIT;
+               if (excl && excl != 'e')
+                   usage_excl(1);
+               break;
            case 'v':
                rval = MODE_VALIDATE;
                if (excl && excl != 'v')
@@ -787,7 +806,8 @@ parse_args(argc, argv)
        NewArgv++;
     }
 
-    if (NewArgc > 0 && !(rval & MODE_RUN))
+    if ((NewArgc == 0 && (rval & MODE_EDIT)) ||
+       (NewArgc > 0 && !(rval & (MODE_RUN | MODE_EDIT))))
        usage(1);
 
     return(rval);
@@ -1041,8 +1061,7 @@ static void
 usage_excl(exit_val)
     int exit_val;
 {
-    (void) fprintf(stderr,
-       "Only one of the -h, -k, -K, -l, -s, -v or -V options may be used\n");
+    warnx("Only one of the -e, -h, -k, -K, -l, -s, -v or -V options may be used");
     usage(exit_val);
 }
 
@@ -1055,7 +1074,7 @@ usage(exit_val)
 {
 
     (void) fprintf(stderr, "usage: sudo -K | -L | -V | -h | -k | -l | -v\n");
-    (void) fprintf(stderr, "usage: sudo [-HPSb]%s%s [-p prompt] [-u username|#uid]\n            { -i | -s | <command> }\n",
+    (void) fprintf(stderr, "usage: sudo [-HPSb]%s%s [-p prompt] [-u username|#uid]\n            { -e file [...] | -i | -s | <command> }\n",
 #ifdef HAVE_BSD_AUTH_H
     " [-a auth_type]",
 #else
diff --git a/sudo_edit.c b/sudo_edit.c
new file mode 100644 (file)
index 0000000..b6bfd9b
--- /dev/null
@@ -0,0 +1,288 @@
+/*
+ * Copyright (c) 2004 Todd C. Miller <Todd.Miller@courtesan.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * 3. The name of the author may not be used to endorse or promote products
+ *    derived from this software without specific prior written permission.
+ *
+ * 4. Products derived from this software may not be called "Sudo" nor
+ *    may "Sudo" appear in their names without specific prior written
+ *    permission from the author.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
+ * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <sys/socket.h>
+#include <stdio.h>
+#ifdef STDC_HEADERS
+# include <stdlib.h>
+# include <stddef.h>
+#else
+# ifdef HAVE_STDLIB_H
+#  include <stdlib.h>
+# endif
+#endif /* STDC_HEADERS */
+#ifdef HAVE_STRING_H
+# include <string.h>
+#else
+# ifdef HAVE_STRINGS_H
+#  include <strings.h>
+# endif
+#endif /* HAVE_STRING_H */
+#ifdef HAVE_UNISTD_H
+# include <unistd.h>
+#endif /* HAVE_UNISTD_H */
+#ifdef HAVE_ERR_H
+# include <err.h>
+#else
+# include "emul/err.h"
+#endif /* HAVE_ERR_H */
+#include <ctype.h>
+#include <pwd.h>
+#include <errno.h>
+#include <fcntl.h>
+
+#include "sudo.h"
+
+#ifndef lint
+static const char rcsid[] = "$Sudo$";
+#endif /* lint */
+
+/*
+ * Wrapper to allow users to edit privileged files with their own uid.
+ */
+int sudo_edit(argc, argv)
+    int argc;
+    char **argv;
+{
+    ssize_t nread, nwritten;
+    pid_t pid;
+    const char *tmpdir;
+    char **nargv, **ap, *editor, *cp;
+    char buf[BUFSIZ];
+    int i, ac, ofd, tfd, nargc, rval;
+    struct stat sb;
+    struct tempfile {
+       char *tfile;
+       char *ofile;
+       time_t omtime;          /* XXX - use st_mtimespec / st_mtim? */
+       off_t osize;
+    } *tf;
+
+    /*
+     * Find our temporary directory, one of /var/tmp, /usr/tmp, or /tmp
+     */
+    if (stat(_PATH_VARTMP, &sb) == 0 && S_ISDIR(sb.st_mode))
+       tmpdir = _PATH_VARTMP;
+    else if (stat(_PATH_USRTMP, &sb) == 0 && S_ISDIR(sb.st_mode))
+       tmpdir = _PATH_USRTMP;
+    else
+       tmpdir = _PATH_TMP;
+
+    /*
+     * For each file specified, by the user, make a tempoary version
+     * and copy the contents of the original to it.  We make these files
+     * as root so the user can't steal them out from under us until we are
+     * done writing (and at that point the user will be able to edit the
+     * file anyway).
+     * XXX - It would be nice to lock the original files but that means
+     *       keeping an fd open for each file.
+     */
+    tf = emalloc2(argc - 1, sizeof(*tf));
+    memset(tf, 0, (argc - 1) * sizeof(*tf));
+    for (i = 0, ap = argv + 1; i < argc - 1 && *ap != NULL; i++, ap++) {
+       set_perms(PERM_RUNAS);
+       ofd = open(*ap, O_RDONLY, 0644);
+       if (ofd != -1) {
+#ifdef HAVE_FSTAT
+           if (fstat(ofd, &sb) != 0) {
+#else
+           if (stat(tf[i].ofile, &sb) != 0) {
+#endif
+               close(ofd);
+               ofd = -1;
+           }
+       }
+       set_perms(PERM_ROOT);
+       if (ofd == -1) {
+           if (errno != ENOENT) {
+               warn("%s", *ap);
+               argc--;
+               i--;
+               continue;
+           }
+           sb.st_mtime = 0;
+           sb.st_size = 0;
+       }
+       tf[i].ofile = *ap;
+       tf[i].omtime = sb.st_mtime;
+       tf[i].osize = sb.st_size;
+       if ((cp = strrchr(tf[i].ofile, '/')) != NULL)
+           cp++;
+       else
+           cp = tf[i].ofile;
+       easprintf(&tf[i].tfile, "%s%s.XXXXXXXX", tmpdir, cp);
+       if ((tfd = mkstemp(tf[i].tfile)) == -1) {
+           warn("mkstemp");
+           goto cleanup;
+       }
+       if (ofd != -1) {
+           while ((nread = read(ofd, buf, sizeof(buf))) != 0) {
+               if ((nwritten = write(tfd, buf, nread)) != nread) {
+                   if (nwritten == -1)
+                       warn("%s", tf[i].tfile);
+                   else
+                       warnx("%s: short write", tf[i].tfile);
+                   goto cleanup;
+               }
+           }
+       }
+#ifdef HAVE_FCHOWN
+       fchown(tfd, user_uid, user_gid);
+#else
+       chown(tf[i].tfile, user_uid, user_gid);
+#endif
+       if (ofd != -1)
+           close(ofd);
+       close(tfd);
+       touch(tf[i].tfile, tf[i].omtime);
+    }
+    if (argc == 1)
+       return(1);                      /* no files readable, you lose */
+
+    /*
+     * Determine which editor to use.  We don't bother restricting this
+     * based on def_env_editor or def_editor since the editor runs with
+     * the uid of the invoking user, not the runas (privileged) user.
+     */
+    if (((editor = getenv("VISUAL")) != NULL && *editor != '\0') ||
+       ((editor = getenv("EDITOR")) != NULL && *editor != '\0')) {
+       editor = estrdup(editor);
+    } else {
+       editor = estrdup(def_editor);
+       if ((cp = strchr(editor, ':')) != NULL)
+           *cp = '\0';                 /* def_editor could be a path */
+    }
+
+    /*
+     * Allocate space for the new argument vector and fill it in.
+     * The EDITOR and VISUAL environment variables may contain command
+     * line args so look for those and alloc space for them too.
+     */
+    nargc = argc;
+    for (cp = editor + 1; *cp != '\0'; cp++) {
+       if (isblank((unsigned char)cp[0]) && !isblank((unsigned char)cp[-1]))
+           nargc++;
+    }
+    nargv = (char **) emalloc2(nargc + 1, sizeof(char *));
+    ac = 0;
+    for ((cp = strtok(editor, " \t")); cp != NULL; (cp = strtok(NULL, " \t")))
+       nargv[ac++] = cp;
+    for (i = 0; i < argc - 1 && ac < nargc; )
+       nargv[ac++] = tf[i++].tfile;
+    nargv[ac] = NULL;
+
+    /*
+     * Fork and exec the editor as with the invoking user's creds.
+     */
+    pid = fork();
+    if (pid == -1) {
+       warn("fork");
+       goto cleanup;
+    } else if (pid == 0) {
+       /* child */
+       set_perms(PERM_FULL_USER);
+       execvp(nargv[0], nargv);
+       warn("unable to execute %s", nargv[0]);
+       _exit(127);
+    }
+
+    /* In parent, wait for child to finish. */
+#ifdef sudo_waitpid
+    pid = sudo_waitpid(pid, &i, 0);
+#else
+    pid = wait(&i);
+#endif
+    rval = pid == -1 ? -1 : (i >> 8);
+
+    /* Copy contents of temp files to real ones */
+    for (i = 0; i < argc - 1; i++) {
+       /* XXX - open file with PERM_USER for nfs? */
+       if ((tfd = open(tf[i].tfile, O_RDONLY, 0644)) == -1) {
+           warn("unable to read edited file %s, cannot update %s",
+               tf[i].tfile, tf[i].ofile);
+           continue;
+       }
+#ifdef HAVE_FSTAT
+       if (fstat(tfd, &sb) == 0) {
+#else
+       if (stat(tf[i].tfile, &sb) == 0) {
+#endif
+           if (tf[i].osize == sb.st_size && tf[i].omtime == sb.st_mtime) {
+               warnx("%s unchanged", tf[i].ofile);
+               unlink(tf[i].tfile);
+               close(tfd);
+               continue;
+           }
+       }
+       set_perms(PERM_RUNAS);
+       ofd = open(tf[i].ofile, O_WRONLY|O_TRUNC|O_CREAT, 0644);
+       set_perms(PERM_ROOT);
+       if (ofd == -1) {
+           warn("unable to save to %s, contents of edit session saved in %s",
+               tf[i].ofile, tf[i].tfile);
+           close(tfd);
+           continue;
+       }
+       while ((nread = read(tfd, buf, sizeof(buf))) != 0) {
+           if ((nwritten = write(ofd, buf, nread)) != nread) {
+               if (nwritten == -1)
+                   warn("%s", tf[i].ofile);
+               else
+                   warnx("%s: short write", tf[i].ofile);
+               break;
+           }
+       }
+       if (nread == 0)
+           unlink(tf[i].tfile);
+       else
+           warn("unable to save to %s, contents of edit session saved in %s",
+               tf[i].ofile, tf[i].tfile);
+       close(ofd);
+       close(tfd);
+    }
+
+    return(rval);
+cleanup:
+    /* Clean up temp files and return. */
+    for (i = 0; i < argc - 1; i++) {
+       if (tf[i].tfile != NULL)
+           unlink(tf[i].tfile);
+    }
+    return(1);
+}