]> git.ipfire.org Git - thirdparty/util-linux.git/blobdiff - login-utils/chsh.c
scriptreplay: cleanup usage()
[thirdparty/util-linux.git] / login-utils / chsh.c
index 8bd2d0bda2dd0ec011e763031045830495ce7ab2..a9ebec86ff66b8b67cfbb3a871b714f63582c9da 100644 (file)
@@ -1,6 +1,7 @@
 /*
  *   chsh.c -- change your login shell
  *   (c) 1994 by salvatore valente <svalente@athena.mit.edu>
+ *   (c) 2012 by Cody Maloney <cmaloney@theoreticalchaos.com>
  *
  *   this program is free software.  you can redistribute it and
  *   modify it under the terms of the gnu general public license.
  *   suggestion from Zefram.  Disallowing users with shells not in /etc/shells
  *   from changing their shell.
  *
- *   1999-02-22 Arkadiusz Mikiewicz <misiek@pld.ORG.PL>
+ *   1999-02-22 Arkadiusz Miśkiewicz <misiek@pld.ORG.PL>
  *   - added Native Language Support
- *
- *
  */
 
-#if 0
-#define _POSIX_SOURCE 1
-#endif
-
-#include <sys/types.h>
+#include <ctype.h>
+#include <errno.h>
+#include <getopt.h>
+#include <pwd.h>
 #include <stdio.h>
-#include <string.h>
 #include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
 #include <unistd.h>
-#include <pwd.h>
-#include <errno.h>
-#include <ctype.h>
-#include <getopt.h>
-#include "my_crypt.h"
+
+#include "c.h"
+#include "env.h"
+#include "closestream.h"
 #include "islocal.h"
-#include "setpwnam.h"
 #include "nls.h"
-#include "env.h"
+#include "pathnames.h"
+#include "pwdutils.h"
+#include "setpwnam.h"
+#include "strutils.h"
+#include "xalloc.h"
 
-#if defined(REQUIRE_PASSWORD) && defined(HAVE_SECURITY_PAM_MISC_H)
-#include <security/pam_appl.h>
-#include <security/pam_misc.h>
-#endif
+#include "ch-common.h"
 
 #ifdef HAVE_LIBSELINUX
-#include <selinux/selinux.h>
-#include <selinux/av_permissions.h>
-#include "selinux_utils.h"
+# include <selinux/selinux.h>
+# include "selinux_utils.h"
 #endif
 
-typedef unsigned char boolean;
-#define false 0
-#define true 1
 
-/* Only root is allowed to assign a luser a non-listed shell, by default */
-#define ONLY_LISTED_SHELLS 1
-
-
-static char *whoami;
+#ifdef HAVE_LIBUSER
+# include <libuser/user.h>
+# include "libuser.h"
+#elif CHFN_CHSH_PASSWORD
+# include "auth.h"
+#endif
 
-static char buf[FILENAME_MAX];
+#ifdef HAVE_LIBREADLINE
+# define _FUNCTION_DEF
+# include <readline/readline.h>
+#endif
 
 struct sinfo {
-    char *username;
-    char *shell;
+       char *username;
+       char *shell;
 };
 
-static void parse_argv (int argc, char *argv[], struct sinfo *pinfo);
-static void usage (FILE *fp);
-static char *prompt (char *question, char *def_val);
-static int check_shell (char *shell);
-static boolean get_shell_list (char *shell);
-static void *xmalloc (int bytes);
-
-#define memzero(ptr, size) memset((char *) ptr, 0, size)
-
-int
-main (int argc, char *argv[]) {
-    char *cp, *shell, *oldshell;
-    uid_t uid;
-    struct sinfo info;
-    struct passwd *pw;
-#if defined(REQUIRE_PASSWORD) && defined(HAVE_SECURITY_PAM_MISC_H)
-    pam_handle_t *pamh = NULL;
-    int retcode;
-    struct pam_conv conv = { misc_conv, NULL };
-#endif
-
-    sanitize_env();
-    setlocale(LC_ALL, "");
-    bindtextdomain(PACKAGE, LOCALEDIR);
-    textdomain(PACKAGE);
-
-    /* whoami is the program name for error messages */
-    whoami = argv[0];
-    if (! whoami) whoami = "chsh";
-    for (cp = whoami; *cp; cp++)
-       if (*cp == '/') whoami = cp + 1;
-
-    uid = getuid ();
-    memzero (&info, sizeof (info));
-
-    parse_argv (argc, argv, &info);
-    pw = NULL;
-    if (! info.username) {
-       pw = getpwuid (uid);
-       if (! pw) {
-           fprintf (stderr, _("%s: you (user %d) don't exist.\n"), whoami, uid);
-           return (-1); }
-    }
-    else {
-       pw = getpwnam (info.username);
-       if (! pw) {
-           cp = info.username;
-           fprintf (stderr, _("%s: user \"%s\" does not exist.\n"), whoami, cp);
-           return (-1); }
-    }
-
-    if (!(is_local(pw->pw_name))) {
-       fprintf (stderr, _("%s: can only change local entries; use yp%s instead.\n"),
-           whoami, whoami);
-       exit(1);
-    }
+static void __attribute__((__noreturn__)) usage(void)
+{
+       FILE *fp = stdout;
+       fputs(USAGE_HEADER, fp);
+       fprintf(fp, _(" %s [options] [<username>]\n"), program_invocation_short_name);
+
+       fputs(USAGE_SEPARATOR, fp);
+       fputs(_("Change your login shell.\n"), fp);
+
+       fputs(USAGE_OPTIONS, fp);
+       fputs(_(" -s, --shell <shell>  specify login shell\n"), fp);
+       fputs(_(" -l, --list-shells    print list of shells and exit\n"), fp);
+       fputs(USAGE_SEPARATOR, fp);
+       printf( " -u, --help           %s\n", USAGE_OPTSTR_HELP);
+       printf( " -v, --version        %s\n", USAGE_OPTSTR_VERSION);
+       printf(USAGE_MAN_TAIL("chsh(1)"));
+       exit(EXIT_SUCCESS);
+}
 
-#ifdef HAVE_LIBSELINUX
-    if (is_selinux_enabled()) {
-      if(uid == 0) {
-       if (checkAccess(pw->pw_name,PASSWD__CHSH)!=0) {
-         security_context_t user_context;
-         if (getprevcon(&user_context) < 0)
-           user_context=(security_context_t) strdup(_("Unknown user context"));
-         fprintf(stderr, _("%s: %s is not authorized to change the shell of %s\n"),
-                 whoami, user_context, pw->pw_name);
-         freecon(user_context);
-         exit(1);
+/*
+ *  is_known_shell() -- if the given shell appears in /etc/shells,
+ *     return true.  if not, return false.
+ */
+static int is_known_shell(const char *shell_name)
+{
+       char *s, ret = 0;
+
+       if (!shell_name)
+               return 0;
+
+       setusershell();
+       while ((s = getusershell())) {
+               if (strcmp(shell_name, s) == 0) {
+                       ret = 1;
+                       break;
+               }
        }
-      }
-      if (setupDefaultContext("/etc/passwd") != 0) {
-       fprintf(stderr,_("%s: Can't set default context for /etc/passwd"),
-               whoami);
-       exit(1);
-      }
-    }
-#endif
+       endusershell();
+       return ret;
+}
 
-    oldshell = pw->pw_shell;
-    if (!oldshell[0]) oldshell = "/bin/sh";
-
-    /* reality check */
-    if (uid != 0 && uid != pw->pw_uid) {
-       errno = EACCES;
-       fprintf(stderr,_("%s: Running UID doesn't match UID of user we're "
-                        "altering, shell change denied\n"), whoami);
-       return (-1);
-    }
-    if (uid != 0 && !get_shell_list(oldshell)) {
-       errno = EACCES;
-       fprintf(stderr,_("%s: Your shell is not in /etc/shells, shell change"
-               " denied\n"),whoami);
-       return (-1);
-    }
-    
-    shell = info.shell;
-
-    printf( _("Changing shell for %s.\n"), pw->pw_name );
-
-#ifdef REQUIRE_PASSWORD
-#ifdef HAVE_SECURITY_PAM_MISC_H
-    if(uid != 0) {
-        if (pam_start("chsh", pw->pw_name, &conv, &pamh)) {
-           puts(_("Password error."));
-           exit(1);
-       }
-        if (pam_authenticate(pamh, 0)) {
-           puts(_("Password error."));
-           exit(1);
-       }
-        retcode = pam_acct_mgmt(pamh, 0);
-        if (retcode == PAM_NEW_AUTHTOK_REQD)
-           retcode = pam_chauthtok(pamh, PAM_CHANGE_EXPIRED_AUTHTOK);
-        if (retcode) {
-           puts(_("Password error."));
-           exit(1);
-       }
-        if (pam_setcred(pamh, 0)) {
-           puts(_("Password error."));
-           exit(1);
+/*
+ *  print_shells () -- /etc/shells is outputted to stdout.
+ */
+static void print_shells(void)
+{
+       char *s;
+
+       while ((s = getusershell()))
+               printf("%s\n", s);
+       endusershell();
+}
+
+#ifdef HAVE_LIBREADLINE
+static char *shell_name_generator(const char *text, int state)
+{
+       static size_t len;
+       char *s;
+
+       if (!state) {
+               setusershell();
+               len = strlen(text);
        }
-        /* no need to establish a session; this isn't a session-oriented
-         * activity... */
-    }
-#else /* HAVE_SECURITY_PAM_MISC_H */
-    /* require password, unless root */
-    if(uid != 0 && pw->pw_passwd && pw->pw_passwd[0]) {
-       char *pwdstr = getpass(_("Password: "));
-       if(strncmp(pw->pw_passwd,
-                  crypt(pwdstr, pw->pw_passwd), 13)) {
-           puts(_("Incorrect password."));
-           exit(1);
+
+       while ((s = getusershell())) {
+               if (strncmp(s, text, len) == 0)
+                       return xstrdup(s);
        }
-    }
-#endif /* HAVE_SECURITY_PAM_MISC_H */
-#endif /* REQUIRE_PASSWORD */
-
-    if (! shell) {
-       shell = prompt (_("New shell"), oldshell);
-       if (! shell) return 0;
-    }
-    
-    if (check_shell (shell) < 0) return (-1);
-
-    if (! strcmp (pw->pw_shell, shell)) {
-       printf (_("Shell not changed.\n"));
-       return 0;
-    }
-    if (!strcmp(shell, "/bin/sh")) shell = "";
-    pw->pw_shell = shell;
-    if (setpwnam (pw) < 0) {
-       perror ("setpwnam");
-       printf( _("Shell *NOT* changed.  Try again later.\n") );
-       return (-1);
-    }
-    printf (_("Shell changed.\n"));
-    return 0;
+       return NULL;
+}
+
+static char **shell_name_completion(const char *text,
+                                   int start __attribute__((__unused__)),
+                                   int end __attribute__((__unused__)))
+{
+       rl_attempted_completion_over = 1;
+       return rl_completion_matches(text, shell_name_generator);
 }
+#endif
 
 /*
  *  parse_argv () --
  *     parse the command line arguments, and fill in "pinfo" with any
  *     information from the command line.
  */
-static void
-parse_argv (int argc, char *argv[], struct sinfo *pinfo) {
-    int index, c;
-
-    static struct option long_options[] = {
-       { "shell",       required_argument, 0, 's' },
-       { "list-shells", no_argument,       0, 'l' },
-       { "help",        no_argument,       0, 'u' },
-       { "version",     no_argument,       0, 'v' },
-       { NULL,          no_argument,       0, '0' },
-    };
-
-    optind = c = 0;
-    while (c != EOF) {
-       c = getopt_long (argc, argv, "s:luv", long_options, &index);
-       switch (c) {
-       case -1:
-           break;
-       case 'v':
-           printf ("%s\n", PACKAGE_STRING);
-           exit (0);
-       case 'u':
-           usage (stdout);
-           exit (0);
-       case 'l':
-           get_shell_list (NULL);
-           exit (0);
-       case 's':
-           if (! optarg) {
-               usage (stderr);
-               exit (-1);
-           }
-           pinfo->shell = optarg;
-           break;
-       default:
-           usage (stderr);
-           exit (-1);
+static void parse_argv(int argc, char **argv, struct sinfo *pinfo)
+{
+       static const struct option long_options[] = {
+               {"shell",       required_argument, NULL, 's'},
+               {"list-shells", no_argument,       NULL, 'l'},
+               {"help",        no_argument,       NULL, 'h'},
+               {"version",     no_argument,       NULL, 'v'},
+               {NULL, 0, NULL, 0},
+       };
+       int c;
+
+       while ((c = getopt_long(argc, argv, "s:lhuv", long_options, NULL)) != -1) {
+               switch (c) {
+               case 'v':
+                       print_version(EXIT_SUCCESS);
+               case 'u': /* deprecated */
+               case 'h':
+                       usage();
+               case 'l':
+                       print_shells();
+                       exit(EXIT_SUCCESS);
+               case 's':
+                       pinfo->shell = optarg;
+                       break;
+               default:
+                       errtryhelp(EXIT_FAILURE);
+               }
        }
-    }
-    /* done parsing arguments. check for a username. */
-    if (optind < argc) {
-       if (optind + 1 < argc) {
-           usage (stderr);
-           exit (-1);
+       /* done parsing arguments.  check for a username. */
+       if (optind < argc) {
+               if (optind + 1 < argc) {
+                       errx(EXIT_FAILURE, _("cannot handle multiple usernames"));
+               }
+               pinfo->username = argv[optind];
        }
-       pinfo->username = argv[optind];
-    }
 }
 
 /*
- *  usage () --
- *     print out a usage message.
+ *  ask_new_shell () --
+ *     ask the user for a shell and return it.
  */
-static void
-usage (FILE *fp) {
-    fprintf (fp,
-            _("Usage: %s [ -s shell ] [ --list-shells ] "
-              "[ --help ] [ --version ]\n"
-              "       [ username ]\n"), whoami);
+static char *ask_new_shell(char *question, char *oldshell)
+{
+       int len;
+       char *ans = NULL;
+#ifdef HAVE_LIBREADLINE
+       rl_attempted_completion_function = shell_name_completion;
+#else
+       size_t dummy = 0;
+#endif
+       if (!oldshell)
+               oldshell = "";
+       printf("%s [%s]\n", question, oldshell);
+#ifdef HAVE_LIBREADLINE
+       if ((ans = readline("> ")) == NULL)
+#else
+       if (getline(&ans, &dummy, stdin) < 0)
+#endif
+               return NULL;
+       /* remove the newline at the end of ans. */
+       ltrim_whitespace((unsigned char *) ans);
+       len = rtrim_whitespace((unsigned char *) ans);
+       if (len == 0)
+               return NULL;
+       return ans;
 }
 
 /*
- *  prompt () --
- *     ask the user for a given field and return it.
+ *  check_shell () -- if the shell is completely invalid, print
+ *     an error and exit.
  */
-static char *
-prompt (char *question, char *def_val) {
-    int len;
-    char *ans, *cp;
-  
-    if (! def_val) def_val = "";
-    printf("%s [%s]: ", question, def_val);
-    *buf = 0;
-    if (fgets (buf, sizeof (buf), stdin) == NULL) {
-       printf (_("\nAborted.\n"));
-       exit (-1);
-    }
-    /* remove the newline at the end of buf. */
-    ans = buf;
-    while (isspace (*ans)) ans++;
-    len = strlen (ans);
-    while (len > 0 && isspace (ans[len-1])) len--;
-    if (len <= 0) return NULL;
-    ans[len] = 0;
-    cp = (char *) xmalloc (len + 1);
-    strcpy (cp, ans);
-    return cp;
+static void check_shell(const char *shell)
+{
+       if (*shell != '/')
+               errx(EXIT_FAILURE, _("shell must be a full path name"));
+       if (access(shell, F_OK) < 0)
+               errx(EXIT_FAILURE, _("\"%s\" does not exist"), shell);
+       if (access(shell, X_OK) < 0)
+               errx(EXIT_FAILURE, _("\"%s\" is not executable"), shell);
+       if (illegal_passwd_chars(shell))
+               errx(EXIT_FAILURE, _("%s: has illegal characters"), shell);
+       if (!is_known_shell(shell)) {
+#ifdef ONLY_LISTED_SHELLS
+               if (!getuid())
+                       warnx(_("Warning: \"%s\" is not listed in %s."), shell,
+                             _PATH_SHELLS);
+               else
+                       errx(EXIT_FAILURE,
+                            _("\"%s\" is not listed in %s.\n"
+                              "Use %s -l to see list."), shell, _PATH_SHELLS,
+                            program_invocation_short_name);
+#else
+               warnx(_("\"%s\" is not listed in %s.\n"
+                       "Use %s -l to see list."), shell, _PATH_SHELLS,
+                      program_invocation_short_name);
+#endif
+       }
 }
 
-/*
- *  check_shell () -- if the shell is completely invalid, print
- *     an error and return (-1).
- *     if the shell is a bad idea, print a warning.
- */
-static int
-check_shell (char *shell) {
-    int i, c;
-
-    if (*shell != '/') {
-       printf (_("%s: shell must be a full path name.\n"), whoami);
-       return (-1);
-    }
-    if (access (shell, F_OK) < 0) {
-       printf (_("%s: \"%s\" does not exist.\n"), whoami, shell);
-       return (-1);
-    }
-    if (access (shell, X_OK) < 0) {
-       printf (_("%s: \"%s\" is not executable.\n"), whoami, shell);
-       return (-1);
-    }
-    /* keep /etc/passwd clean. */
-    for (i = 0; i < strlen (shell); i++) {
-       c = shell[i];
-       if (c == ',' || c == ':' || c == '=' || c == '"' || c == '\n') {
-           printf (_("%s: '%c' is not allowed.\n"), whoami, c);
-           return (-1);
+int main(int argc, char **argv)
+{
+       char *oldshell, *pwbuf;
+       int nullshell = 0;
+       const uid_t uid = getuid();
+       struct sinfo info = { NULL };
+       struct passwd *pw;
+
+       sanitize_env();
+       setlocale(LC_ALL, "");
+       bindtextdomain(PACKAGE, LOCALEDIR);
+       textdomain(PACKAGE);
+       close_stdout_atexit();
+
+       parse_argv(argc, argv, &info);
+       if (!info.username) {
+               pw = xgetpwuid(uid, &pwbuf);
+               if (!pw)
+                       errx(EXIT_FAILURE, _("you (user %d) don't exist."),
+                            uid);
+       } else {
+               pw = xgetpwnam(info.username, &pwbuf);
+               if (!pw)
+                       errx(EXIT_FAILURE, _("user \"%s\" does not exist."),
+                            info.username);
        }
-       if (iscntrl (c)) {
-           printf (_("%s: Control characters are not allowed.\n"), whoami);
-           return (-1);
+
+#ifndef HAVE_LIBUSER
+       if (!(is_local(pw->pw_name)))
+               errx(EXIT_FAILURE, _("can only change local entries"));
+#endif
+
+#ifdef HAVE_LIBSELINUX
+       if (is_selinux_enabled() > 0) {
+               if (uid == 0) {
+                       access_vector_t av = get_access_vector("passwd", "chsh");
+
+                       if (selinux_check_passwd_access(av) != 0) {
+                               security_context_t user_context;
+                               if (getprevcon(&user_context) < 0)
+                                       user_context =
+                                           (security_context_t) NULL;
+
+                               errx(EXIT_FAILURE,
+                                    _("%s is not authorized to change the shell of %s"),
+                                    user_context ? : _("Unknown user context"),
+                                    pw->pw_name);
+                       }
+               }
+               if (setupDefaultContext(_PATH_PASSWD) != 0)
+                       errx(EXIT_FAILURE,
+                            _("can't set default context for %s"), _PATH_PASSWD);
        }
-    }
-#ifdef ONLY_LISTED_SHELLS
-    if (! get_shell_list (shell)) {
-       if (!getuid())
-         printf (_("Warning: \"%s\" is not listed in /etc/shells\n"), shell);
-       else {
-         printf (_("%s: \"%s\" is not listed in /etc/shells.\n"),
-                 whoami, shell);
-         printf( _("%s: use -l option to see list\n"), whoami );
-         exit(1);
-       }
-    }
+#endif
+
+       oldshell = pw->pw_shell;
+       if (oldshell == NULL || *oldshell == '\0') {
+               oldshell = _PATH_BSHELL;        /* default */
+               nullshell = 1;
+       }
+
+       /* reality check */
+#ifdef HAVE_LIBUSER
+       /* If we're setuid and not really root, disallow the password change. */
+       if (geteuid() != getuid() && uid != pw->pw_uid) {
 #else
-    if (! get_shell_list (shell)) {
-       printf (_("Warning: \"%s\" is not listed in /etc/shells.\n"), shell);
-       printf( _("Use %s -l to see list.\n"), whoami );
-    }
+       if (uid != 0 && uid != pw->pw_uid) {
 #endif
-    return 0;
-}
+               errno = EACCES;
+               err(EXIT_FAILURE,
+                   _("running UID doesn't match UID of user we're "
+                     "altering, shell change denied"));
+       }
+       if (uid != 0 && !is_known_shell(oldshell)) {
+               errno = EACCES;
+               err(EXIT_FAILURE, _("your shell is not in %s, "
+                                   "shell change denied"), _PATH_SHELLS);
+       }
 
-/*
- *  get_shell_list () -- if the given shell appears in /etc/shells,
- *     return true.  if not, return false.
- *     if the given shell is NULL, /etc/shells is outputted to stdout.
- */
-static boolean
-get_shell_list (char *shell_name) {
-    FILE *fp;
-    boolean found;
-    int len;
-
-    found = false;
-    fp = fopen ("/etc/shells", "r");
-    if (! fp) {
-       if (! shell_name) printf (_("No known shells.\n"));
-       return true;
-    }
-    while (fgets (buf, sizeof (buf), fp) != NULL) {
-       /* ignore comments */
-       if (*buf == '#') continue;
-       len = strlen (buf);
-       /* strip the ending newline */
-       if (buf[len - 1] == '\n') buf[len - 1] = 0;
-       /* ignore lines that are too damn long */
-       else continue;
-       /* check or output the shell */
-       if (shell_name) {
-           if (! strcmp (shell_name, buf)) {
-               found = true;
-               break;
-           }
+       printf(_("Changing shell for %s.\n"), pw->pw_name);
+
+#if !defined(HAVE_LIBUSER) && defined(CHFN_CHSH_PASSWORD)
+       if (!auth_pam("chsh", uid, pw->pw_name)) {
+               return EXIT_FAILURE;
+       }
+#endif
+       if (!info.shell) {
+               info.shell = ask_new_shell(_("New shell"), oldshell);
+               if (!info.shell)
+                       return EXIT_SUCCESS;
        }
-       else printf ("%s\n", buf);
-    }
-    fclose (fp);
-    return found;
-}
 
-/*
- *  xmalloc () -- malloc that never fails.
- */
-static void *
-xmalloc (int bytes) {
-    void *vp;
-
-    vp = malloc (bytes);
-    if (! vp && bytes > 0) {
-       perror (_("malloc failed"));
-       exit (-1);
-    }
-    return vp;
+       check_shell(info.shell);
+
+       if (!nullshell && strcmp(oldshell, info.shell) == 0)
+               errx(EXIT_SUCCESS, _("Shell not changed."));
+
+#ifdef HAVE_LIBUSER
+       if (set_value_libuser("chsh", pw->pw_name, uid,
+           LU_LOGINSHELL, info.shell) < 0)
+               errx(EXIT_FAILURE, _("Shell *NOT* changed.  Try again later."));
+#else
+       pw->pw_shell = info.shell;
+       if (setpwnam(pw, ".chsh") < 0)
+               err(EXIT_FAILURE, _("setpwnam failed\n"
+                       "Shell *NOT* changed.  Try again later."));
+#endif
+
+       printf(_("Shell changed.\n"));
+       return EXIT_SUCCESS;
 }