/*
* 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 Mi¶kiewicz <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;
}