diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-06 02:22:06 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-06 02:22:06 +0000 |
commit | 741c1ef7a4f2ac316ad6e557ddbe03023413478d (patch) | |
tree | 38890f681daa26c57e865b4feca10d0ca53e1046 /src | |
parent | Initial commit. (diff) | |
download | shadow-upstream.tar.xz shadow-upstream.zip |
Adding upstream version 1:4.5.upstream/1%4.5upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | src/.gitignore | 35 | ||||
-rw-r--r-- | src/.indent.pro | 5 | ||||
-rw-r--r-- | src/Makefile.am | 138 | ||||
-rw-r--r-- | src/chage.c | 948 | ||||
-rw-r--r-- | src/chfn.c | 753 | ||||
-rw-r--r-- | src/chgpasswd.c | 587 | ||||
-rw-r--r-- | src/chpasswd.c | 630 | ||||
-rw-r--r-- | src/chsh.c | 564 | ||||
-rw-r--r-- | src/expiry.c | 210 | ||||
-rw-r--r-- | src/faillog.c | 741 | ||||
-rw-r--r-- | src/gpasswd.c | 1207 | ||||
-rw-r--r-- | src/groupadd.c | 624 | ||||
-rw-r--r-- | src/groupdel.c | 491 | ||||
-rw-r--r-- | src/groupmems.c | 652 | ||||
-rw-r--r-- | src/groupmod.c | 871 | ||||
-rw-r--r-- | src/groups.c | 218 | ||||
-rw-r--r-- | src/grpck.c | 887 | ||||
-rw-r--r-- | src/grpconv.c | 286 | ||||
-rw-r--r-- | src/grpunconv.c | 250 | ||||
-rw-r--r-- | src/id.c | 207 | ||||
-rw-r--r-- | src/lastlog.c | 428 | ||||
-rw-r--r-- | src/login.c | 1339 | ||||
-rw-r--r-- | src/login_nopam.c | 336 | ||||
-rw-r--r-- | src/logoutd.c | 299 | ||||
-rw-r--r-- | src/newgidmap.c | 186 | ||||
-rw-r--r-- | src/newgrp.c | 853 | ||||
-rw-r--r-- | src/newuidmap.c | 186 | ||||
-rw-r--r-- | src/newusers.c | 1251 | ||||
-rw-r--r-- | src/nologin.c | 55 | ||||
-rw-r--r-- | src/passwd.c | 1168 | ||||
-rw-r--r-- | src/pwck.c | 893 | ||||
-rw-r--r-- | src/pwconv.c | 333 | ||||
-rw-r--r-- | src/pwunconv.c | 256 | ||||
-rw-r--r-- | src/su.c | 1222 | ||||
-rw-r--r-- | src/suauth.c | 239 | ||||
-rw-r--r-- | src/sulogin.c | 257 | ||||
-rw-r--r-- | src/useradd.c | 2315 | ||||
-rw-r--r-- | src/userdel.c | 1283 | ||||
-rw-r--r-- | src/usermod.c | 2285 | ||||
-rw-r--r-- | src/vipw.c | 562 |
40 files changed, 26050 insertions, 0 deletions
diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..d5716b9 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,35 @@ +/chage +/chfn +/chgpasswd +/chpasswd +/chsh +/expiry +/faillog +/gpasswd +/groupadd +/groupdel +/groupmems +/groupmod +/groups +/grpck +/grpconv +/grpunconv +/id +/lastlog +/login +/logoutd +/newgrp +/newgidmap +/newuidmap +/newusers +/nologin +/passwd +/pwck +/pwconv +/pwunconv +/su +/sulogin +/useradd +/userdel +/usermod +/vipw diff --git a/src/.indent.pro b/src/.indent.pro new file mode 100644 index 0000000..fe572bb --- /dev/null +++ b/src/.indent.pro @@ -0,0 +1,5 @@ +-kr +-i8 +-bad +-pcs +-l80 diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..12ef630 --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,138 @@ + +EXTRA_DIST = \ + .indent.pro + +ubindir = ${prefix}/bin +usbindir = ${prefix}/sbin +suidperms = 4755 +sgidperms = 2755 + +AM_CPPFLAGS = \ + -I${top_srcdir}/lib \ + -I$(top_srcdir)/libmisc \ + -DLOCALEDIR=\"$(datadir)/locale\" + +# XXX why are login and su in /bin anyway (other than for +# historical reasons)? +# +# if the system is screwed so badly that it can't mount /usr, +# you can (hopefully) boot single user, and then you're root +# so you don't need these programs for recovery. +# +# also /lib/libshadow.so.x.xx (if any) could be moved to /usr/lib +# and installation would be much simpler (just two directories, +# $prefix/bin and $prefix/sbin, no install-data hacks...) + +bin_PROGRAMS = groups login su +sbin_PROGRAMS = nologin +ubin_PROGRAMS = faillog lastlog chage chfn chsh expiry gpasswd newgrp passwd +if ENABLE_SUBIDS +ubin_PROGRAMS += newgidmap newuidmap +endif +usbin_PROGRAMS = \ + chgpasswd \ + chpasswd \ + groupadd \ + groupdel \ + groupmems \ + groupmod \ + grpck \ + grpconv \ + grpunconv \ + logoutd \ + newusers \ + pwck \ + pwconv \ + pwunconv \ + useradd \ + userdel \ + usermod \ + vipw + +# id and groups are from gnu, sulogin from sysvinit +noinst_PROGRAMS = id sulogin + +suidbins = su +suidubins = chage chfn chsh expiry gpasswd newgrp passwd +if ACCT_TOOLS_SETUID +suidubins += chage chgpasswd chpasswd groupadd groupdel groupmod newusers useradd userdel usermod +endif +if ENABLE_SUBIDS +suidubins += newgidmap newuidmap +endif + +if WITH_TCB +suidubins -= passwd +shadowsgidubins = passwd +endif + +LDADD = $(INTLLIBS) \ + $(LIBTCB) \ + $(top_builddir)/libmisc/libmisc.a \ + $(top_builddir)/lib/libshadow.la + +if ACCT_TOOLS_SETUID +LIBPAM_SUID = $(LIBPAM) +else +LIBPAM_SUID = +endif + +if USE_PAM +LIBCRYPT_NOPAM = +else +LIBCRYPT_NOPAM = $(LIBCRYPT) +endif + +chage_LDADD = $(LDADD) $(LIBPAM_SUID) $(LIBAUDIT) $(LIBSELINUX) +newuidmap_LDADD = $(LDADD) $(LIBSELINUX) +newgidmap_LDADD = $(LDADD) $(LIBSELINUX) +chfn_LDADD = $(LDADD) $(LIBPAM) $(LIBSELINUX) $(LIBCRYPT_NOPAM) $(LIBSKEY) $(LIBMD) +chgpasswd_LDADD = $(LDADD) $(LIBPAM_SUID) $(LIBSELINUX) $(LIBCRYPT) +chsh_LDADD = $(LDADD) $(LIBPAM) $(LIBSELINUX) $(LIBCRYPT_NOPAM) $(LIBSKEY) $(LIBMD) +chpasswd_LDADD = $(LDADD) $(LIBPAM) $(LIBSELINUX) $(LIBCRYPT) +gpasswd_LDADD = $(LDADD) $(LIBAUDIT) $(LIBSELINUX) $(LIBCRYPT) +groupadd_LDADD = $(LDADD) $(LIBPAM_SUID) $(LIBAUDIT) $(LIBSELINUX) +groupdel_LDADD = $(LDADD) $(LIBPAM_SUID) $(LIBAUDIT) $(LIBSELINUX) +groupmems_LDADD = $(LDADD) $(LIBPAM) $(LIBSELINUX) +groupmod_LDADD = $(LDADD) $(LIBPAM_SUID) $(LIBAUDIT) $(LIBSELINUX) +grpck_LDADD = $(LDADD) $(LIBSELINUX) +grpconv_LDADD = $(LDADD) $(LIBSELINUX) +grpunconv_LDADD = $(LDADD) $(LIBSELINUX) +lastlog_LDADD = $(LDADD) $(LIBAUDIT) +login_SOURCES = \ + login.c \ + login_nopam.c +login_LDADD = $(LDADD) $(LIBPAM) $(LIBAUDIT) $(LIBCRYPT_NOPAM) $(LIBSKEY) $(LIBMD) +newgrp_LDADD = $(LDADD) $(LIBAUDIT) $(LIBCRYPT) +newusers_LDADD = $(LDADD) $(LIBPAM) $(LIBSELINUX) $(LIBCRYPT) +nologin_LDADD = +passwd_LDADD = $(LDADD) $(LIBPAM) $(LIBCRACK) $(LIBAUDIT) $(LIBSELINUX) $(LIBCRYPT_NOPAM) +pwck_LDADD = $(LDADD) $(LIBSELINUX) +pwconv_LDADD = $(LDADD) $(LIBSELINUX) +pwunconv_LDADD = $(LDADD) $(LIBSELINUX) +su_SOURCES = \ + su.c \ + suauth.c +su_LDADD = $(LDADD) $(LIBPAM) $(LIBAUDIT) $(LIBCRYPT_NOPAM) $(LIBSKEY) $(LIBMD) +sulogin_LDADD = $(LDADD) $(LIBCRYPT) +useradd_LDADD = $(LDADD) $(LIBPAM_SUID) $(LIBAUDIT) $(LIBSELINUX) $(LIBSEMANAGE) $(LIBACL) $(LIBATTR) +userdel_LDADD = $(LDADD) $(LIBPAM_SUID) $(LIBAUDIT) $(LIBSELINUX) $(LIBSEMANAGE) +usermod_LDADD = $(LDADD) $(LIBPAM_SUID) $(LIBAUDIT) $(LIBSELINUX) $(LIBSEMANAGE) $(LIBACL) $(LIBATTR) +vipw_LDADD = $(LDADD) $(LIBSELINUX) + +install-am: all-am + $(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am + ln -sf newgrp $(DESTDIR)$(ubindir)/sg + ln -sf vipw $(DESTDIR)$(usbindir)/vigr + for i in $(suidbins); do \ + chmod $(suidperms) $(DESTDIR)$(bindir)/$$i; \ + done + for i in $(suidubins); do \ + chmod $(suidperms) $(DESTDIR)$(ubindir)/$$i; \ + done +if WITH_TCB + for i in $(shadowsgidubins); do \ + chown root:shadow $(DESTDIR)$(ubindir)/$$i; \ + chmod $(sgidperms) $(DESTDIR)$(ubindir)/$$i; \ + done +endif diff --git a/src/chage.c b/src/chage.c new file mode 100644 index 0000000..617e90f --- /dev/null +++ b/src/chage.c @@ -0,0 +1,948 @@ +/* + * Copyright (c) 1989 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2000 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2011, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <ctype.h> +#include <fcntl.h> +#include <getopt.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/types.h> +#include <time.h> +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM +#include "pam_defs.h" +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ +#include <pwd.h> +#ifdef WITH_SELINUX +#include <selinux/selinux.h> +#include <selinux/av_permissions.h> +#endif +#include "prototypes.h" +#include "defines.h" +#include "pwio.h" +#include "shadowio.h" +#ifdef WITH_TCB +#include "tcbfuncs.h" +#endif +/*@-exitarg@*/ +#include "exitcodes.h" + +/* + * Global variables + */ +const char *Prog; + +static bool + dflg = false, /* set last password change date */ + Eflg = false, /* set account expiration date */ + Iflg = false, /* set password inactive after expiration */ + lflg = false, /* show account aging information */ + mflg = false, /* set minimum number of days before password change */ + Mflg = false, /* set maximum number of days before password change */ + Wflg = false; /* set expiration warning days */ +static bool amroot = false; + +static bool pw_locked = false; /* Indicate if the password file is locked */ +static bool spw_locked = false; /* Indicate if the shadow file is locked */ +/* The name and UID of the user being worked on */ +static char user_name[BUFSIZ] = ""; +static uid_t user_uid = -1; + +static long mindays; +static long maxdays; +static long lstchgdate; +static long warndays; +static long inactdays; +static long expdate; + +/* local function prototypes */ +static /*@noreturn@*/void usage (int status); +static void date_to_str (char *buf, size_t maxsize, time_t date); +static int new_fields (void); +static void print_date (time_t date); +static void list_fields (void); +static void process_flags (int argc, char **argv); +static void check_flags (int argc, int opt_index); +static void check_perms (void); +static void open_files (bool readonly); +static void close_files (void); +static /*@noreturn@*/void fail_exit (int code); + +/* + * fail_exit - do some cleanup and exit with the given error code + */ +static /*@noreturn@*/void fail_exit (int code) +{ + if (spw_locked) { + if (spw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", spw_dbname ())); + /* continue */ + } + } + if (pw_locked) { + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + } + closelog (); + +#ifdef WITH_AUDIT + if (E_SUCCESS != code) { + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "change age", + user_name, (unsigned int) user_uid, 0); + } +#endif + + exit (code); +} + +/* + * usage - print command line syntax and exit + */ +static /*@noreturn@*/void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options] LOGIN\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -d, --lastday LAST_DAY set date of last password change to LAST_DAY\n"), usageout); + (void) fputs (_(" -E, --expiredate EXPIRE_DATE set account expiration date to EXPIRE_DATE\n"), usageout); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -I, --inactive INACTIVE set password inactive after expiration\n" + " to INACTIVE\n"), usageout); + (void) fputs (_(" -l, --list show account aging information\n"), usageout); + (void) fputs (_(" -m, --mindays MIN_DAYS set minimum number of days before password\n" + " change to MIN_DAYS\n"), usageout); + (void) fputs (_(" -M, --maxdays MAX_DAYS set maximim number of days before password\n" + " change to MAX_DAYS\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs (_(" -W, --warndays WARN_DAYS set expiration warning days to WARN_DAYS\n"), usageout); + (void) fputs ("\n", usageout); + exit (status); +} + +static void date_to_str (char *buf, size_t maxsize, time_t date) +{ + struct tm *tp; + + tp = gmtime (&date); +#ifdef HAVE_STRFTIME + (void) strftime (buf, maxsize, "%Y-%m-%d", tp); +#else + (void) snprintf (buf, maxsize, "%04d-%02d-%02d", + tp->tm_year + 1900, tp->tm_mon + 1, tp->tm_mday); +#endif /* HAVE_STRFTIME */ +} + +/* + * new_fields - change the user's password aging information interactively. + * + * prompt the user for all of the password age values. set the fields + * from the user's response, or leave alone if nothing was entered. The + * value (-1) is used to indicate the field should be removed if possible. + * any other negative value is an error. very large positive values will + * be handled elsewhere. + */ +static int new_fields (void) +{ + char buf[200]; + + (void) puts (_("Enter the new value, or press ENTER for the default")); + (void) puts (""); + + (void) snprintf (buf, sizeof buf, "%ld", mindays); + change_field (buf, sizeof buf, _("Minimum Password Age")); + if ( (getlong (buf, &mindays) == 0) + || (mindays < -1)) { + return 0; + } + + (void) snprintf (buf, sizeof buf, "%ld", maxdays); + change_field (buf, sizeof buf, _("Maximum Password Age")); + if ( (getlong (buf, &maxdays) == 0) + || (maxdays < -1)) { + return 0; + } + + if (-1 == lstchgdate) { + strcpy (buf, "-1"); + } else { + date_to_str (buf, sizeof buf, (time_t) lstchgdate * SCALE); + } + + change_field (buf, sizeof buf, _("Last Password Change (YYYY-MM-DD)")); + + if (strcmp (buf, "-1") == 0) { + lstchgdate = -1; + } else { + lstchgdate = strtoday (buf); + if (lstchgdate <= -1) { + return 0; + } + } + + (void) snprintf (buf, sizeof buf, "%ld", warndays); + change_field (buf, sizeof buf, _("Password Expiration Warning")); + if ( (getlong (buf, &warndays) == 0) + || (warndays < -1)) { + return 0; + } + + (void) snprintf (buf, sizeof buf, "%ld", inactdays); + change_field (buf, sizeof buf, _("Password Inactive")); + if ( (getlong (buf, &inactdays) == 0) + || (inactdays < -1)) { + return 0; + } + + if (-1 == expdate) { + strcpy (buf, "-1"); + } else { + date_to_str (buf, sizeof buf, (time_t) expdate * SCALE); + } + + change_field (buf, sizeof buf, + _("Account Expiration Date (YYYY-MM-DD)")); + + if (strcmp (buf, "-1") == 0) { + expdate = -1; + } else { + expdate = strtoday (buf); + if (expdate <= -1) { + return 0; + } + } + + return 1; +} + +static void print_date (time_t date) +{ +#ifdef HAVE_STRFTIME + struct tm *tp; + char buf[80]; + + tp = gmtime (&date); + if (NULL == tp) { + (void) printf ("time_t: %lu\n", (unsigned long)date); + } else { + (void) strftime (buf, sizeof buf, "%b %d, %Y", tp); + (void) puts (buf); + } +#else + struct tm *tp; + char *cp = NULL; + + tp = gmtime (&date); + if (NULL != tp) { + cp = asctime (tp); + } + if (NULL != cp) { + (void) printf ("%6.6s, %4.4s\n", cp + 4, cp + 20); + } else { + (void) printf ("time_t: %lu\n", date); + } +#endif +} + +/* + * list_fields - display the current values of the expiration fields + * + * display the password age information from the password fields. Date + * values will be displayed as a calendar date, or the word "never" if + * the date is 1/1/70, which is day number 0. + */ +static void list_fields (void) +{ + long changed = 0; + long expires; + + /* + * The "last change" date is either "never" or the date the password + * was last modified. The date is the number of days since 1/1/1970. + */ + (void) fputs (_("Last password change\t\t\t\t\t: "), stdout); + if (lstchgdate < 0) { + (void) puts (_("never")); + } else if (lstchgdate == 0) { + (void) puts (_("password must be changed")); + } else { + changed = lstchgdate * SCALE; + print_date ((time_t) changed); + } + + /* + * The password expiration date is determined from the last change + * date plus the number of days the password is valid for. + */ + (void) fputs (_("Password expires\t\t\t\t\t: "), stdout); + if (lstchgdate == 0) { + (void) puts (_("password must be changed")); + } else if ( (lstchgdate < 0) + || (maxdays >= (10000 * (DAY / SCALE))) + || (maxdays < 0)) { + (void) puts (_("never")); + } else { + expires = changed + maxdays * SCALE; + print_date ((time_t) expires); + } + + /* + * The account becomes inactive if the password is expired for more + * than "inactdays". The expiration date is calculated and the + * number of inactive days is added. The resulting date is when the + * active will be disabled. + */ + (void) fputs (_("Password inactive\t\t\t\t\t: "), stdout); + if (lstchgdate == 0) { + (void) puts (_("password must be changed")); + } else if ( (lstchgdate < 0) + || (inactdays < 0) + || (maxdays >= (10000 * (DAY / SCALE))) + || (maxdays < 0)) { + (void) puts (_("never")); + } else { + expires = changed + (maxdays + inactdays) * SCALE; + print_date ((time_t) expires); + } + + /* + * The account will expire on the given date regardless of the + * password expiring or not. + */ + (void) fputs (_("Account expires\t\t\t\t\t\t: "), stdout); + if (expdate < 0) { + (void) puts (_("never")); + } else { + expires = expdate * SCALE; + print_date ((time_t) expires); + } + + /* + * Start with the easy numbers - the number of days before the + * password can be changed, the number of days after which the + * password must be chaged, the number of days before the password + * expires that the user is told, and the number of days after the + * password expires that the account becomes unusable. + */ + printf (_("Minimum number of days between password change\t\t: %ld\n"), + mindays); + printf (_("Maximum number of days between password change\t\t: %ld\n"), + maxdays); + printf (_("Number of days of warning before password expires\t: %ld\n"), + warndays); +} + +/* + * process_flags - parse the command line options + * + * It will not return if an error is encountered. + */ +static void process_flags (int argc, char **argv) +{ + /* + * Parse the command line options. + */ + int c; + static struct option long_options[] = { + {"lastday", required_argument, NULL, 'd'}, + {"expiredate", required_argument, NULL, 'E'}, + {"help", no_argument, NULL, 'h'}, + {"inactive", required_argument, NULL, 'I'}, + {"list", no_argument, NULL, 'l'}, + {"mindays", required_argument, NULL, 'm'}, + {"maxdays", required_argument, NULL, 'M'}, + {"root", required_argument, NULL, 'R'}, + {"warndays", required_argument, NULL, 'W'}, + {NULL, 0, NULL, '\0'} + }; + + while ((c = getopt_long (argc, argv, "d:E:hI:lm:M:R:W:", + long_options, NULL)) != -1) { + switch (c) { + case 'd': + dflg = true; + lstchgdate = strtoday (optarg); + if (lstchgdate < -1) { + fprintf (stderr, + _("%s: invalid date '%s'\n"), + Prog, optarg); + usage (E_USAGE); + } + break; + case 'E': + Eflg = true; + expdate = strtoday (optarg); + if (expdate < -1) { + fprintf (stderr, + _("%s: invalid date '%s'\n"), + Prog, optarg); + usage (E_USAGE); + } + break; + case 'h': + usage (E_SUCCESS); + /*@notreached@*/break; + case 'I': + Iflg = true; + if ( (getlong (optarg, &inactdays) == 0) + || (inactdays < -1)) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + usage (E_USAGE); + } + break; + case 'l': + lflg = true; + break; + case 'm': + mflg = true; + if ( (getlong (optarg, &mindays) == 0) + || (mindays < -1)) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + usage (E_USAGE); + } + break; + case 'M': + Mflg = true; + if ( (getlong (optarg, &maxdays) == 0) + || (maxdays < -1)) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + usage (E_USAGE); + } + break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + case 'W': + Wflg = true; + if ( (getlong (optarg, &warndays) == 0) + || (warndays < -1)) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + usage (E_USAGE); + } + break; + default: + usage (E_USAGE); + } + } + + check_flags (argc, optind); +} + +/* + * check_flags - check flags and parameters consistency + * + * It will not return if an error is encountered. + */ +static void check_flags (int argc, int opt_index) +{ + /* + * Make certain the flags do not conflict and that there is a user + * name on the command line. + */ + + if (argc != opt_index + 1) { + usage (E_USAGE); + } + + if (lflg && (mflg || Mflg || dflg || Wflg || Iflg || Eflg)) { + fprintf (stderr, + _("%s: do not include \"l\" with other flags\n"), + Prog); + usage (E_USAGE); + } +} + +/* + * check_perms - check if the caller is allowed to add a group + * + * Non-root users are only allowed to display their aging information. + * (we will later make sure that the user is only listing her aging + * information) + * + * With PAM support, the setuid bit can be set on chage to allow + * non-root users to groups. + * Without PAM support, only users who can write in the group databases + * can add groups. + * + * It will not return if the user is not allowed. + */ +static void check_perms (void) +{ +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM + pam_handle_t *pamh = NULL; + struct passwd *pampw; + int retval; +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ + + /* + * An unprivileged user can ask for their own aging information, but + * only root can change it, or list another user's aging + * information. + */ + + if (!amroot && !lflg) { + fprintf (stderr, _("%s: Permission denied.\n"), Prog); + fail_exit (E_NOPERM); + } + +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM + pampw = getpwuid (getuid ()); /* local, no need for xgetpwuid */ + if (NULL == pampw) { + fprintf (stderr, + _("%s: Cannot determine your user name.\n"), + Prog); + exit (E_NOPERM); + } + + retval = pam_start ("chage", pampw->pw_name, &conv, &pamh); + + if (PAM_SUCCESS == retval) { + retval = pam_authenticate (pamh, 0); + } + + if (PAM_SUCCESS == retval) { + retval = pam_acct_mgmt (pamh, 0); + } + + if (PAM_SUCCESS != retval) { + fprintf (stderr, _("%s: PAM: %s\n"), + Prog, pam_strerror (pamh, retval)); + SYSLOG((LOG_ERR, "%s", pam_strerror (pamh, retval))); + if (NULL != pamh) { + (void) pam_end (pamh, retval); + } + fail_exit (E_NOPERM); + } + (void) pam_end (pamh, retval); +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ +} + +/* + * open_files - open the shadow database + * + * In read-only mode, the databases are not locked and are opened + * only for reading. + */ +static void open_files (bool readonly) +{ + /* + * Lock and open the password file. This loads all of the password + * file entries into memory. Then we get a pointer to the password + * file entry for the requested user. + */ + if (!readonly) { + if (pw_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, pw_dbname ()); + fail_exit (E_NOPERM); + } + pw_locked = true; + } + if (pw_open (readonly ? O_RDONLY: O_CREAT | O_RDWR) == 0) { + fprintf (stderr, _("%s: cannot open %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_WARN, "cannot open %s", pw_dbname ())); + fail_exit (E_NOPERM); + } + + /* + * For shadow password files we have to lock the file and read in + * the entries as was done for the password file. The user entries + * does not have to exist in this case; a new entry will be created + * for this user if one does not exist already. + */ + if (!readonly) { + if (spw_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, spw_dbname ()); + fail_exit (E_NOPERM); + } + spw_locked = true; + } + if (spw_open (readonly ? O_RDONLY: O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), Prog, spw_dbname ()); + SYSLOG ((LOG_WARN, "cannot open %s", spw_dbname ())); + fail_exit (E_NOPERM); + } +} + +/* + * close_files - close and unlock the password/shadow databases + */ +static void close_files (void) +{ + /* + * Now close the shadow password file, which will cause all of the + * entries to be re-written. + */ + if (spw_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", spw_dbname ())); + fail_exit (E_NOPERM); + } + + /* + * Close the password file. If any entries were modified, the file + * will be re-written. + */ + if (pw_close () == 0) { + fprintf (stderr, _("%s: failure while writing changes to %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", pw_dbname ())); + fail_exit (E_NOPERM); + } + if (spw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", spw_dbname ())); + /* continue */ + } + spw_locked = false; + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + pw_locked = false; +} + +/* + * update_age - update the aging information in the database + * + * It will not return in case of error + */ +static void update_age (/*@null@*/const struct spwd *sp, + /*@notnull@*/const struct passwd *pw) +{ + struct spwd spwent; + + /* + * There was no shadow entry. The new entry will have the encrypted + * password transferred from the normal password file along with the + * aging information. + */ + if (NULL == sp) { + struct passwd pwent = *pw; + + memzero (&spwent, sizeof spwent); + spwent.sp_namp = xstrdup (pwent.pw_name); + spwent.sp_pwdp = xstrdup (pwent.pw_passwd); + spwent.sp_flag = SHADOW_SP_FLAG_UNSET; + + pwent.pw_passwd = SHADOW_PASSWD_STRING; /* XXX warning: const */ + if (pw_update (&pwent) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), Prog, pw_dbname (), pwent.pw_name); + fail_exit (E_NOPERM); + } + } else { + spwent.sp_namp = xstrdup (sp->sp_namp); + spwent.sp_pwdp = xstrdup (sp->sp_pwdp); + spwent.sp_flag = sp->sp_flag; + } + + /* + * Copy the fields back to the shadow file entry and write the + * modified entry back to the shadow file. Closing the shadow and + * password files will commit any changes that have been made. + */ + spwent.sp_max = maxdays; + spwent.sp_min = mindays; + spwent.sp_lstchg = lstchgdate; + spwent.sp_warn = warndays; + spwent.sp_inact = inactdays; + spwent.sp_expire = expdate; + + if (spw_update (&spwent) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), Prog, spw_dbname (), spwent.sp_namp); + fail_exit (E_NOPERM); + } + +} + +/* + * get_defaults - get the value of the fields not set from the command line + */ +static void get_defaults (/*@null@*/const struct spwd *sp) +{ + /* + * Set the fields that aren't being set from the command line from + * the password file. + */ + if (NULL != sp) { + if (!Mflg) { + maxdays = sp->sp_max; + } + if (!mflg) { + mindays = sp->sp_min; + } + if (!dflg) { + lstchgdate = sp->sp_lstchg; + } + if (!Wflg) { + warndays = sp->sp_warn; + } + if (!Iflg) { + inactdays = sp->sp_inact; + } + if (!Eflg) { + expdate = sp->sp_expire; + } + } else { + /* + * Use default values that will not change the behavior of the + * account. + */ + if (!Mflg) { + maxdays = -1; + } + if (!mflg) { + mindays = -1; + } + if (!dflg) { + lstchgdate = -1; + } + if (!Wflg) { + warndays = -1; + } + if (!Iflg) { + inactdays = -1; + } + if (!Eflg) { + expdate = -1; + } + } +} + +/* + * chage - change a user's password aging information + * + * This command controls the password aging information. + * + * The valid options are + * + * -d set last password change date (*) + * -E set account expiration date (*) + * -I set password inactive after expiration (*) + * -l show account aging information + * -M set maximim number of days before password change (*) + * -m set minimum number of days before password change (*) + * -W set expiration warning days (*) + * + * (*) requires root permission to execute. + * + * All of the time fields are entered in the internal format which is + * either seconds or days. + */ + +int main (int argc, char **argv) +{ + const struct spwd *sp; + uid_t ruid; + gid_t rgid; + const struct passwd *pw; + + /* + * Get the program name so that error messages can use it. + */ + Prog = Basename (argv[0]); + + sanitize_env (); + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + +#ifdef WITH_AUDIT + audit_help_open (); +#endif + OPENLOG ("chage"); + + ruid = getuid (); + rgid = getgid (); + amroot = (ruid == 0); +#ifdef WITH_SELINUX + if (amroot && (is_selinux_enabled () > 0)) { + amroot = (selinux_check_passwd_access (PASSWD__ROOTOK) == 0); + } +#endif + + process_flags (argc, argv); + + check_perms (); + + if (!spw_file_present ()) { + fprintf (stderr, + _("%s: the shadow password file is not present\n"), + Prog); + SYSLOG ((LOG_WARN, "can't find the shadow password file")); + closelog (); + exit (E_SHADOW_NOTFOUND); + } + + open_files (lflg); + /* Drop privileges */ + if (lflg && ( (setregid (rgid, rgid) != 0) + || (setreuid (ruid, ruid) != 0))) { + fprintf (stderr, _("%s: failed to drop privileges (%s)\n"), + Prog, strerror (errno)); + fail_exit (E_NOPERM); + } + + pw = pw_locate (argv[optind]); + if (NULL == pw) { + fprintf (stderr, _("%s: user '%s' does not exist in %s\n"), + Prog, argv[optind], pw_dbname ()); + closelog (); + fail_exit (E_NOPERM); + } + + STRFCPY (user_name, pw->pw_name); +#ifdef WITH_TCB + if (shadowtcb_set_user (pw->pw_name) == SHADOWTCB_FAILURE) { + fail_exit (E_NOPERM); + } +#endif + user_uid = pw->pw_uid; + + sp = spw_locate (argv[optind]); + get_defaults (sp); + + /* + * Print out the expiration fields if the user has requested the + * list option. + */ + if (lflg) { + if (!amroot && (ruid != user_uid)) { + fprintf (stderr, _("%s: Permission denied.\n"), Prog); + fail_exit (E_NOPERM); + } +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "display aging info", + user_name, (unsigned int) user_uid, 1); +#endif + list_fields (); + fail_exit (E_SUCCESS); + } + + /* + * If none of the fields were changed from the command line, let the + * user interactively change them. + */ + if (!mflg && !Mflg && !dflg && !Wflg && !Iflg && !Eflg) { + printf (_("Changing the aging information for %s\n"), + user_name); + if (new_fields () == 0) { + fprintf (stderr, _("%s: error changing fields\n"), + Prog); + fail_exit (E_NOPERM); + } +#ifdef WITH_AUDIT + else { + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "change all aging information", + user_name, (unsigned int) user_uid, 1); + } +#endif + } else { +#ifdef WITH_AUDIT + if (Mflg) { + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "change max age", + user_name, (unsigned int) user_uid, 1); + } + if (mflg) { + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "change min age", + user_name, (unsigned int) user_uid, 1); + } + if (dflg) { + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "change last change date", + user_name, (unsigned int) user_uid, 1); + } + if (Wflg) { + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "change passwd warning", + user_name, (unsigned int) user_uid, 1); + } + if (Iflg) { + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "change inactive days", + user_name, (unsigned int) user_uid, 1); + } + if (Eflg) { + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "change passwd expiration", + user_name, (unsigned int) user_uid, 1); + } +#endif + } + + update_age (sp, pw); + + close_files (); + + SYSLOG ((LOG_INFO, "changed password expiry for %s", user_name)); + + closelog (); + exit (E_SUCCESS); +} + diff --git a/src/chfn.c b/src/chfn.c new file mode 100644 index 0000000..18aa3de --- /dev/null +++ b/src/chfn.c @@ -0,0 +1,753 @@ +/* + * Copyright (c) 1989 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2001 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2011, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <fcntl.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <sys/types.h> +#include <getopt.h> +#ifdef WITH_SELINUX +#include <selinux/selinux.h> +#include <selinux/av_permissions.h> +#endif +#include "defines.h" +#include "getdef.h" +#include "nscd.h" +#ifdef USE_PAM +#include "pam_defs.h" +#endif +#include "prototypes.h" +#include "pwauth.h" +#include "pwio.h" +/*@-exitarg@*/ +#include "exitcodes.h" + +/* + * Global variables. + */ +const char *Prog; +static char fullnm[BUFSIZ]; +static char roomno[BUFSIZ]; +static char workph[BUFSIZ]; +static char homeph[BUFSIZ]; +static char slop[BUFSIZ]; +static bool amroot; +/* Flags */ +static bool fflg = false; /* -f - set full name */ +static bool rflg = false; /* -r - set room number */ +static bool wflg = false; /* -w - set work phone number */ +static bool hflg = false; /* -h - set home phone number */ +static bool oflg = false; /* -o - set other information */ +static bool pw_locked = false; + +/* + * External identifiers + */ + +/* local function prototypes */ +static void fail_exit (int code); +static /*@noreturn@*/void usage (int status); +static bool may_change_field (int); +static void new_fields (void); +static char *copy_field (char *, char *, char *); +static void process_flags (int argc, char **argv); +static void check_perms (const struct passwd *pw); +static void update_gecos (const char *user, char *gecos); +static void get_old_fields (const char *gecos); + +/* + * fail_exit - exit with an error and do some cleanup + */ +static void fail_exit (int code) +{ + if (pw_locked) { + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + } + pw_locked = false; + + closelog (); + + exit (code); +} + +/* + * usage - print command line syntax and exit + */ +static /*@noreturn@*/void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options] [LOGIN]\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -f, --full-name FULL_NAME change user's full name\n"), usageout); + (void) fputs (_(" -h, --home-phone HOME_PHONE change user's home phone number\n"), usageout); + (void) fputs (_(" -o, --other OTHER_INFO change user's other GECOS information\n"), usageout); + (void) fputs (_(" -r, --room ROOM_NUMBER change user's room number\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs (_(" -u, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -w, --work-phone WORK_PHONE change user's office phone number\n"), usageout); + (void) fputs ("\n", usageout); + exit (status); +} + +/* + * may_change_field - indicate if the user is allowed to change a given field + * of her gecos information + * + * root can change any field. + * + * field should be one of 'f', 'r', 'w', 'h' + * + * Return true if the user can change the field and false otherwise. + */ +static bool may_change_field (int field) +{ + const char *cp; + + /* + * CHFN_RESTRICT can now specify exactly which fields may be changed + * by regular users, by using any combination of the following + * letters: + * f - full name + * r - room number + * w - work phone + * h - home phone + * + * This makes it possible to disallow changing the room number + * information, for example - this feature was suggested by Maciej + * 'Tycoon' Majchrowski. + * + * For backward compatibility, "yes" is equivalent to "rwh", + * "no" is equivalent to "frwh". Only root can change anything + * if the string is empty or not defined at all. + */ + if (amroot) { + return true; + } + + cp = getdef_str ("CHFN_RESTRICT"); + if (NULL == cp) { + cp = ""; + } else if (strcmp (cp, "yes") == 0) { + cp = "rwh"; + } else if (strcmp (cp, "no") == 0) { + cp = "frwh"; + } + + if (strchr (cp, field) != NULL) { + return true; + } + + return false; +} + +/* + * new_fields - change the user's GECOS information interactively + * + * prompt the user for each of the four fields and fill in the fields from + * the user's response, or leave alone if nothing was entered. + */ +static void new_fields (void) +{ + puts (_("Enter the new value, or press ENTER for the default")); + + if (may_change_field ('f')) { + change_field (fullnm, sizeof fullnm, _("Full Name")); + } else { + printf (_("\t%s: %s\n"), _("Full Name"), fullnm); + } + + if (may_change_field ('r')) { + change_field (roomno, sizeof roomno, _("Room Number")); + } else { + printf (_("\t%s: %s\n"), _("Room Number"), fullnm); + } + + if (may_change_field ('w')) { + change_field (workph, sizeof workph, _("Work Phone")); + } else { + printf (_("\t%s: %s\n"), _("Work Phone"), fullnm); + } + + if (may_change_field ('h')) { + change_field (homeph, sizeof homeph, _("Home Phone")); + } else { + printf (_("\t%s: %s\n"), _("Home Phone"), fullnm); + } + + if (amroot) { + change_field (slop, sizeof slop, _("Other")); + } +} + +/* + * copy_field - get the next field from the gecos field + * + * copy_field copies the next field from the gecos field, returning a + * pointer to the field which follows, or NULL if there are no more fields. + * + * in - the current GECOS field + * out - where to copy the field to + * extra - fields with '=' get copied here + */ +static char *copy_field (char *in, char *out, char *extra) +{ + char *cp = NULL; + + while (NULL != in) { + cp = strchr (in, ','); + if (NULL != cp) { + *cp++ = '\0'; + } + + if (strchr (in, '=') == NULL) { + break; + } + + if (NULL != extra) { + if ('\0' != extra[0]) { + strcat (extra, ","); + } + + strcat (extra, in); + } + in = cp; + } + if ((NULL != in) && (NULL != out)) { + strcpy (out, in); + } + + return cp; +} + +/* + * process_flags - parse the command line options + * + * It will not return if an error is encountered. + */ +static void process_flags (int argc, char **argv) +{ + int c; /* flag currently being processed */ + static struct option long_options[] = { + {"full-name", required_argument, NULL, 'f'}, + {"home-phone", required_argument, NULL, 'h'}, + {"other", required_argument, NULL, 'o'}, + {"room", required_argument, NULL, 'r'}, + {"root", required_argument, NULL, 'R'}, + {"help", no_argument, NULL, 'u'}, + {"work-phone", required_argument, NULL, 'w'}, + {NULL, 0, NULL, '\0'} + }; + + /* + * The remaining arguments will be processed one by one and executed + * by this command. The name is the last argument if it does not + * begin with a "-", otherwise the name is determined from the + * environment and must agree with the real UID. Also, the UID will + * be checked for any commands which are restricted to root only. + */ + while ((c = getopt_long (argc, argv, "f:h:o:r:R:uw:", + long_options, NULL)) != -1) { + switch (c) { + case 'f': + if (!may_change_field ('f')) { + fprintf (stderr, + _("%s: Permission denied.\n"), Prog); + exit (E_NOPERM); + } + fflg = true; + STRFCPY (fullnm, optarg); + break; + case 'h': + if (!may_change_field ('h')) { + fprintf (stderr, + _("%s: Permission denied.\n"), Prog); + exit (E_NOPERM); + } + hflg = true; + STRFCPY (homeph, optarg); + break; + case 'o': + if (!amroot) { + fprintf (stderr, + _("%s: Permission denied.\n"), Prog); + exit (E_NOPERM); + } + oflg = true; + STRFCPY (slop, optarg); + break; + case 'r': + if (!may_change_field ('r')) { + fprintf (stderr, + _("%s: Permission denied.\n"), Prog); + exit (E_NOPERM); + } + rflg = true; + STRFCPY (roomno, optarg); + break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + case 'u': + usage (E_SUCCESS); + /*@notreached@*/break; + case 'w': + if (!may_change_field ('w')) { + fprintf (stderr, + _("%s: Permission denied.\n"), Prog); + exit (E_NOPERM); + } + wflg = true; + STRFCPY (workph, optarg); + break; + default: + usage (E_USAGE); + } + } +} + +/* + * check_perms - check if the caller is allowed to add a group + * + * Non-root users are only allowed to change their gecos field. + * (see also may_change_field()) + * + * Non-root users must be authenticated. + * + * It will not return if the user is not allowed. + */ +static void check_perms (const struct passwd *pw) +{ +#ifdef USE_PAM + pam_handle_t *pamh = NULL; + int retval; + struct passwd *pampw; +#endif + + /* + * Non-privileged users are only allowed to change the gecos field + * if the UID of the user matches the current real UID. + */ + if (!amroot && pw->pw_uid != getuid ()) { + fprintf (stderr, _("%s: Permission denied.\n"), Prog); + closelog (); + exit (E_NOPERM); + } +#ifdef WITH_SELINUX + /* + * If the UID of the user does not match the current real UID, + * check if the change is allowed by SELinux policy. + */ + if ((pw->pw_uid != getuid ()) + && (is_selinux_enabled () > 0) + && (selinux_check_passwd_access (PASSWD__CHFN) != 0)) { + fprintf (stderr, _("%s: Permission denied.\n"), Prog); + closelog (); + exit (E_NOPERM); + } +#endif + +#ifndef USE_PAM + /* + * Non-privileged users are optionally authenticated (must enter the + * password of the user whose information is being changed) before + * any changes can be made. Idea from util-linux chfn/chsh. + * --marekm + */ + if (!amroot && getdef_bool ("CHFN_AUTH")) { + passwd_check (pw->pw_name, pw->pw_passwd, "chfn"); + } + +#else /* !USE_PAM */ + pampw = getpwuid (getuid ()); /* local, no need for xgetpwuid */ + if (NULL == pampw) { + fprintf (stderr, + _("%s: Cannot determine your user name.\n"), + Prog); + exit (E_NOPERM); + } + + retval = pam_start ("chfn", pampw->pw_name, &conv, &pamh); + + if (PAM_SUCCESS == retval) { + retval = pam_authenticate (pamh, 0); + } + + if (PAM_SUCCESS == retval) { + retval = pam_acct_mgmt (pamh, 0); + } + + if (PAM_SUCCESS != retval) { + fprintf (stderr, _("%s: PAM: %s\n"), + Prog, pam_strerror (pamh, retval)); + SYSLOG((LOG_ERR, "%s", pam_strerror (pamh, retval))); + if (NULL != pamh) { + (void) pam_end (pamh, retval); + } + exit (E_NOPERM); + } + (void) pam_end (pamh, retval); +#endif /* USE_PAM */ +} + +/* + * update_gecos - update the gecos fields in the password database + * + * Commit the user's entry after changing her gecos field. + */ +static void update_gecos (const char *user, char *gecos) +{ + const struct passwd *pw; /* The user's password file entry */ + struct passwd pwent; /* modified password file entry */ + + /* + * Before going any further, raise the ulimit to prevent colliding + * into a lowered ulimit, and set the real UID to root to protect + * against unexpected signals. Any keyboard signals are set to be + * ignored. + */ + if (setuid (0) != 0) { + fputs (_("Cannot change ID to root.\n"), stderr); + SYSLOG ((LOG_ERR, "can't setuid(0)")); + fail_exit (E_NOPERM); + } + pwd_init (); + + /* + * The passwd entry is now ready to be committed back to the + * password file. Get a lock on the file and open it. + */ + if (pw_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, pw_dbname ()); + fail_exit (E_NOPERM); + } + pw_locked = true; + if (pw_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), Prog, pw_dbname ()); + fail_exit (E_NOPERM); + } + + /* + * Get the entry to update using pw_locate() - we want the real one + * from /etc/passwd, not the one from getpwnam() which could contain + * the shadow password if (despite the warnings) someone enables + * AUTOSHADOW (or SHADOW_COMPAT in libc). --marekm + */ + pw = pw_locate (user); + if (NULL == pw) { + fprintf (stderr, + _("%s: user '%s' does not exist in %s\n"), + Prog, user, pw_dbname ()); + fail_exit (E_NOPERM); + } + + /* + * Make a copy of the entry, then change the gecos field. The other + * fields remain unchanged. + */ + pwent = *pw; + pwent.pw_gecos = gecos; + + /* + * Update the passwd file entry. If there is a DBM file, update that + * entry as well. + */ + if (pw_update (&pwent) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, pw_dbname (), pwent.pw_name); + fail_exit (E_NOPERM); + } + + /* + * Changes have all been made, so commit them and unlock the file. + */ + if (pw_close () == 0) { + fprintf (stderr, _("%s: failure while writing changes to %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", pw_dbname ())); + fail_exit (E_NOPERM); + } + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + pw_locked = false; +} + +/* + * get_old_fields - parse the old gecos and use the old value for the fields + * which are not set on the command line + */ +static void get_old_fields (const char *gecos) +{ + char *cp; /* temporary character pointer */ + char old_gecos[BUFSIZ]; /* buffer for old GECOS fields */ + STRFCPY (old_gecos, gecos); + + /* + * Now get the full name. It is the first comma separated field in + * the GECOS field. + */ + cp = copy_field (old_gecos, fflg ? (char *) 0 : fullnm, slop); + + /* + * Now get the room number. It is the next comma separated field, + * if there is indeed one. + */ + if (NULL != cp) { + cp = copy_field (cp, rflg ? (char *) 0 : roomno, slop); + } + + /* + * Now get the work phone number. It is the third field. + */ + if (NULL != cp) { + cp = copy_field (cp, wflg ? (char *) 0 : workph, slop); + } + + /* + * Now get the home phone number. It is the fourth field. + */ + if (NULL != cp) { + cp = copy_field (cp, hflg ? (char *) 0 : homeph, slop); + } + + /* + * Anything left over is "slop". + */ + if ((NULL != cp) && !oflg) { + if ('\0' != slop[0]) { + strcat (slop, ","); + } + + strcat (slop, cp); + } +} + +/* + * check_fields - check all of the fields for valid information + * + * It will not return if a field is not valid. + */ +static void check_fields (void) +{ + int err; + err = valid_field (fullnm, ":,=\n"); + if (err > 0) { + fprintf (stderr, _("%s: name with non-ASCII characters: '%s'\n"), Prog, fullnm); + } else if (err < 0) { + fprintf (stderr, _("%s: invalid name: '%s'\n"), Prog, fullnm); + fail_exit (E_NOPERM); + } + err = valid_field (roomno, ":,=\n"); + if (err > 0) { + fprintf (stderr, _("%s: room number with non-ASCII characters: '%s'\n"), Prog, roomno); + } else if (err < 0) { + fprintf (stderr, _("%s: invalid room number: '%s'\n"), + Prog, roomno); + fail_exit (E_NOPERM); + } + if (valid_field (workph, ":,=\n") != 0) { + fprintf (stderr, _("%s: invalid work phone: '%s'\n"), + Prog, workph); + fail_exit (E_NOPERM); + } + if (valid_field (homeph, ":,=\n") != 0) { + fprintf (stderr, _("%s: invalid home phone: '%s'\n"), + Prog, homeph); + fail_exit (E_NOPERM); + } + err = valid_field (slop, ":\n"); + if (err > 0) { + fprintf (stderr, _("%s: '%s' contains non-ASCII characters\n"), Prog, slop); + } else if (err < 0) { + fprintf (stderr, + _("%s: '%s' contains illegal characters\n"), + Prog, slop); + fail_exit (E_NOPERM); + } +} + +/* + * chfn - change a user's password file information + * + * This command controls the GECOS field information in the password + * file entry. + * + * The valid options are + * + * -f full name + * -r room number + * -w work phone number + * -h home phone number + * -o other information (*) + * + * (*) requires root permission to execute. + */ +int main (int argc, char **argv) +{ + const struct passwd *pw; /* password file entry */ + char new_gecos[BUFSIZ]; /* buffer for new GECOS fields */ + char *user; + + /* + * Get the program name. The program name is used as a + * prefix to most error messages. + */ + Prog = Basename (argv[0]); + + sanitize_env (); + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + /* + * This command behaves different for root and non-root + * users. + */ + amroot = (getuid () == 0); + + OPENLOG ("chfn"); + + /* parse the command line options */ + process_flags (argc, argv); + + /* + * Get the name of the user to check. It is either the command line + * name, or the name getlogin() returns. + */ + if (optind < argc) { + user = argv[optind]; + pw = xgetpwnam (user); + if (NULL == pw) { + fprintf (stderr, _("%s: user '%s' does not exist\n"), Prog, + user); + fail_exit (E_NOPERM); + } + } else { + pw = get_my_pwent (); + if (NULL == pw) { + fprintf (stderr, + _("%s: Cannot determine your user name.\n"), + Prog); + SYSLOG ((LOG_WARN, "Cannot determine the user name of the caller (UID %lu)", + (unsigned long) getuid ())); + fail_exit (E_NOPERM); + } + user = xstrdup (pw->pw_name); + } + +#ifdef USE_NIS + /* + * Now we make sure this is a LOCAL password entry for this user ... + */ + if (__ispwNIS ()) { + char *nis_domain; + char *nis_master; + + fprintf (stderr, + _("%s: cannot change user '%s' on NIS client.\n"), + Prog, user); + + if (!yp_get_default_domain (&nis_domain) && + !yp_master (nis_domain, "passwd.byname", &nis_master)) { + fprintf (stderr, + _ + ("%s: '%s' is the NIS master for this client.\n"), + Prog, nis_master); + } + fail_exit (E_NOPERM); + } +#endif + + /* Check that the caller is allowed to change the gecos of the + * specified user */ + check_perms (pw); + + /* If some fields were not set on the command line, load the value from + * the old gecos fields. */ + get_old_fields (pw->pw_gecos); + + /* + * If none of the fields were changed from the command line, let the + * user interactively change them. + */ + if (!fflg && !rflg && !wflg && !hflg && !oflg) { + printf (_("Changing the user information for %s\n"), user); + new_fields (); + } + + /* + * Check all of the fields for valid information + */ + check_fields (); + + /* + * Build the new GECOS field by plastering all the pieces together, + * if they will fit ... + */ + if ((strlen (fullnm) + strlen (roomno) + strlen (workph) + + strlen (homeph) + strlen (slop)) > (unsigned int) 80) { + fprintf (stderr, _("%s: fields too long\n"), Prog); + fail_exit (E_NOPERM); + } + snprintf (new_gecos, sizeof new_gecos, "%s,%s,%s,%s%s%s", + fullnm, roomno, workph, homeph, + ('\0' != slop[0]) ? "," : "", slop); + + /* Rewrite the user's gecos in the passwd file */ + update_gecos (user, new_gecos); + + SYSLOG ((LOG_INFO, "changed user '%s' information", user)); + + nscd_flush_cache ("passwd"); + + closelog (); + exit (E_SUCCESS); +} + diff --git a/src/chgpasswd.c b/src/chgpasswd.c new file mode 100644 index 0000000..13203a4 --- /dev/null +++ b/src/chgpasswd.c @@ -0,0 +1,587 @@ +/* + * Copyright (c) 1990 - 1994, Julianne Frances Haugh + * Copyright (c) 2006 , Tomasz Kłoczko + * Copyright (c) 2006 , Jonas Meurer + * Copyright (c) 2007 - 2011, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <fcntl.h> +#include <getopt.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM +#include "pam_defs.h" +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ +#include "defines.h" +#include "nscd.h" +#include "prototypes.h" +#include "groupio.h" +#ifdef SHADOWGRP +#include "sgroupio.h" +#endif +/*@-exitarg@*/ +#include "exitcodes.h" + +/* + * Global variables + */ +const char *Prog; +static bool eflg = false; +static bool md5flg = false; +#ifdef USE_SHA_CRYPT +static bool sflg = false; +#endif + +static /*@null@*//*@observer@*/const char *crypt_method = NULL; +#define cflg (NULL != crypt_method) +#ifdef USE_SHA_CRYPT +static long sha_rounds = 5000; +#endif + +#ifdef SHADOWGRP +static bool is_shadow_grp; +static bool sgr_locked = false; +#endif +static bool gr_locked = false; + +/* local function prototypes */ +static void fail_exit (int code); +static /*@noreturn@*/void usage (int status); +static void process_flags (int argc, char **argv); +static void check_flags (void); +static void check_perms (void); +static void open_files (void); +static void close_files (void); + +/* + * fail_exit - exit with a failure code after unlocking the files + */ +static void fail_exit (int code) +{ + if (gr_locked) { + if (gr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", gr_dbname ())); + /* continue */ + } + } + +#ifdef SHADOWGRP + if (sgr_locked) { + if (sgr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sgr_dbname ())); + /* continue */ + } + } +#endif + + exit (code); +} + +/* + * usage - display usage message and exit + */ +static /*@noreturn@*/void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options]\n" + "\n" + "Options:\n"), + Prog); + (void) fprintf (usageout, + _(" -c, --crypt-method METHOD the crypt method (one of %s)\n"), +#ifndef USE_SHA_CRYPT + "NONE DES MD5" +#else /* USE_SHA_CRYPT */ + "NONE DES MD5 SHA256 SHA512" +#endif /* USE_SHA_CRYPT */ + ); + (void) fputs (_(" -e, --encrypted supplied passwords are encrypted\n"), usageout); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -m, --md5 encrypt the clear text password using\n" + " the MD5 algorithm\n"), + usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); +#ifdef USE_SHA_CRYPT + (void) fputs (_(" -s, --sha-rounds number of SHA rounds for the SHA*\n" + " crypt algorithms\n"), + usageout); +#endif /* USE_SHA_CRYPT */ + (void) fputs ("\n", usageout); + + exit (status); +} + +/* + * process_flags - parse the command line options + * + * It will not return if an error is encountered. + */ +static void process_flags (int argc, char **argv) +{ + int c; + static struct option long_options[] = { + {"crypt-method", required_argument, NULL, 'c'}, + {"encrypted", no_argument, NULL, 'e'}, + {"help", no_argument, NULL, 'h'}, + {"md5", no_argument, NULL, 'm'}, + {"root", required_argument, NULL, 'R'}, +#ifdef USE_SHA_CRYPT + {"sha-rounds", required_argument, NULL, 's'}, +#endif + {NULL, 0, NULL, '\0'} + }; + + while ((c = getopt_long (argc, argv, +#ifdef USE_SHA_CRYPT + "c:ehmR:s:", +#else + "c:ehmR:", +#endif + long_options, NULL)) != -1) { + switch (c) { + case 'c': + crypt_method = optarg; + break; + case 'e': + eflg = true; + break; + case 'h': + usage (E_SUCCESS); + /*@notreached@*/break; + case 'm': + md5flg = true; + break; + case 'R': /* no-op, handled in process_root_flag () */ + break; +#ifdef USE_SHA_CRYPT + case 's': + sflg = true; + if (getlong(optarg, &sha_rounds) == 0) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + usage (E_USAGE); + } + break; +#endif + default: + usage (E_USAGE); + /*@notreached@*/break; + } + } + + /* validate options */ + check_flags (); +} + +/* + * check_flags - check flags and parameters consistency + * + * It will not return if an error is encountered. + */ +static void check_flags (void) +{ +#ifdef USE_SHA_CRYPT + if (sflg && !cflg) { + fprintf (stderr, + _("%s: %s flag is only allowed with the %s flag\n"), + Prog, "-s", "-c"); + usage (E_USAGE); + } +#endif + + if ((eflg && (md5flg || cflg)) || + (md5flg && cflg)) { + fprintf (stderr, + _("%s: the -c, -e, and -m flags are exclusive\n"), + Prog); + usage (E_USAGE); + } + + if (cflg) { + if ( (0 != strcmp (crypt_method, "DES")) + && (0 != strcmp (crypt_method, "MD5")) + && (0 != strcmp (crypt_method, "NONE")) +#ifdef USE_SHA_CRYPT + && (0 != strcmp (crypt_method, "SHA256")) + && (0 != strcmp (crypt_method, "SHA512")) +#endif + ) { + fprintf (stderr, + _("%s: unsupported crypt method: %s\n"), + Prog, crypt_method); + usage (E_USAGE); + } + } +} + +/* + * check_perms - check if the caller is allowed to add a group + * + * With PAM support, the setuid bit can be set on chgpasswd to allow + * non-root users to groups. + * Without PAM support, only users who can write in the group databases + * can add groups. + * + * It will not return if the user is not allowed. + */ +static void check_perms (void) +{ +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM + pam_handle_t *pamh = NULL; + int retval; + struct passwd *pampw; + + pampw = getpwuid (getuid ()); /* local, no need for xgetpwuid */ + if (NULL == pampw) { + fprintf (stderr, + _("%s: Cannot determine your user name.\n"), + Prog); + exit (1); + } + + retval = pam_start ("chgpasswd", pampw->pw_name, &conv, &pamh); + + if (PAM_SUCCESS == retval) { + retval = pam_authenticate (pamh, 0); + } + + if (PAM_SUCCESS == retval) { + retval = pam_acct_mgmt (pamh, 0); + } + + if (PAM_SUCCESS != retval) { + fprintf (stderr, _("%s: PAM: %s\n"), + Prog, pam_strerror (pamh, retval)); + SYSLOG((LOG_ERR, "%s", pam_strerror (pamh, retval))); + if (NULL != pamh) { + (void) pam_end (pamh, retval); + } + exit (1); + } + (void) pam_end (pamh, retval); +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ +} + +/* + * open_files - lock and open the group databases + */ +static void open_files (void) +{ + /* + * Lock the group file and open it for reading and writing. This will + * bring all of the entries into memory where they may be updated. + */ + if (gr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, gr_dbname ()); + fail_exit (1); + } + gr_locked = true; + if (gr_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), Prog, gr_dbname ()); + fail_exit (1); + } + +#ifdef SHADOWGRP + /* Do the same for the shadowed database, if it exist */ + if (is_shadow_grp) { + if (sgr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sgr_dbname ()); + fail_exit (1); + } + sgr_locked = true; + if (sgr_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, _("%s: cannot open %s\n"), + Prog, sgr_dbname ()); + fail_exit (1); + } + } +#endif +} + +/* + * close_files - close and unlock the group databases + */ +static void close_files (void) +{ +#ifdef SHADOWGRP + if (is_shadow_grp) { + if (sgr_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", sgr_dbname ())); + fail_exit (1); + } + if (sgr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sgr_dbname ())); + /* continue */ + } + sgr_locked = false; + } +#endif + + if (gr_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", gr_dbname ())); + fail_exit (1); + } + if (gr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", gr_dbname ())); + /* continue */ + } + gr_locked = false; +} + +int main (int argc, char **argv) +{ + char buf[BUFSIZ]; + char *name; + char *newpwd; + char *cp; + +#ifdef SHADOWGRP + const struct sgrp *sg; + struct sgrp newsg; +#endif + + const struct group *gr; + struct group newgr; + int errors = 0; + int line = 0; + + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + process_flags (argc, argv); + + OPENLOG ("chgpasswd"); + + check_perms (); + +#ifdef SHADOWGRP + is_shadow_grp = sgr_file_present (); +#endif + + open_files (); + + /* + * Read each line, separating the group name from the password. The + * group entry for each group will be looked up in the appropriate + * file (gshadow or group) and the password changed. + */ + while (fgets (buf, (int) sizeof buf, stdin) != (char *) 0) { + line++; + cp = strrchr (buf, '\n'); + if (NULL != cp) { + *cp = '\0'; + } else { + fprintf (stderr, _("%s: line %d: line too long\n"), + Prog, line); + errors++; + continue; + } + + /* + * The group's name is the first field. It is separated from + * the password with a ":" character which is replaced with a + * NUL to give the new password. The new password will then + * be encrypted in the normal fashion with a new salt + * generated, unless the '-e' is given, in which case it is + * assumed to already be encrypted. + */ + + name = buf; + cp = strchr (name, ':'); + if (NULL != cp) { + *cp = '\0'; + cp++; + } else { + fprintf (stderr, + _("%s: line %d: missing new password\n"), + Prog, line); + errors++; + continue; + } + newpwd = cp; + if ( (!eflg) + && ( (NULL == crypt_method) + || (0 != strcmp (crypt_method, "NONE")))) { + void *arg = NULL; + const char *salt; + if (md5flg) { + crypt_method = "MD5"; + } +#ifdef USE_SHA_CRYPT + if (sflg) { + arg = &sha_rounds; + } +#endif + salt = crypt_make_salt (crypt_method, arg); + cp = pw_encrypt (newpwd, salt); + if (NULL == cp) { + fprintf (stderr, + _("%s: failed to crypt password with salt '%s': %s\n"), + Prog, salt, strerror (errno)); + fail_exit (1); + } + } + + /* + * Get the group file entry for this group. The group must + * already exist. + */ + gr = gr_locate (name); + if (NULL == gr) { + fprintf (stderr, + _("%s: line %d: group '%s' does not exist\n"), Prog, + line, name); + errors++; + continue; + } +#ifdef SHADOWGRP + if (is_shadow_grp) { + /* The gshadow entry should be updated if the + * group entry has a password set to 'x'. + * But on the other hand, if there is already both + * a group and a gshadow password, it's preferable + * to update both. + */ + sg = sgr_locate (name); + + if ( (NULL == sg) + && (strcmp (gr->gr_passwd, + SHADOW_PASSWD_STRING) == 0)) { + static char *empty = NULL; + /* If the password is set to 'x' in + * group, but there are no entries in + * gshadow, create one. + */ + newsg.sg_name = name; + /* newsg.sg_passwd = NULL; will be set later */ + newsg.sg_adm = ∅ + newsg.sg_mem = dup_list (gr->gr_mem); + sg = &newsg; + } + } else { + sg = NULL; + } +#endif + + /* + * The freshly encrypted new password is merged into the + * group's entry. + */ +#ifdef SHADOWGRP + if (NULL != sg) { + newsg = *sg; + newsg.sg_passwd = cp; + } + if ( (NULL == sg) + || (strcmp (gr->gr_passwd, SHADOW_PASSWD_STRING) != 0)) +#endif + { + newgr = *gr; + newgr.gr_passwd = cp; + } + + /* + * The updated group file entry is then put back and will + * be written to the group file later, after all the + * other entries have been updated as well. + */ +#ifdef SHADOWGRP + if (NULL != sg) { + if (sgr_update (&newsg) == 0) { + fprintf (stderr, + _("%s: line %d: failed to prepare the new %s entry '%s'\n"), + Prog, line, sgr_dbname (), newsg.sg_name); + errors++; + continue; + } + } + if ( (NULL == sg) + || (strcmp (gr->gr_passwd, SHADOW_PASSWD_STRING) != 0)) +#endif + { + if (gr_update (&newgr) == 0) { + fprintf (stderr, + _("%s: line %d: failed to prepare the new %s entry '%s'\n"), + Prog, line, gr_dbname (), newgr.gr_name); + errors++; + continue; + } + } + } + + /* + * Any detected errors will cause the entire set of changes to be + * aborted. Unlocking the group file will cause all of the + * changes to be ignored. Otherwise the file is closed, causing the + * changes to be written out all at once, and then unlocked + * afterwards. + */ + if (0 != errors) { + fprintf (stderr, + _("%s: error detected, changes ignored\n"), Prog); + fail_exit (1); + } + + close_files (); + + nscd_flush_cache ("group"); + + return (0); +} + diff --git a/src/chpasswd.c b/src/chpasswd.c new file mode 100644 index 0000000..f985672 --- /dev/null +++ b/src/chpasswd.c @@ -0,0 +1,630 @@ +/* + * Copyright (c) 1990 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2000 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2011, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <fcntl.h> +#include <getopt.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#ifdef USE_PAM +#include "pam_defs.h" +#endif /* USE_PAM */ +#include "defines.h" +#include "nscd.h" +#include "getdef.h" +#include "prototypes.h" +#include "pwio.h" +#include "shadowio.h" +/*@-exitarg@*/ +#include "exitcodes.h" + +/* + * Global variables + */ +const char *Prog; +static bool eflg = false; +static bool md5flg = false; +#ifdef USE_SHA_CRYPT +static bool sflg = false; +#endif /* USE_SHA_CRYPT */ + +static /*@null@*//*@observer@*/const char *crypt_method = NULL; +#define cflg (NULL != crypt_method) +#ifdef USE_SHA_CRYPT +static long sha_rounds = 5000; +#endif /* USE_SHA_CRYPT */ + +static bool is_shadow_pwd; +static bool pw_locked = false; +static bool spw_locked = false; + +/* local function prototypes */ +static void fail_exit (int code); +static /*@noreturn@*/void usage (int status); +static void process_flags (int argc, char **argv); +static void check_flags (void); +static void check_perms (void); +static void open_files (void); +static void close_files (void); + +/* + * fail_exit - exit with a failure code after unlocking the files + */ +static void fail_exit (int code) +{ + if (pw_locked) { + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + } + + if (spw_locked) { + if (spw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", spw_dbname ())); + /* continue */ + } + } + + exit (code); +} + +/* + * usage - display usage message and exit + */ +static /*@noreturn@*/void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options]\n" + "\n" + "Options:\n"), + Prog); + (void) fprintf (usageout, + _(" -c, --crypt-method METHOD the crypt method (one of %s)\n"), +#ifndef USE_SHA_CRYPT + "NONE DES MD5" +#else /* USE_SHA_CRYPT */ + "NONE DES MD5 SHA256 SHA512" +#endif /* USE_SHA_CRYPT */ + ); + (void) fputs (_(" -e, --encrypted supplied passwords are encrypted\n"), usageout); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -m, --md5 encrypt the clear text password using\n" + " the MD5 algorithm\n"), + usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); +#ifdef USE_SHA_CRYPT + (void) fputs (_(" -s, --sha-rounds number of SHA rounds for the SHA*\n" + " crypt algorithms\n"), + usageout); +#endif /* USE_SHA_CRYPT */ + (void) fputs ("\n", usageout); + + exit (status); +} + +/* + * process_flags - parse the command line options + * + * It will not return if an error is encountered. + */ +static void process_flags (int argc, char **argv) +{ + int c; + static struct option long_options[] = { + {"crypt-method", required_argument, NULL, 'c'}, + {"encrypted", no_argument, NULL, 'e'}, + {"help", no_argument, NULL, 'h'}, + {"md5", no_argument, NULL, 'm'}, + {"root", required_argument, NULL, 'R'}, +#ifdef USE_SHA_CRYPT + {"sha-rounds", required_argument, NULL, 's'}, +#endif /* USE_SHA_CRYPT */ + {NULL, 0, NULL, '\0'} + }; + + while ((c = getopt_long (argc, argv, +#ifdef USE_SHA_CRYPT + "c:ehmR:s:", +#else /* !USE_SHA_CRYPT */ + "c:ehmR:", +#endif /* !USE_SHA_CRYPT */ + long_options, NULL)) != -1) { + switch (c) { + case 'c': + crypt_method = optarg; + break; + case 'e': + eflg = true; + break; + case 'h': + usage (E_SUCCESS); + /*@notreached@*/break; + case 'm': + md5flg = true; + break; + case 'R': /* no-op, handled in process_root_flag () */ + break; +#ifdef USE_SHA_CRYPT + case 's': + sflg = true; + if (getlong(optarg, &sha_rounds) == 0) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + usage (E_USAGE); + } + break; +#endif /* USE_SHA_CRYPT */ + default: + usage (E_USAGE); + /*@notreached@*/break; + } + } + + /* validate options */ + check_flags (); +} + +/* + * check_flags - check flags and parameters consistency + * + * It will not return if an error is encountered. + */ +static void check_flags (void) +{ +#ifdef USE_SHA_CRYPT + if (sflg && !cflg) { + fprintf (stderr, + _("%s: %s flag is only allowed with the %s flag\n"), + Prog, "-s", "-c"); + usage (E_USAGE); + } +#endif + + if ((eflg && (md5flg || cflg)) || + (md5flg && cflg)) { + fprintf (stderr, + _("%s: the -c, -e, and -m flags are exclusive\n"), + Prog); + usage (E_USAGE); + } + + if (cflg) { + if ( (0 != strcmp (crypt_method, "DES")) + && (0 != strcmp (crypt_method, "MD5")) + && (0 != strcmp (crypt_method, "NONE")) +#ifdef USE_SHA_CRYPT + && (0 != strcmp (crypt_method, "SHA256")) + && (0 != strcmp (crypt_method, "SHA512")) +#endif /* USE_SHA_CRYPT */ + ) { + fprintf (stderr, + _("%s: unsupported crypt method: %s\n"), + Prog, crypt_method); + usage (E_USAGE); + } + } +} + +/* + * check_perms - check if the caller is allowed to add a group + * + * With PAM support, the setuid bit can be set on chpasswd to allow + * non-root users to groups. + * Without PAM support, only users who can write in the group databases + * can add groups. + * + * It will not return if the user is not allowed. + */ +static void check_perms (void) +{ +#ifdef USE_PAM +#ifdef ACCT_TOOLS_SETUID + /* If chpasswd uses PAM and is SUID, check the permissions, + * otherwise, the permissions are enforced by the access to the + * passwd and shadow files. + */ + pam_handle_t *pamh = NULL; + int retval; + struct passwd *pampw; + + pampw = getpwuid (getuid ()); /* local, no need for xgetpwuid */ + if (NULL == pampw) { + fprintf (stderr, + _("%s: Cannot determine your user name.\n"), + Prog); + exit (1); + } + + retval = pam_start ("chpasswd", pampw->pw_name, &conv, &pamh); + + if (PAM_SUCCESS == retval) { + retval = pam_authenticate (pamh, 0); + } + + if (PAM_SUCCESS == retval) { + retval = pam_acct_mgmt (pamh, 0); + } + + if (PAM_SUCCESS != retval) { + fprintf (stderr, _("%s: PAM: %s\n"), + Prog, pam_strerror (pamh, retval)); + SYSLOG((LOG_ERR, "%s", pam_strerror (pamh, retval))); + if (NULL != pamh) { + (void) pam_end (pamh, retval); + } + exit (1); + } + (void) pam_end (pamh, retval); +#endif /* ACCT_TOOLS_SETUID */ +#endif /* USE_PAM */ +} + +/* + * open_files - lock and open the password databases + */ +static void open_files (void) +{ + /* + * Lock the password file and open it for reading and writing. This + * will bring all of the entries into memory where they may be updated. + */ + if (pw_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, pw_dbname ()); + fail_exit (1); + } + pw_locked = true; + if (pw_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), Prog, pw_dbname ()); + fail_exit (1); + } + + /* Do the same for the shadowed database, if it exist */ + if (is_shadow_pwd) { + if (spw_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, spw_dbname ()); + fail_exit (1); + } + spw_locked = true; + if (spw_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, spw_dbname ()); + fail_exit (1); + } + } +} + +/* + * close_files - close and unlock the password databases + */ +static void close_files (void) +{ + if (is_shadow_pwd) { + if (spw_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", spw_dbname ())); + fail_exit (1); + } + if (spw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", spw_dbname ())); + /* continue */ + } + spw_locked = false; + } + + if (pw_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", pw_dbname ())); + fail_exit (1); + } + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + pw_locked = false; +} + +int main (int argc, char **argv) +{ + char buf[BUFSIZ]; + char *name; + char *newpwd; + char *cp; + +#ifdef USE_PAM + bool use_pam = true; +#endif /* USE_PAM */ + + int errors = 0; + int line = 0; + + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + process_flags (argc, argv); + +#ifdef USE_PAM + if (md5flg || eflg || cflg) { + use_pam = false; + } +#endif /* USE_PAM */ + + OPENLOG ("chpasswd"); + + check_perms (); + +#ifdef USE_PAM + if (!use_pam) +#endif /* USE_PAM */ + { + is_shadow_pwd = spw_file_present (); + + open_files (); + } + + /* + * Read each line, separating the user name from the password. The + * password entry for each user will be looked up in the appropriate + * file (shadow or passwd) and the password changed. For shadow + * files the last change date is set directly, for passwd files the + * last change date is set in the age only if aging information is + * present. + */ + while (fgets (buf, (int) sizeof buf, stdin) != (char *) 0) { + line++; + cp = strrchr (buf, '\n'); + if (NULL != cp) { + *cp = '\0'; + } else { + if (feof (stdin) == 0) { + fprintf (stderr, + _("%s: line %d: line too long\n"), + Prog, line); + errors++; + continue; + } + } + + /* + * The username is the first field. It is separated from the + * password with a ":" character which is replaced with a + * NUL to give the new password. The new password will then + * be encrypted in the normal fashion with a new salt + * generated, unless the '-e' is given, in which case it is + * assumed to already be encrypted. + */ + + name = buf; + cp = strchr (name, ':'); + if (NULL != cp) { + *cp = '\0'; + cp++; + } else { + fprintf (stderr, + _("%s: line %d: missing new password\n"), + Prog, line); + errors++; + continue; + } + newpwd = cp; + +#ifdef USE_PAM + if (use_pam){ + if (do_pam_passwd_non_interractive ("chpasswd", name, newpwd) != 0) { + fprintf (stderr, + _("%s: (line %d, user %s) password not changed\n"), + Prog, line, name); + errors++; + } + } else +#endif /* USE_PAM */ + { + const struct spwd *sp; + struct spwd newsp; + const struct passwd *pw; + struct passwd newpw; + + if ( !eflg + && ( (NULL == crypt_method) + || (0 != strcmp (crypt_method, "NONE")))) { + void *arg = NULL; + const char *salt; + if (md5flg) { + crypt_method = "MD5"; + } +#ifdef USE_SHA_CRYPT + if (sflg) { + arg = &sha_rounds; + } +#endif + salt = crypt_make_salt (crypt_method, arg); + cp = pw_encrypt (newpwd, salt); + if (NULL == cp) { + fprintf (stderr, + _("%s: failed to crypt password with salt '%s': %s\n"), + Prog, salt, strerror (errno)); + fail_exit (1); + } + } + + /* + * Get the password file entry for this user. The user must + * already exist. + */ + pw = pw_locate (name); + if (NULL == pw) { + fprintf (stderr, + _("%s: line %d: user '%s' does not exist\n"), Prog, + line, name); + errors++; + continue; + } + if (is_shadow_pwd) { + /* The shadow entry should be updated if the + * passwd entry has a password set to 'x'. + * But on the other hand, if there is already both + * a passwd and a shadow password, it's preferable + * to update both. + */ + sp = spw_locate (name); + + if ( (NULL == sp) + && (strcmp (pw->pw_passwd, + SHADOW_PASSWD_STRING) == 0)) { + /* If the password is set to 'x' in + * passwd, but there are no entries in + * shadow, create one. + */ + newsp.sp_namp = name; + /* newsp.sp_pwdp = NULL; will be set later */ + /* newsp.sp_lstchg= 0; will be set later */ + newsp.sp_min = getdef_num ("PASS_MIN_DAYS", -1); + newsp.sp_max = getdef_num ("PASS_MAX_DAYS", -1); + newsp.sp_warn = getdef_num ("PASS_WARN_AGE", -1); + newsp.sp_inact = -1; + newsp.sp_expire= -1; + newsp.sp_flag = SHADOW_SP_FLAG_UNSET; + sp = &newsp; + } + } else { + sp = NULL; + } + + /* + * The freshly encrypted new password is merged into the + * user's password file entry and the last password change + * date is set to the current date. + */ + if (NULL != sp) { + newsp = *sp; + newsp.sp_pwdp = cp; + newsp.sp_lstchg = (long) gettime () / SCALE; + if (0 == newsp.sp_lstchg) { + /* Better disable aging than requiring a + * password change */ + newsp.sp_lstchg = -1; + } + } + + if ( (NULL == sp) + || (strcmp (pw->pw_passwd, SHADOW_PASSWD_STRING) != 0)) { + newpw = *pw; + newpw.pw_passwd = cp; + } + + /* + * The updated password file entry is then put back and will + * be written to the password file later, after all the + * other entries have been updated as well. + */ + if (NULL != sp) { + if (spw_update (&newsp) == 0) { + fprintf (stderr, + _("%s: line %d: failed to prepare the new %s entry '%s'\n"), + Prog, line, spw_dbname (), newsp.sp_namp); + errors++; + continue; + } + } + if ( (NULL == sp) + || (strcmp (pw->pw_passwd, SHADOW_PASSWD_STRING) != 0)) { + if (pw_update (&newpw) == 0) { + fprintf (stderr, + _("%s: line %d: failed to prepare the new %s entry '%s'\n"), + Prog, line, pw_dbname (), newpw.pw_name); + errors++; + continue; + } + } + } + } + + /* + * Any detected errors will cause the entire set of changes to be + * aborted. Unlocking the password file will cause all of the + * changes to be ignored. Otherwise the file is closed, causing the + * changes to be written out all at once, and then unlocked + * afterwards. + * + * With PAM, it is not possible to delay the update of the + * password database. + */ + if (0 != errors) { +#ifdef USE_PAM + if (!use_pam) +#endif /* USE_PAM */ + { + fprintf (stderr, + _("%s: error detected, changes ignored\n"), + Prog); + } + fail_exit (1); + } + +#ifdef USE_PAM + if (!use_pam) +#endif /* USE_PAM */ + { + /* Save the changes */ + close_files (); + } + + nscd_flush_cache ("passwd"); + + return (0); +} + diff --git a/src/chsh.c b/src/chsh.c new file mode 100644 index 0000000..c89708b --- /dev/null +++ b/src/chsh.c @@ -0,0 +1,564 @@ +/* + * Copyright (c) 1989 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2001 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2011, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <fcntl.h> +#include <getopt.h> +#include <pwd.h> +#include <stdio.h> +#include <sys/types.h> +#ifdef WITH_SELINUX +#include <selinux/selinux.h> +#include <selinux/av_permissions.h> +#endif +#include "defines.h" +#include "getdef.h" +#include "nscd.h" +#include "prototypes.h" +#include "pwauth.h" +#include "pwio.h" +#ifdef USE_PAM +#include "pam_defs.h" +#endif +/*@-exitarg@*/ +#include "exitcodes.h" + +#ifndef SHELLS_FILE +#define SHELLS_FILE "/etc/shells" +#endif +/* + * Global variables + */ +const char *Prog; /* Program name */ +static bool amroot; /* Real UID is root */ +static char loginsh[BUFSIZ]; /* Name of new login shell */ +/* command line options */ +static bool sflg = false; /* -s - set shell from command line */ +static bool pw_locked = false; + +/* external identifiers */ + +/* local function prototypes */ +static /*@noreturn@*/void fail_exit (int code); +static /*@noreturn@*/void usage (int status); +static void new_fields (void); +static bool shell_is_listed (const char *); +static bool is_restricted_shell (const char *); +static void process_flags (int argc, char **argv); +static void check_perms (const struct passwd *pw); +static void update_shell (const char *user, char *loginsh); + +/* + * fail_exit - do some cleanup and exit with the given error code + */ +static /*@noreturn@*/void fail_exit (int code) +{ + if (pw_locked) { + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + } + + closelog (); + + exit (code); +} + +/* + * usage - print command line syntax and exit + */ +static /*@noreturn@*/void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options] [LOGIN]\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs (_(" -s, --shell SHELL new login shell for the user account\n"), usageout); + (void) fputs ("\n", usageout); + exit (status); +} + +/* + * new_fields - change the user's login shell information interactively + * + * prompt the user for the login shell and change it according to the + * response, or leave it alone if nothing was entered. + */ +static void new_fields (void) +{ + puts (_("Enter the new value, or press ENTER for the default")); + change_field (loginsh, sizeof loginsh, _("Login Shell")); +} + +/* + * is_restricted_shell - return true if the shell is restricted + * + */ +static bool is_restricted_shell (const char *sh) +{ + /* + * Shells not listed in /etc/shells are considered to be restricted. + * Changed this to avoid confusion with "rc" (the plan9 shell - not + * restricted despite the name starting with 'r'). --marekm + */ + return !shell_is_listed (sh); +} + +/* + * shell_is_listed - see if the user's login shell is listed in /etc/shells + * + * The /etc/shells file is read for valid names of login shells. If the + * /etc/shells file does not exist the user cannot set any shell unless + * they are root. + * + * If getusershell() is available (Linux, *BSD, possibly others), use it + * instead of re-implementing it. + */ +static bool shell_is_listed (const char *sh) +{ + char *cp; + bool found = false; + +#ifndef HAVE_GETUSERSHELL + char buf[BUFSIZ]; + FILE *fp; +#endif + +#ifdef HAVE_GETUSERSHELL + setusershell (); + while ((cp = getusershell ())) { + if (strcmp (cp, sh) == 0) { + found = true; + break; + } + } + endusershell (); +#else + fp = fopen (SHELLS_FILE, "r"); + if (NULL == fp) { + return false; + } + + while (fgets (buf, sizeof (buf), fp) == buf) { + cp = strrchr (buf, '\n'); + if (NULL != cp) { + *cp = '\0'; + } + + if (buf[0] == '#') { + continue; + } + + if (strcmp (buf, sh) == 0) { + found = true; + break; + } + } + fclose (fp); +#endif + return found; +} + +/* + * process_flags - parse the command line options + * + * It will not return if an error is encountered. + */ +static void process_flags (int argc, char **argv) +{ + int c; + static struct option long_options[] = { + {"help", no_argument, NULL, 'h'}, + {"root", required_argument, NULL, 'R'}, + {"shell", required_argument, NULL, 's'}, + {NULL, 0, NULL, '\0'} + }; + + while ((c = getopt_long (argc, argv, "hR:s:", + long_options, NULL)) != -1) { + switch (c) { + case 'h': + usage (E_SUCCESS); + /*@notreached@*/break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + case 's': + sflg = true; + STRFCPY (loginsh, optarg); + break; + default: + usage (E_USAGE); + } + } + + /* + * There should be only one remaining argument at most and it should + * be the user's name. + */ + if (argc > (optind + 1)) { + usage (E_USAGE); + } +} + +/* + * check_perms - check if the caller is allowed to add a group + * + * Non-root users are only allowed to change their shell, if their current + * shell is not a restricted shell. + * + * Non-root users must be authenticated. + * + * It will not return if the user is not allowed. + */ +static void check_perms (const struct passwd *pw) +{ +#ifdef USE_PAM + pam_handle_t *pamh = NULL; + int retval; + struct passwd *pampw; +#endif + + /* + * Non-privileged users are only allowed to change the shell if the + * UID of the user matches the current real UID. + */ + if (!amroot && pw->pw_uid != getuid ()) { + SYSLOG ((LOG_WARN, "can't change shell for '%s'", pw->pw_name)); + fprintf (stderr, + _("You may not change the shell for '%s'.\n"), + pw->pw_name); + fail_exit (1); + } + + /* + * Non-privileged users are only allowed to change the shell if it + * is not a restricted one. + */ + if (!amroot && is_restricted_shell (pw->pw_shell)) { + SYSLOG ((LOG_WARN, "can't change shell for '%s'", pw->pw_name)); + fprintf (stderr, + _("You may not change the shell for '%s'.\n"), + pw->pw_name); + fail_exit (1); + } +#ifdef WITH_SELINUX + /* + * If the UID of the user does not match the current real UID, + * check if the change is allowed by SELinux policy. + */ + if ((pw->pw_uid != getuid ()) + && (is_selinux_enabled () > 0) + && (selinux_check_passwd_access (PASSWD__CHSH) != 0)) { + SYSLOG ((LOG_WARN, "can't change shell for '%s'", pw->pw_name)); + fprintf (stderr, + _("You may not change the shell for '%s'.\n"), + pw->pw_name); + fail_exit (1); + } +#endif + +#ifndef USE_PAM + /* + * Non-privileged users are optionally authenticated (must enter + * the password of the user whose information is being changed) + * before any changes can be made. Idea from util-linux + * chfn/chsh. --marekm + */ + if (!amroot && getdef_bool ("CHSH_AUTH")) { + passwd_check (pw->pw_name, pw->pw_passwd, "chsh"); + } + +#else /* !USE_PAM */ + pampw = getpwuid (getuid ()); /* local, no need for xgetpwuid */ + if (NULL == pampw) { + fprintf (stderr, + _("%s: Cannot determine your user name.\n"), + Prog); + exit (E_NOPERM); + } + + retval = pam_start ("chsh", pampw->pw_name, &conv, &pamh); + + if (PAM_SUCCESS == retval) { + retval = pam_authenticate (pamh, 0); + } + + if (PAM_SUCCESS == retval) { + retval = pam_acct_mgmt (pamh, 0); + } + + if (PAM_SUCCESS != retval) { + fprintf (stderr, _("%s: PAM: %s\n"), + Prog, pam_strerror (pamh, retval)); + SYSLOG((LOG_ERR, "%s", pam_strerror (pamh, retval))); + if (NULL != pamh) { + (void) pam_end (pamh, retval); + } + exit (E_NOPERM); + } + (void) pam_end (pamh, retval); +#endif /* USE_PAM */ +} + +/* + * update_shell - update the user's shell in the passwd database + * + * Commit the user's entry after changing her shell field. + * + * It will not return in case of error. + */ +static void update_shell (const char *user, char *newshell) +{ + const struct passwd *pw; /* Password entry from /etc/passwd */ + struct passwd pwent; /* New password entry */ + + /* + * Before going any further, raise the ulimit to prevent + * colliding into a lowered ulimit, and set the real UID + * to root to protect against unexpected signals. Any + * keyboard signals are set to be ignored. + */ + if (setuid (0) != 0) { + SYSLOG ((LOG_ERR, "can't setuid(0)")); + fputs (_("Cannot change ID to root.\n"), stderr); + fail_exit (1); + } + pwd_init (); + + /* + * The passwd entry is now ready to be committed back to + * the password file. Get a lock on the file and open it. + */ + if (pw_lock () == 0) { + fprintf (stderr, _("%s: cannot lock %s; try again later.\n"), + Prog, pw_dbname ()); + fail_exit (1); + } + pw_locked = true; + if (pw_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, _("%s: cannot open %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_WARN, "cannot open %s", pw_dbname ())); + fail_exit (1); + } + + /* + * Get the entry to update using pw_locate() - we want the real + * one from /etc/passwd, not the one from getpwnam() which could + * contain the shadow password if (despite the warnings) someone + * enables AUTOSHADOW (or SHADOW_COMPAT in libc). --marekm + */ + pw = pw_locate (user); + if (NULL == pw) { + fprintf (stderr, + _("%s: user '%s' does not exist in %s\n"), + Prog, user, pw_dbname ()); + fail_exit (1); + } + + /* + * Make a copy of the entry, then change the shell field. The other + * fields remain unchanged. + */ + pwent = *pw; + pwent.pw_shell = newshell; + + /* + * Update the passwd file entry. If there is a DBM file, update + * that entry as well. + */ + if (pw_update (&pwent) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, pw_dbname (), pwent.pw_name); + fail_exit (1); + } + + /* + * Changes have all been made, so commit them and unlock the file. + */ + if (pw_close () == 0) { + fprintf (stderr, _("%s: failure while writing changes to %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", pw_dbname ())); + fail_exit (1); + } + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + pw_locked= false; +} + +/* + * chsh - this command controls changes to the user's shell + * + * The only supported option is -s which permits the the login shell to + * be set from the command line. + */ +int main (int argc, char **argv) +{ + char *user; /* User name */ + const struct passwd *pw; /* Password entry from /etc/passwd */ + + sanitize_env (); + + /* + * Get the program name. The program name is used as a prefix to + * most error messages. + */ + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + /* + * This command behaves different for root and non-root users. + */ + amroot = (getuid () == 0); + + OPENLOG ("chsh"); + + /* parse the command line options */ + process_flags (argc, argv); + + /* + * Get the name of the user to check. It is either the command line + * name, or the name getlogin() returns. + */ + if (optind < argc) { + user = argv[optind]; + pw = xgetpwnam (user); + if (NULL == pw) { + fprintf (stderr, + _("%s: user '%s' does not exist\n"), Prog, user); + fail_exit (1); + } + } else { + pw = get_my_pwent (); + if (NULL == pw) { + fprintf (stderr, + _("%s: Cannot determine your user name.\n"), + Prog); + SYSLOG ((LOG_WARN, "Cannot determine the user name of the caller (UID %lu)", + (unsigned long) getuid ())); + fail_exit (1); + } + user = xstrdup (pw->pw_name); + } + +#ifdef USE_NIS + /* + * Now we make sure this is a LOCAL password entry for this user ... + */ + if (__ispwNIS ()) { + char *nis_domain; + char *nis_master; + + fprintf (stderr, + _("%s: cannot change user '%s' on NIS client.\n"), + Prog, user); + + if (!yp_get_default_domain (&nis_domain) && + !yp_master (nis_domain, "passwd.byname", &nis_master)) { + fprintf (stderr, + _("%s: '%s' is the NIS master for this client.\n"), + Prog, nis_master); + } + fail_exit (1); + } +#endif + + check_perms (pw); + + /* + * Now get the login shell. Either get it from the password + * file, or use the value from the command line. + */ + if (!sflg) { + STRFCPY (loginsh, pw->pw_shell); + } + + /* + * If the login shell was not set on the command line, let the user + * interactively change it. + */ + if (!sflg) { + printf (_("Changing the login shell for %s\n"), user); + new_fields (); + } + + /* + * Check all of the fields for valid information. The shell + * field may not contain any illegal characters. Non-privileged + * users are restricted to using the shells in /etc/shells. + * The shell must be executable by the user. + */ + if (valid_field (loginsh, ":,=\n") != 0) { + fprintf (stderr, _("%s: Invalid entry: %s\n"), Prog, loginsh); + fail_exit (1); + } + if ( !amroot + && ( is_restricted_shell (loginsh) + || (access (loginsh, X_OK) != 0))) { + fprintf (stderr, _("%s: %s is an invalid shell\n"), Prog, loginsh); + fail_exit (1); + } + + /* Even for root, warn if an invalid shell is specified. */ + if (access (loginsh, F_OK) != 0) { + fprintf (stderr, _("%s: Warning: %s does not exist\n"), Prog, loginsh); + } else if (access (loginsh, X_OK) != 0) { + fprintf (stderr, _("%s: Warning: %s is not executable\n"), Prog, loginsh); + } + + update_shell (user, loginsh); + + SYSLOG ((LOG_INFO, "changed user '%s' shell to '%s'", user, loginsh)); + + nscd_flush_cache ("passwd"); + + closelog (); + exit (E_SUCCESS); +} + diff --git a/src/expiry.c b/src/expiry.c new file mode 100644 index 0000000..41add94 --- /dev/null +++ b/src/expiry.c @@ -0,0 +1,210 @@ +/* + * Copyright (c) 1994 , Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2001 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2011, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <sys/types.h> +#include <getopt.h> +#include "defines.h" +#include "prototypes.h" +/*@-exitarg@*/ +#include "exitcodes.h" + +/* Global variables */ +const char *Prog; +static bool cflg = false; + +/* local function prototypes */ +static RETSIGTYPE catch_signals (unused int sig); +static /*@noreturn@*/void usage (int status); +static void process_flags (int argc, char **argv); + +/* + * catch_signals - signal catcher + */ +static RETSIGTYPE catch_signals (unused int sig) +{ + _exit (10); +} + +/* + * usage - print syntax message and exit + */ +static /*@noreturn@*/void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options]\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -c, --check check the user's password expiration\n"), usageout); + (void) fputs (_(" -f, --force force password change if the user's password\n" + " is expired\n"), usageout); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs ("\n", usageout); + exit (status); +} + +/* + * process_flags - parse the command line options + * + * It will not return if an error is encountered. + */ +static void process_flags (int argc, char **argv) +{ + bool fflg = false; + int c; + static struct option long_options[] = { + {"check", no_argument, NULL, 'c'}, + {"force", no_argument, NULL, 'f'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, '\0'} + }; + + while ((c = getopt_long (argc, argv, "cfh", + long_options, NULL)) != -1) { + switch (c) { + case 'c': + cflg = true; + break; + case 'f': + fflg = true; + break; + case 'h': + usage (E_SUCCESS); + /*@notreached@*/break; + default: + usage (E_USAGE); + } + } + + if (! (cflg || fflg)) { + usage (E_USAGE); + } + + if (cflg && fflg) { + fprintf (stderr, + _("%s: options %s and %s conflict\n"), + Prog, "-c", "-f"); + usage (E_USAGE); + } + + if (argc != optind) { + fprintf (stderr, + _("%s: unexpected argument: %s\n"), + Prog, argv[optind]); + usage (E_USAGE); + } +} + +/* + * expiry - check and enforce password expiration policy + * + * expiry checks (-c) the current password expiration and forces (-f) + * changes when required. It is callable as a normal user command. + */ +int main (int argc, char **argv) +{ + struct passwd *pwd; + struct spwd *spwd; + + Prog = Basename (argv[0]); + + sanitize_env (); + + /* + * Start by disabling all of the keyboard signals. + */ + (void) signal (SIGHUP, catch_signals); + (void) signal (SIGINT, catch_signals); + (void) signal (SIGQUIT, catch_signals); +#ifdef SIGTSTP + (void) signal (SIGTSTP, catch_signals); +#endif + + /* + * expiry takes one of two arguments. The default action is to give + * the usage message. + */ + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + OPENLOG ("expiry"); + + process_flags (argc, argv); + + /* + * Get user entries for /etc/passwd and /etc/shadow + */ + pwd = get_my_pwent (); + if (NULL == pwd) { + fprintf (stderr, _("%s: Cannot determine your user name.\n"), + Prog); + SYSLOG ((LOG_WARN, "Cannot determine the user name of the caller (UID %lu)", + (unsigned long) getuid ())); + exit (10); + } + spwd = getspnam (pwd->pw_name); /* !USE_PAM, No need for xgetspnam */ + + /* + * If checking accounts, use agecheck() function. + */ + if (cflg) { + /* + * Print out number of days until expiration. + */ + agecheck (spwd); + + /* + * Exit with status indicating state of account. + */ + exit (isexpired (pwd, spwd)); + } + + /* + * Otherwise, force a password change with the expire() function. + * It will force the change or give a message indicating what to + * do. + * It won't return unless the account is unexpired. + */ + (void) expire (pwd, spwd); + + return E_SUCCESS; +} + diff --git a/src/faillog.c b/src/faillog.c new file mode 100644 index 0000000..073561c --- /dev/null +++ b/src/faillog.c @@ -0,0 +1,741 @@ +/* + * Copyright (c) 1989 - 1993, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2002 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2011, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <getopt.h> +#include <pwd.h> +#include <stdio.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <time.h> +#include <assert.h> +#include "defines.h" +#include "faillog.h" +#include "prototypes.h" +/*@-exitarg@*/ +#include "exitcodes.h" + +/* local function prototypes */ +static /*@noreturn@*/void usage (int status); +static void print_one (/*@null@*/const struct passwd *pw, bool force); +static void set_locktime (long locktime); +static bool set_locktime_one (uid_t uid, long locktime); +static void setmax (short max); +static bool setmax_one (uid_t uid, short max); +static void print (void); +static bool reset_one (uid_t uid); +static void reset (void); + +/* + * Global variables + */ +const char *Prog; /* Program name */ +static FILE *fail; /* failure file stream */ +static time_t seconds; /* that number of days in seconds */ +static unsigned long umin; /* if uflg and has_umin, only display users with uid >= umin */ +static bool has_umin = false; +static unsigned long umax; /* if uflg and has_umax, only display users with uid <= umax */ +static bool has_umax = false; +static bool errors = false; + +static bool aflg = false; /* set if all users are to be printed always */ +static bool uflg = false; /* set if user is a valid user id */ +static bool tflg = false; /* print is restricted to most recent days */ +static bool lflg = false; /* set the locktime */ +static bool mflg = false; /* set maximum failed login counters */ +static bool rflg = false; /* reset the counters of login failures */ + +static struct stat statbuf; /* fstat buffer for file size */ + +#define NOW (time((time_t *) 0)) + +static /*@noreturn@*/void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options]\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -a, --all display faillog records for all users\n"), usageout); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -l, --lock-secs SEC after failed login lock account for SEC seconds\n"), usageout); + (void) fputs (_(" -m, --maximum MAX set maximum failed login counters to MAX\n"), usageout); + (void) fputs (_(" -r, --reset reset the counters of login failures\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs (_(" -t, --time DAYS display faillog records more recent than DAYS\n"), usageout); + (void) fputs (_(" -u, --user LOGIN/RANGE display faillog record or maintains failure\n" + " counters and limits (if used with -r, -m,\n" + " or -l) only for the specified LOGIN(s)\n"), usageout); + (void) fputs ("\n", usageout); + exit (status); +} + +static void print_one (/*@null@*/const struct passwd *pw, bool force) +{ + static bool once = false; + struct tm *tm; + off_t offset; + struct faillog fl; + time_t now; + +#ifdef HAVE_STRFTIME + char *cp; + char ptime[80]; +#endif + + if (NULL == pw) { + return; + } + + offset = (off_t) pw->pw_uid * sizeof (fl); + if (offset + sizeof (fl) <= statbuf.st_size) { + /* fseeko errors are not really relevant for us. */ + int err = fseeko (fail, offset, SEEK_SET); + assert (0 == err); + /* faillog is a sparse file. Even if no entries were + * entered for this user, which should be able to get the + * empty entry in this case. + */ + if (fread ((char *) &fl, sizeof (fl), 1, fail) != 1) { + fprintf (stderr, + _("%s: Failed to get the entry for UID %lu\n"), + Prog, (unsigned long int)pw->pw_uid); + return; + } + } else { + /* Outsize of the faillog file. + * Behave as if there were a missing entry (same behavior + * as if we were reading an non existing entry in the + * sparse faillog file). + */ + memzero (&fl, sizeof (fl)); + } + + /* Nothing to report */ + if (!force && (0 == fl.fail_time)) { + return; + } + + (void) time(&now); + + /* Filter out entries that do not match with the -t option */ + if (tflg && ((now - fl.fail_time) > seconds)) { + return; + } + + /* Print the header only once */ + if (!once) { + puts (_("Login Failures Maximum Latest On\n")); + once = true; + } + + tm = localtime (&fl.fail_time); +#ifdef HAVE_STRFTIME + strftime (ptime, sizeof (ptime), "%D %H:%M:%S %z", tm); + cp = ptime; +#endif + printf ("%-9s %5d %5d ", + pw->pw_name, fl.fail_cnt, fl.fail_max); + /* FIXME: cp is not defined ifndef HAVE_STRFTIME */ + printf ("%s %s", cp, fl.fail_line); + if (0 != fl.fail_locktime) { + if ( ((fl.fail_time + fl.fail_locktime) > now) + && (0 != fl.fail_cnt)) { + printf (_(" [%lus left]"), + (unsigned long) fl.fail_time + fl.fail_locktime - now); + } else { + printf (_(" [%lds lock]"), + fl.fail_locktime); + } + } + putchar ('\n'); +} + +static void print (void) +{ + if (uflg && has_umin && has_umax && (umin==umax)) { + print_one (getpwuid ((uid_t)umin), true); + } else { + /* We only print records for existing users. + * Loop based on the user database instead of reading the + * whole file. We will have to query the database anyway + * so except for very small ranges and large user + * database, this should not be a performance issue. + */ + struct passwd *pwent; + + setpwent (); + while ( (pwent = getpwent ()) != NULL ) { + if ( uflg + && ( (has_umin && (pwent->pw_uid < (uid_t)umin)) + || (has_umax && (pwent->pw_uid > (uid_t)umax)))) { + continue; + } + print_one (pwent, aflg); + } + endpwent (); + } +} + +/* + * reset_one - Reset the fail count for one user + * + * This returns a boolean indicating if an error occurred. + */ +static bool reset_one (uid_t uid) +{ + off_t offset; + struct faillog fl; + + offset = (off_t) uid * sizeof (fl); + if (offset + sizeof (fl) <= statbuf.st_size) { + /* fseeko errors are not really relevant for us. */ + int err = fseeko (fail, offset, SEEK_SET); + assert (0 == err); + /* faillog is a sparse file. Even if no entries were + * entered for this user, which should be able to get the + * empty entry in this case. + */ + if (fread ((char *) &fl, sizeof (fl), 1, fail) != 1) { + fprintf (stderr, + _("%s: Failed to get the entry for UID %lu\n"), + Prog, (unsigned long int)uid); + return true; + } + } else { + /* Outsize of the faillog file. + * Behave as if there were a missing entry (same behavior + * as if we were reading an non existing entry in the + * sparse faillog file). + */ + memzero (&fl, sizeof (fl)); + } + + if (0 == fl.fail_cnt) { + /* If the count is already null, do not write in the file. + * This avoids writing 0 when no entries were present for + * the user. + */ + return false; + } + + fl.fail_cnt = 0; + + if ( (fseeko (fail, offset, SEEK_SET) == 0) + && (fwrite ((char *) &fl, sizeof (fl), 1, fail) == 1)) { + (void) fflush (fail); + return false; + } + + fprintf (stderr, + _("%s: Failed to reset fail count for UID %lu\n"), + Prog, (unsigned long int)uid); + return true; +} + +static void reset (void) +{ + if (uflg && has_umin && has_umax && (umin==umax)) { + if (reset_one ((uid_t)umin)) { + errors = true; + } + } else { + /* There is no need to reset outside of the faillog + * database. + */ + uid_t uidmax = statbuf.st_size / sizeof (struct faillog); + if (uidmax > 1) { + uidmax--; + } + if (has_umax && (uid_t)umax < uidmax) { + uidmax = (uid_t)umax; + } + + /* Reset all entries in the specified range. + * Non existing entries will not be touched. + */ + if (aflg) { + /* Entries for non existing users are also reset. + */ + uid_t uid = 0; + + /* Make sure we stay in the umin-umax range if specified */ + if (has_umin) { + uid = (uid_t)umin; + } + + while (uid <= uidmax) { + if (reset_one (uid)) { + errors = true; + } + uid++; + } + } else { + /* Only reset records for existing users. + */ + struct passwd *pwent; + + setpwent (); + while ( (pwent = getpwent ()) != NULL ) { + if ( uflg + && ( (has_umin && (pwent->pw_uid < (uid_t)umin)) + || (pwent->pw_uid > (uid_t)uidmax))) { + continue; + } + if (reset_one (pwent->pw_uid)) { + errors = true; + } + } + endpwent (); + } + } +} + +/* + * setmax_one - Set the maximum failed login counter for one user + * + * This returns a boolean indicating if an error occurred. + */ +static bool setmax_one (uid_t uid, short max) +{ + off_t offset; + struct faillog fl; + + offset = (off_t) uid * sizeof (fl); + if (offset + sizeof (fl) <= statbuf.st_size) { + /* fseeko errors are not really relevant for us. */ + int err = fseeko (fail, offset, SEEK_SET); + assert (0 == err); + /* faillog is a sparse file. Even if no entries were + * entered for this user, which should be able to get the + * empty entry in this case. + */ + if (fread ((char *) &fl, sizeof (fl), 1, fail) != 1) { + fprintf (stderr, + _("%s: Failed to get the entry for UID %lu\n"), + Prog, (unsigned long int)uid); + return true; + } + } else { + /* Outsize of the faillog file. + * Behave as if there were a missing entry (same behavior + * as if we were reading an non existing entry in the + * sparse faillog file). + */ + memzero (&fl, sizeof (fl)); + } + + if (max == fl.fail_max) { + /* If the max is already set to the right value, do not + * write in the file. + * This avoids writing 0 when no entries were present for + * the user and the max argument is 0. + */ + return false; + } + + fl.fail_max = max; + + if ( (fseeko (fail, offset, SEEK_SET) == 0) + && (fwrite ((char *) &fl, sizeof (fl), 1, fail) == 1)) { + (void) fflush (fail); + return false; + } + + fprintf (stderr, + _("%s: Failed to set max for UID %lu\n"), + Prog, (unsigned long int)uid); + return true; +} + +static void setmax (short max) +{ + if (uflg && has_umin && has_umax && (umin==umax)) { + if (setmax_one ((uid_t)umin, max)) { + errors = true; + } + } else { + /* Set max for entries in the specified range. + * If max is unchanged for an entry, the entry is not touched. + * If max is null, and no entries exist for this user, no + * entries will be created. + */ + if (aflg) { + /* Entries for non existing user are also taken into + * account (in order to define policy for future users). + */ + uid_t uid = 0; + /* The default umax value is based on the size of the + * faillog database. + */ + uid_t uidmax = statbuf.st_size / sizeof (struct faillog); + if (uidmax > 1) { + uidmax--; + } + + /* Make sure we stay in the umin-umax range if specified */ + if (has_umin) { + uid = (uid_t)umin; + } + if (has_umax) { + uidmax = (uid_t)umax; + } + + while (uid <= uidmax) { + if (setmax_one (uid, max)) { + errors = true; + } + uid++; + } + } else { + /* Only change records for existing users. + */ + struct passwd *pwent; + + setpwent (); + while ( (pwent = getpwent ()) != NULL ) { + if ( uflg + && ( (has_umin && (pwent->pw_uid < (uid_t)umin)) + || (has_umax && (pwent->pw_uid > (uid_t)umax)))) { + continue; + } + if (setmax_one (pwent->pw_uid, max)) { + errors = true; + } + } + endpwent (); + } + } +} + +/* + * set_locktime_one - Set the locktime for one user + * + * This returns a boolean indicating if an error occurred. + */ +static bool set_locktime_one (uid_t uid, long locktime) +{ + off_t offset; + struct faillog fl; + + offset = (off_t) uid * sizeof (fl); + if (offset + sizeof (fl) <= statbuf.st_size) { + /* fseeko errors are not really relevant for us. */ + int err = fseeko (fail, offset, SEEK_SET); + assert (0 == err); + /* faillog is a sparse file. Even if no entries were + * entered for this user, which should be able to get the + * empty entry in this case. + */ + if (fread ((char *) &fl, sizeof (fl), 1, fail) != 1) { + fprintf (stderr, + _("%s: Failed to get the entry for UID %lu\n"), + Prog, (unsigned long int)uid); + return true; + } + } else { + /* Outsize of the faillog file. + * Behave as if there were a missing entry (same behavior + * as if we were reading an non existing entry in the + * sparse faillog file). + */ + memzero (&fl, sizeof (fl)); + } + + if (locktime == fl.fail_locktime) { + /* If the locktime is already set to the right value, do not + * write in the file. + * This avoids writing 0 when no entries were present for + * the user and the locktime argument is 0. + */ + return false; + } + + fl.fail_locktime = locktime; + + if ( (fseeko (fail, offset, SEEK_SET) == 0) + && (fwrite ((char *) &fl, sizeof (fl), 1, fail) == 1)) { + (void) fflush (fail); + return false; + } + + fprintf (stderr, + _("%s: Failed to set locktime for UID %lu\n"), + Prog, (unsigned long int)uid); + return true; +} + +static void set_locktime (long locktime) +{ + if (uflg && has_umin && has_umax && (umin==umax)) { + if (set_locktime_one ((uid_t)umin, locktime)) { + errors = true; + } + } else { + /* Set locktime for entries in the specified range. + * If locktime is unchanged for an entry, the entry is not touched. + * If locktime is null, and no entries exist for this user, no + * entries will be created. + */ + if (aflg) { + /* Entries for non existing user are also taken into + * account (in order to define policy for future users). + */ + uid_t uid = 0; + /* The default umax value is based on the size of the + * faillog database. + */ + uid_t uidmax = statbuf.st_size / sizeof (struct faillog); + if (uidmax > 1) { + uidmax--; + } + + /* Make sure we stay in the umin-umax range if specified */ + if (has_umin) { + uid = (uid_t)umin; + } + if (has_umax) { + uidmax = (uid_t)umax; + } + + while (uid <= uidmax) { + if (set_locktime_one (uid, locktime)) { + errors = true; + } + uid++; + } + } else { + /* Only change records for existing users. + */ + struct passwd *pwent; + + setpwent (); + while ( (pwent = getpwent ()) != NULL ) { + if ( uflg + && ( (has_umin && (pwent->pw_uid < (uid_t)umin)) + || (has_umax && (pwent->pw_uid > (uid_t)umax)))) { + continue; + } + if (set_locktime_one (pwent->pw_uid, locktime)) { + errors = true; + } + } + endpwent (); + } + } +} + +int main (int argc, char **argv) +{ + long fail_locktime; + short fail_max; + long days; + + /* + * Get the program name. The program name is used as a prefix to + * most error messages. + */ + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + { + int c; + static struct option long_options[] = { + {"all", no_argument, NULL, 'a'}, + {"help", no_argument, NULL, 'h'}, + {"lock-secs", required_argument, NULL, 'l'}, + {"maximum", required_argument, NULL, 'm'}, + {"reset", no_argument, NULL, 'r'}, + {"root", required_argument, NULL, 'R'}, + {"time", required_argument, NULL, 't'}, + {"user", required_argument, NULL, 'u'}, + {NULL, 0, NULL, '\0'} + }; + while ((c = getopt_long (argc, argv, "ahl:m:rR:t:u:", + long_options, NULL)) != -1) { + switch (c) { + case 'a': + aflg = true; + break; + case 'h': + usage (E_SUCCESS); + /*@notreached@*/break; + case 'l': + if (getlong (optarg, &fail_locktime) == 0) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + lflg = true; + break; + case 'm': + { + long int lmax; + if ( (getlong (optarg, &lmax) == 0) + || ((long int)(short) lmax != lmax)) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + fail_max = (short) lmax; + mflg = true; + break; + } + case 'r': + rflg = true; + break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + case 't': + if (getlong (optarg, &days) == 0) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + seconds = (time_t) days * DAY; + tflg = true; + break; + case 'u': + { + /* + * The user can be: + * - a login name + * - numerical + * - a numerical login ID + * - a range (-x, x-, x-y) + */ + struct passwd *pwent; + + uflg = true; + /* local, no need for xgetpwnam */ + pwent = getpwnam (optarg); + if (NULL != pwent) { + umin = (unsigned long) pwent->pw_uid; + has_umin = true; + umax = umin; + has_umax = true; + } else { + if (getrange (optarg, + &umin, &has_umin, + &umax, &has_umax) == 0) { + fprintf (stderr, + _("%s: Unknown user or range: %s\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + } + + break; + } + default: + usage (E_USAGE); + } + } + if (argc > optind) { + fprintf (stderr, + _("%s: unexpected argument: %s\n"), + Prog, argv[optind]); + usage (EXIT_FAILURE); + } + } + + if (tflg && (lflg || mflg || rflg)) { + usage (E_USAGE); + } + + /* Open the faillog database */ + if (lflg || mflg || rflg) { + fail = fopen (FAILLOG_FILE, "r+"); + } else { + fail = fopen (FAILLOG_FILE, "r"); + } + if (NULL == fail) { + fprintf (stderr, + _("%s: Cannot open %s: %s\n"), + Prog, FAILLOG_FILE, strerror (errno)); + exit (E_NOPERM); + } + + /* Get the size of the faillog */ + if (fstat (fileno (fail), &statbuf) != 0) { + fprintf (stderr, + _("%s: Cannot get the size of %s: %s\n"), + Prog, FAILLOG_FILE, strerror (errno)); + exit (E_NOPERM); + } + + if (lflg) { + set_locktime (fail_locktime); + } + + if (mflg) { + setmax (fail_max); + } + + if (rflg) { + reset (); + } + + if (!(lflg || mflg || rflg)) { + print (); + } + + if (lflg || mflg || rflg) { + if ( (ferror (fail) != 0) + || (fflush (fail) != 0) + || (fsync (fileno (fail)) != 0) + || (fclose (fail) != 0)) { + fprintf (stderr, + _("%s: Failed to write %s: %s\n"), + Prog, FAILLOG_FILE, strerror (errno)); + (void) fclose (fail); + errors = true; + } + } else { + (void) fclose (fail); + } + + exit (errors ? E_NOPERM : E_SUCCESS); +} + diff --git a/src/gpasswd.c b/src/gpasswd.c new file mode 100644 index 0000000..c4a492b --- /dev/null +++ b/src/gpasswd.c @@ -0,0 +1,1207 @@ +/* + * Copyright (c) 1990 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2001 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2011, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <grp.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <sys/types.h> +#include "defines.h" +#include "groupio.h" +#include "nscd.h" +#include "prototypes.h" +#ifdef SHADOWGRP +#include "sgroupio.h" +#endif +/*@-exitarg@*/ +#include "exitcodes.h" + +/* + * Global variables + */ +/* The name of this command, as it is invoked */ +const char *Prog; + +#ifdef SHADOWGRP +/* Indicate if shadow groups are enabled on the system + * (/etc/gshadow present) */ +static bool is_shadowgrp; +#endif + +/* Flags set by options */ +static bool aflg = false; +static bool Aflg = false; +static bool dflg = false; +static bool Mflg = false; +static bool rflg = false; +static bool Rflg = false; +/* The name of the group that is being affected */ +static char *group = NULL; +/* The name of the user being added (-a) or removed (-d) from group */ +static char *user = NULL; +/* The new list of members set with -M */ +static char *members = NULL; +#ifdef SHADOWGRP +/* The new list of group administrators set with -A */ +static char *admins = NULL; +#endif +/* The name of the caller */ +static char *myname = NULL; +/* The UID of the caller */ +static uid_t bywho; +/* Indicate if gpasswd was called by root */ +#define amroot (0 == bywho) + +/* The number of retries for th user to provide and repeat a new password */ +#ifndef RETRIES +#define RETRIES 3 +#endif + +/* local function prototypes */ +static void usage (int status); +static RETSIGTYPE catch_signals (int killed); +static bool is_valid_user_list (const char *users); +static void process_flags (int argc, char **argv); +static void check_flags (int argc, int opt_index); +static void open_files (void); +static void close_files (void); +#ifdef SHADOWGRP +static void get_group (struct group *gr, struct sgrp *sg); +static void check_perms (const struct group *gr, const struct sgrp *sg); +static void update_group (struct group *gr, struct sgrp *sg); +static void change_passwd (struct group *gr, struct sgrp *sg); +#else +static void get_group (struct group *gr); +static void check_perms (const struct group *gr); +static void update_group (struct group *gr); +static void change_passwd (struct group *gr); +#endif +static void log_gpasswd_failure (const char *suffix); +static void log_gpasswd_failure_system (/*@null@*/unused void *arg); +static void log_gpasswd_failure_group (/*@null@*/unused void *arg); +#ifdef SHADOWGRP +static void log_gpasswd_failure_gshadow (/*@null@*/unused void *arg); +#endif +static void log_gpasswd_success (const char *suffix); +static void log_gpasswd_success_system (/*@null@*/unused void *arg); +static void log_gpasswd_success_group (/*@null@*/unused void *arg); + +/* + * usage - display usage message + */ +static void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [option] GROUP\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -a, --add USER add USER to GROUP\n"), usageout); + (void) fputs (_(" -d, --delete USER remove USER from GROUP\n"), usageout); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -Q, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs (_(" -r, --remove-password remove the GROUP's password\n"), usageout); + (void) fputs (_(" -R, --restrict restrict access to GROUP to its members\n"), usageout); + (void) fputs (_(" -M, --members USER,... set the list of members of GROUP\n"), usageout); +#ifdef SHADOWGRP + (void) fputs (_(" -A, --administrators ADMIN,...\n" + " set the list of administrators for GROUP\n"), usageout); + (void) fputs (_("Except for the -A and -M options, the options cannot be combined.\n"), usageout); +#else + (void) fputs (_("The options cannot be combined.\n"), usageout); +#endif + exit (status); +} + +/* + * catch_signals - set or reset termio modes. + * + * catch_signals() is called before processing begins. signal() is then + * called with catch_signals() as the signal handler. If signal later + * calls catch_signals() with a signal number, the terminal modes are + * then reset. + */ +static RETSIGTYPE catch_signals (int killed) +{ + static TERMIO sgtty; + + if (0 != killed) { + STTY (0, &sgtty); + } else { + GTTY (0, &sgtty); + } + + if (0 != killed) { + (void) write (STDOUT_FILENO, "\n", 1); + _exit (killed); + } +} + +/* + * is_valid_user_list - check a comma-separated list of user names for validity + * + * is_valid_user_list scans a comma-separated list of user names and + * checks that each listed name exists is the user database. + * + * It returns true if the list of users is valid. + */ +static bool is_valid_user_list (const char *users) +{ + const char *username; + char *end; + bool is_valid = true; + /*@owned@*/char *tmpusers = xstrdup (users); + + for (username = tmpusers; + (NULL != username) && ('\0' != *username); + username = end) { + end = strchr (username, ','); + if (NULL != end) { + *end = '\0'; + end++; + } + + /* + * This user must exist. + */ + + /* local, no need for xgetpwnam */ + if (getpwnam (username) == NULL) { + fprintf (stderr, _("%s: user '%s' does not exist\n"), + Prog, username); + is_valid = false; + } + } + + free (tmpusers); + + return is_valid; +} + +static void failure (void) +{ + fprintf (stderr, _("%s: Permission denied.\n"), Prog); + log_gpasswd_failure (": Permission denied"); + exit (E_NOPERM); +} + +/* + * process_flags - process the command line options and arguments + */ +static void process_flags (int argc, char **argv) +{ + int c; + static struct option long_options[] = { + {"add", required_argument, NULL, 'a'}, + {"administrators", required_argument, NULL, 'A'}, + {"delete", required_argument, NULL, 'd'}, + {"help", no_argument, NULL, 'h'}, + {"members", required_argument, NULL, 'M'}, + {"root", required_argument, NULL, 'Q'}, + {"remove-password", no_argument, NULL, 'r'}, + {"restrict", no_argument, NULL, 'R'}, + {NULL, 0, NULL, '\0'} + }; + + while ((c = getopt_long (argc, argv, "a:A:d:ghM:Q:rR", + long_options, NULL)) != -1) { + switch (c) { + case 'a': /* add a user */ + aflg = true; + user = optarg; + /* local, no need for xgetpwnam */ + if (getpwnam (user) == NULL) { + fprintf (stderr, + _("%s: user '%s' does not exist\n"), + Prog, user); + exit (E_BAD_ARG); + } + break; +#ifdef SHADOWGRP + case 'A': /* set the list of administrators */ + if (!is_shadowgrp) { + fprintf (stderr, + _("%s: shadow group passwords required for -A\n"), + Prog); + exit (E_GSHADOW_NOTFOUND); + } + admins = optarg; + if (!is_valid_user_list (admins)) { + exit (E_BAD_ARG); + } + Aflg = true; + break; +#endif /* SHADOWGRP */ + case 'd': /* delete a user */ + dflg = true; + user = optarg; + break; + case 'g': /* no-op from normal password */ + break; + case 'h': + usage (E_SUCCESS); + break; + case 'M': /* set the list of members */ + members = optarg; + if (!is_valid_user_list (members)) { + exit (E_BAD_ARG); + } + Mflg = true; + break; + case 'Q': /* no-op, handled in process_root_flag () */ + break; + case 'r': /* remove group password */ + rflg = true; + break; + case 'R': /* restrict group password */ + Rflg = true; + break; + default: + usage (E_USAGE); + } + } + + /* Get the name of the group that is being affected. */ + group = argv[optind]; + + check_flags (argc, optind); +} + +/* + * check_flags - check the validity of options + */ +static void check_flags (int argc, int opt_index) +{ + int exclusive = 0; + /* + * Make sure exclusive flags are exclusive + */ + if (aflg) { + exclusive++; + } + if (dflg) { + exclusive++; + } + if (rflg) { + exclusive++; + } + if (Rflg) { + exclusive++; + } + if (Aflg || Mflg) { + exclusive++; + } + if (exclusive > 1) { + usage (E_USAGE); + } + + /* + * Make sure one (and only one) group was provided + */ + if ((argc != (opt_index+1)) || (NULL == group)) { + usage (E_USAGE); + } +} + +/* + * open_files - lock and open the group databases + * + * It will call exit in case of error. + */ +static void open_files (void) +{ + if (gr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, gr_dbname ()); + exit (E_NOPERM); + } + add_cleanup (cleanup_unlock_group, NULL); + +#ifdef SHADOWGRP + if (is_shadowgrp) { + if (sgr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sgr_dbname ()); + exit (E_NOPERM); + } + add_cleanup (cleanup_unlock_gshadow, NULL); + } +#endif /* SHADOWGRP */ + + add_cleanup (log_gpasswd_failure_system, NULL); + + if (gr_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, gr_dbname ()); + SYSLOG ((LOG_WARN, "cannot open %s", gr_dbname ())); + exit (E_NOPERM); + } + +#ifdef SHADOWGRP + if (is_shadowgrp) { + if (sgr_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, sgr_dbname ()); + SYSLOG ((LOG_WARN, "cannot open %s", sgr_dbname ())); + exit (E_NOPERM); + } + add_cleanup (log_gpasswd_failure_gshadow, NULL); + } +#endif /* SHADOWGRP */ + + add_cleanup (log_gpasswd_failure_group, NULL); + del_cleanup (log_gpasswd_failure_system); +} + +static void log_gpasswd_failure (const char *suffix) +{ +#ifdef WITH_AUDIT + char buf[1024]; +#endif + if (aflg) { + SYSLOG ((LOG_ERR, + "%s failed to add user %s to group %s%s", + myname, user, group, suffix)); +#ifdef WITH_AUDIT + snprintf (buf, 1023, + "%s failed to add user %s to group %s%s", + myname, user, group, suffix); + buf[1023] = '\0'; + audit_logger (AUDIT_USER_ACCT, Prog, + buf, + group, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + } else if (dflg) { + SYSLOG ((LOG_ERR, + "%s failed to remove user %s from group %s%s", + myname, user, group, suffix)); +#ifdef WITH_AUDIT + snprintf (buf, 1023, + "%s failed to remove user %s from group %s%s", + myname, user, group, suffix); + buf[1023] = '\0'; + audit_logger (AUDIT_USER_ACCT, Prog, + buf, + group, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + } else if (rflg) { + SYSLOG ((LOG_ERR, + "%s failed to remove password of group %s%s", + myname, group, suffix)); +#ifdef WITH_AUDIT + snprintf (buf, 1023, + "%s failed to remove password of group %s%s", + myname, group, suffix); + buf[1023] = '\0'; + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + buf, + group, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + } else if (Rflg) { + SYSLOG ((LOG_ERR, + "%s failed to restrict access to group %s%s", + myname, group, suffix)); +#ifdef WITH_AUDIT + snprintf (buf, 1023, + "%s failed to restrict access to group %s%s", + myname, group, suffix); + buf[1023] = '\0'; + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + buf, + group, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + } else if (Aflg || Mflg) { +#ifdef SHADOWGRP + if (Aflg) { + SYSLOG ((LOG_ERR, + "%s failed to set the administrators of group %s to %s%s", + myname, group, admins, suffix)); +#ifdef WITH_AUDIT + snprintf (buf, 1023, + "%s failed to set the administrators of group %s to %s%s", + myname, group, admins, suffix); + buf[1023] = '\0'; + audit_logger (AUDIT_USER_ACCT, Prog, + buf, + group, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + } +#endif /* SHADOWGRP */ + if (Mflg) { + SYSLOG ((LOG_ERR, + "%s failed to set the members of group %s to %s%s", + myname, group, members, suffix)); +#ifdef WITH_AUDIT + snprintf (buf, 1023, + "%s failed to set the members of group %s to %s%s", + myname, group, members, suffix); + buf[1023] = '\0'; + audit_logger (AUDIT_USER_ACCT, Prog, + buf, + group, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + } + } else { + SYSLOG ((LOG_ERR, + "%s failed to change password of group %s%s", + myname, group, suffix)); +#ifdef WITH_AUDIT + snprintf (buf, 1023, + "%s failed to change password of group %s%s", + myname, group, suffix); + buf[1023] = '\0'; + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + buf, + group, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + } +} + +static void log_gpasswd_failure_system (unused void *arg) +{ + log_gpasswd_failure (""); +} + +static void log_gpasswd_failure_group (unused void *arg) +{ + char buf[1024]; + snprintf (buf, 1023, " in %s", gr_dbname ()); + buf[1023] = '\0'; + log_gpasswd_failure (buf); +} + +#ifdef SHADOWGRP +static void log_gpasswd_failure_gshadow (unused void *arg) +{ + char buf[1024]; + snprintf (buf, 1023, " in %s", sgr_dbname ()); + buf[1023] = '\0'; + log_gpasswd_failure (buf); +} +#endif /* SHADOWGRP */ + +static void log_gpasswd_success (const char *suffix) +{ +#ifdef WITH_AUDIT + char buf[1024]; +#endif + if (aflg) { + SYSLOG ((LOG_INFO, + "user %s added by %s to group %s%s", + user, myname, group, suffix)); +#ifdef WITH_AUDIT + snprintf (buf, 1023, + "user %s added by %s to group %s%s", + user, myname, group, suffix); + buf[1023] = '\0'; + audit_logger (AUDIT_USER_ACCT, Prog, + buf, + group, AUDIT_NO_ID, + SHADOW_AUDIT_SUCCESS); +#endif + } else if (dflg) { + SYSLOG ((LOG_INFO, + "user %s removed by %s from group %s%s", + user, myname, group, suffix)); +#ifdef WITH_AUDIT + snprintf (buf, 1023, + "user %s removed by %s from group %s%s", + user, myname, group, suffix); + buf[1023] = '\0'; + audit_logger (AUDIT_USER_ACCT, Prog, + buf, + group, AUDIT_NO_ID, + SHADOW_AUDIT_SUCCESS); +#endif + } else if (rflg) { + SYSLOG ((LOG_INFO, + "password of group %s removed by %s%s", + group, myname, suffix)); +#ifdef WITH_AUDIT + snprintf (buf, 1023, + "password of group %s removed by %s%s", + group, myname, suffix); + buf[1023] = '\0'; + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + buf, + group, AUDIT_NO_ID, + SHADOW_AUDIT_SUCCESS); +#endif + } else if (Rflg) { + SYSLOG ((LOG_INFO, + "access to group %s restricted by %s%s", + group, myname, suffix)); +#ifdef WITH_AUDIT + snprintf (buf, 1023, + "access to group %s restricted by %s%s", + group, myname, suffix); + buf[1023] = '\0'; + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + buf, + group, AUDIT_NO_ID, + SHADOW_AUDIT_SUCCESS); +#endif + } else if (Aflg || Mflg) { +#ifdef SHADOWGRP + if (Aflg) { + SYSLOG ((LOG_INFO, + "administrators of group %s set by %s to %s%s", + group, myname, admins, suffix)); +#ifdef WITH_AUDIT + snprintf (buf, 1023, + "administrators of group %s set by %s to %s%s", + group, myname, admins, suffix); + buf[1023] = '\0'; + audit_logger (AUDIT_USER_ACCT, Prog, + buf, + group, AUDIT_NO_ID, + SHADOW_AUDIT_SUCCESS); +#endif + } +#endif /* SHADOWGRP */ + if (Mflg) { + SYSLOG ((LOG_INFO, + "members of group %s set by %s to %s%s", + group, myname, members, suffix)); +#ifdef WITH_AUDIT + snprintf (buf, 1023, + "members of group %s set by %s to %s%s", + group, myname, members, suffix); + buf[1023] = '\0'; + audit_logger (AUDIT_USER_ACCT, Prog, + buf, + group, AUDIT_NO_ID, + SHADOW_AUDIT_SUCCESS); +#endif + } + } else { + SYSLOG ((LOG_INFO, + "password of group %s changed by %s%s", + group, myname, suffix)); +#ifdef WITH_AUDIT + snprintf (buf, 1023, + "password of group %s changed by %s%s", + group, myname, suffix); + buf[1023] = '\0'; + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + buf, + group, AUDIT_NO_ID, + SHADOW_AUDIT_SUCCESS); +#endif + } +} + +static void log_gpasswd_success_system (unused void *arg) +{ + log_gpasswd_success (""); +} + +static void log_gpasswd_success_group (unused void *arg) +{ + char buf[1024]; + snprintf (buf, 1023, " in %s", gr_dbname ()); + buf[1023] = '\0'; + log_gpasswd_success (buf); +} + +/* + * close_files - close and unlock the group databases + * + * This cause any changes in the databases to be committed. + * + * It will call exit in case of error. + */ +static void close_files (void) +{ + if (gr_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, gr_dbname ()); + exit (E_NOPERM); + } + add_cleanup (log_gpasswd_success_group, NULL); + del_cleanup (log_gpasswd_failure_group); + + cleanup_unlock_group (NULL); + del_cleanup (cleanup_unlock_group); + +#ifdef SHADOWGRP + if (is_shadowgrp) { + if (sgr_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, sgr_dbname ()); + exit (E_NOPERM); + } + del_cleanup (log_gpasswd_failure_gshadow); + + cleanup_unlock_gshadow (NULL); + del_cleanup (cleanup_unlock_gshadow); + } +#endif /* SHADOWGRP */ + + log_gpasswd_success_system (NULL); + del_cleanup (log_gpasswd_success_group); +} + +/* + * check_perms - check if the user is allowed to change the password of + * the specified group. + * + * It only returns if the user is allowed. + */ +#ifdef SHADOWGRP +static void check_perms (const struct group *gr, const struct sgrp *sg) +#else +static void check_perms (const struct group *gr) +#endif +{ + /* + * Only root can use the -M and -A options. + */ + if (!amroot && (Aflg || Mflg)) { + failure (); + } + +#ifdef SHADOWGRP + if (is_shadowgrp) { + /* + * The policy here for changing a group is that + * 1) you must be root or + * 2) you must be listed as an administrative member. + * Administrative members can do anything to a group that + * the root user can. + */ + if (!amroot && !is_on_list (sg->sg_adm, myname)) { + failure (); + } + } else +#endif /* SHADOWGRP */ + { +#ifdef FIRST_MEMBER_IS_ADMIN + /* + * The policy here for changing a group is that + * 1) you must be root or + * 2) you must be the first listed member of the group. + * The first listed member of a group can do anything to + * that group that the root user can. The rationale for + * this hack is that the FIRST user is probably the most + * important user in this entire group. + * + * This feature enabled by default could be a security + * problem when installed on existing systems where the + * first group member might be just a normal user. + * --marekm + */ + if (!amroot) { + if (gr->gr_mem[0] == (char *) 0) { + failure (); + } + + if (strcmp (gr->gr_mem[0], myname) != 0) { + failure (); + } + } +#else /* ! FIRST_MEMBER_IS_ADMIN */ + if (!amroot) { + failure (); + } +#endif + } +} + +/* + * update_group - Update the group information in the databases + */ +#ifdef SHADOWGRP +static void update_group (struct group *gr, struct sgrp *sg) +#else +static void update_group (struct group *gr) +#endif +{ + if (gr_update (gr) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, gr_dbname (), gr->gr_name); + exit (1); + } +#ifdef SHADOWGRP + if (is_shadowgrp && (sgr_update (sg) == 0)) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, sgr_dbname (), sg->sg_name); + exit (1); + } +#endif /* SHADOWGRP */ +} + +/* + * get_group - get the current information for the group + * + * The information are copied in group structure(s) so that they can be + * modified later. + * + * Note: If !is_shadowgrp, *sg will not be initialized. + */ +#ifdef SHADOWGRP +static void get_group (struct group *gr, struct sgrp *sg) +#else +static void get_group (struct group *gr) +#endif +{ + struct group const*tmpgr = NULL; +#ifdef SHADOWGRP + struct sgrp const*tmpsg = NULL; +#endif + + if (gr_open (O_RDONLY) == 0) { + fprintf (stderr, _("%s: cannot open %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_WARN, "cannot open %s", gr_dbname ())); + exit (E_NOPERM); + } + + tmpgr = gr_locate (group); + if (NULL == tmpgr) { + fprintf (stderr, + _("%s: group '%s' does not exist in %s\n"), + Prog, group, gr_dbname ()); + exit (E_BAD_ARG); + } + + *gr = *tmpgr; + gr->gr_name = xstrdup (tmpgr->gr_name); + gr->gr_passwd = xstrdup (tmpgr->gr_passwd); + gr->gr_mem = dup_list (tmpgr->gr_mem); + + if (gr_close () == 0) { + fprintf (stderr, + _("%s: failure while closing read-only %s\n"), + Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, + "failure while closing read-only %s", + gr_dbname ())); + exit (E_NOPERM); + } + +#ifdef SHADOWGRP + if (is_shadowgrp) { + if (sgr_open (O_RDONLY) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, sgr_dbname ()); + SYSLOG ((LOG_WARN, "cannot open %s", sgr_dbname ())); + exit (E_NOPERM); + } + tmpsg = sgr_locate (group); + if (NULL != tmpsg) { + *sg = *tmpsg; + sg->sg_name = xstrdup (tmpsg->sg_name); + sg->sg_passwd = xstrdup (tmpsg->sg_passwd); + + sg->sg_mem = dup_list (tmpsg->sg_mem); + sg->sg_adm = dup_list (tmpsg->sg_adm); + } else { + sg->sg_name = xstrdup (group); + sg->sg_passwd = gr->gr_passwd; + gr->gr_passwd = SHADOW_PASSWD_STRING; /* XXX warning: const */ + + sg->sg_mem = dup_list (gr->gr_mem); + + sg->sg_adm = (char **) xmalloc (sizeof (char *) * 2); +#ifdef FIRST_MEMBER_IS_ADMIN + if (sg->sg_mem[0]) { + sg->sg_adm[0] = xstrdup (sg->sg_mem[0]); + sg->sg_adm[1] = NULL; + } else +#endif + { + sg->sg_adm[0] = NULL; + } + + } + if (sgr_close () == 0) { + fprintf (stderr, + _("%s: failure while closing read-only %s\n"), + Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, + "failure while closing read-only %s", + sgr_dbname ())); + exit (E_NOPERM); + } + } +#endif /* SHADOWGRP */ +} + +/* + * change_passwd - change the group's password + * + * Get the new password from the user and update the password in the + * group's structure. + * + * It will call exit in case of error. + */ +#ifdef SHADOWGRP +static void change_passwd (struct group *gr, struct sgrp *sg) +#else +static void change_passwd (struct group *gr) +#endif +{ + char *cp; + static char pass[BUFSIZ]; + int retries; + const char *salt; + + /* + * A new password is to be entered and it must be encrypted, etc. + * The password will be prompted for twice, and both entries must be + * identical. There is no need to validate the old password since + * the invoker is either the group owner, or root. + */ + printf (_("Changing the password for group %s\n"), group); + + for (retries = 0; retries < RETRIES; retries++) { + cp = getpass (_("New Password: ")); + if (NULL == cp) { + exit (1); + } + + STRFCPY (pass, cp); + strzero (cp); + cp = getpass (_("Re-enter new password: ")); + if (NULL == cp) { + exit (1); + } + + if (strcmp (pass, cp) == 0) { + strzero (cp); + break; + } + + strzero (cp); + memzero (pass, sizeof pass); + + if (retries + 1 < RETRIES) { + puts (_("They don't match; try again")); + } + } + + if (retries == RETRIES) { + fprintf (stderr, _("%s: Try again later\n"), Prog); + exit (1); + } + + salt = crypt_make_salt (NULL, NULL); + cp = pw_encrypt (pass, salt); + if (NULL == cp) { + fprintf (stderr, + _("%s: failed to crypt password with salt '%s': %s\n"), + Prog, salt, strerror (errno)); + exit (1); + } + memzero (pass, sizeof pass); +#ifdef SHADOWGRP + if (is_shadowgrp) { + gr->gr_passwd = SHADOW_PASSWD_STRING; + sg->sg_passwd = cp; + } else +#endif + { + gr->gr_passwd = cp; + } +} + +/* + * gpasswd - administer the /etc/group file + */ +int main (int argc, char **argv) +{ + struct group grent; +#ifdef SHADOWGRP + struct sgrp sgent; +#endif + struct passwd *pw = NULL; + +#ifdef WITH_AUDIT + audit_help_open (); +#endif + + sanitize_env (); + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + /* + * Make a note of whether or not this command was invoked by root. + * This will be used to bypass certain checks later on. Also, set + * the real user ID to match the effective user ID. This will + * prevent the invoker from issuing signals which would interfere + * with this command. + */ + bywho = getuid (); + Prog = Basename (argv[0]); + + OPENLOG ("gpasswd"); + setbuf (stdout, NULL); + setbuf (stderr, NULL); + + process_root_flag ("-Q", argc, argv); + +#ifdef SHADOWGRP + is_shadowgrp = sgr_file_present (); +#endif + + /* + * Determine the name of the user that invoked this command. This + * is really hit or miss because there are so many ways that command + * can be executed and so many ways to trip up the routines that + * report the user name. + */ + pw = get_my_pwent (); + if (NULL == pw) { + fprintf (stderr, _("%s: Cannot determine your user name.\n"), + Prog); + SYSLOG ((LOG_WARN, + "Cannot determine the user name of the caller (UID %lu)", + (unsigned long) getuid ())); + exit (E_NOPERM); + } + myname = xstrdup (pw->pw_name); + + /* + * Register an exit function to warn for any inconsistency that we + * could create. + */ + if (atexit (do_cleanups) != 0) { + fprintf(stderr, "%s: cannot set exit function\n", Prog); + exit (1); + } + + /* Parse the options */ + process_flags (argc, argv); + + /* + * Replicate the group so it can be modified later on. + */ +#ifdef SHADOWGRP + get_group (&grent, &sgent); +#else + get_group (&grent); +#endif + + /* + * Check if the user is allowed to change the password of this group. + */ +#ifdef SHADOWGRP + check_perms (&grent, &sgent); +#else + check_perms (&grent); +#endif + + /* + * Removing a password is straight forward. Just set the password + * field to a "". + */ + if (rflg) { +#ifdef SHADOWGRP + if (is_shadowgrp) { + grent.gr_passwd = SHADOW_PASSWD_STRING; /* XXX warning: const */ + sgent.sg_passwd = ""; /* XXX warning: const */ + } else +#endif /* SHADOWGRP */ + { + grent.gr_passwd = ""; /* XXX warning: const */ + } + goto output; + } else if (Rflg) { + /* + * Same thing for restricting the group. Set the password + * field to "!". + */ +#ifdef SHADOWGRP + if (is_shadowgrp) { + grent.gr_passwd = SHADOW_PASSWD_STRING; /* XXX warning: const */ + sgent.sg_passwd = "!"; /* XXX warning: const */ + } else +#endif /* SHADOWGRP */ + { + grent.gr_passwd = "!"; /* XXX warning: const */ + } + goto output; + } + + /* + * Adding a member to a member list is pretty straightforward as + * well. Call the appropriate routine and split. + */ + if (aflg) { + printf (_("Adding user %s to group %s\n"), user, group); + grent.gr_mem = add_list (grent.gr_mem, user); +#ifdef SHADOWGRP + if (is_shadowgrp) { + sgent.sg_mem = add_list (sgent.sg_mem, user); + } +#endif + goto output; + } + + /* + * Removing a member from the member list is the same deal as adding + * one, except the routine is different. + */ + if (dflg) { + bool removed = false; + + printf (_("Removing user %s from group %s\n"), user, group); + + if (is_on_list (grent.gr_mem, user)) { + removed = true; + grent.gr_mem = del_list (grent.gr_mem, user); + } +#ifdef SHADOWGRP + if (is_shadowgrp) { + if (is_on_list (sgent.sg_mem, user)) { + removed = true; + sgent.sg_mem = del_list (sgent.sg_mem, user); + } + } +#endif + if (!removed) { + fprintf (stderr, + _("%s: user '%s' is not a member of '%s'\n"), + Prog, user, group); + exit (E_BAD_ARG); + } + goto output; + } +#ifdef SHADOWGRP + /* + * Replacing the entire list of administrators is simple. Check the + * list to make sure everyone is a real user. Then slap the new list + * in place. + */ + if (Aflg) { + sgent.sg_adm = comma_to_list (admins); + if (!Mflg) { + goto output; + } + } +#endif /* SHADOWGRP */ + + /* + * Replacing the entire list of members is simple. Check the list to + * make sure everyone is a real user. Then slap the new list in + * place. + */ + if (Mflg) { +#ifdef SHADOWGRP + sgent.sg_mem = comma_to_list (members); +#endif + grent.gr_mem = comma_to_list (members); + goto output; + } + + /* + * If the password is being changed, the input and output must both + * be a tty. The typical keyboard signals are caught so the termio + * modes can be restored. + */ + if ((isatty (0) == 0) || (isatty (1) == 0)) { + fprintf (stderr, _("%s: Not a tty\n"), Prog); + exit (E_NOPERM); + } + + catch_signals (0); /* save tty modes */ + + (void) signal (SIGHUP, catch_signals); + (void) signal (SIGINT, catch_signals); + (void) signal (SIGQUIT, catch_signals); + (void) signal (SIGTERM, catch_signals); +#ifdef SIGTSTP + (void) signal (SIGTSTP, catch_signals); +#endif + + /* Prompt for the new password */ +#ifdef SHADOWGRP + change_passwd (&grent, &sgent); +#else + change_passwd (&grent); +#endif + + /* + * This is the common arrival point to output the new group file. + * The freshly crafted entry is in allocated space. The group file + * will be locked and opened for writing. The new entry will be + * output, etc. + */ + output: + if (setuid (0) != 0) { + fputs (_("Cannot change ID to root.\n"), stderr); + SYSLOG ((LOG_ERR, "can't setuid(0)")); + closelog (); + exit (E_NOPERM); + } + pwd_init (); + + open_files (); + +#ifdef SHADOWGRP + update_group (&grent, &sgent); +#else + update_group (&grent); +#endif + + close_files (); + + nscd_flush_cache ("group"); + + exit (E_SUCCESS); +} + diff --git a/src/groupadd.c b/src/groupadd.c new file mode 100644 index 0000000..179438f --- /dev/null +++ b/src/groupadd.c @@ -0,0 +1,624 @@ +/* + * Copyright (c) 1991 - 1993, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2000 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2011, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <ctype.h> +#include <fcntl.h> +#include <getopt.h> +#include <grp.h> +#include <stdio.h> +#include <sys/types.h> +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM +#include "pam_defs.h" +#include <pwd.h> +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ +#include "chkname.h" +#include "defines.h" +#include "getdef.h" +#include "groupio.h" +#include "nscd.h" +#include "prototypes.h" +#ifdef SHADOWGRP +#include "sgroupio.h" +#endif + +/* + * exit status values + */ +/*@-exitarg@*/ +#define E_SUCCESS 0 /* success */ +#define E_USAGE 2 /* invalid command syntax */ +#define E_BAD_ARG 3 /* invalid argument to option */ +#define E_GID_IN_USE 4 /* gid not unique (when -o not used) */ +#define E_NAME_IN_USE 9 /* group name not unique */ +#define E_GRP_UPDATE 10 /* can't update group file */ + +/* + * Global variables + */ +const char *Prog; + +static /*@null@*/char *group_name; +static gid_t group_id; +static /*@null@*/char *group_passwd; +static /*@null@*/char *empty_list = NULL; + +static bool oflg = false; /* permit non-unique group ID to be specified with -g */ +static bool gflg = false; /* ID value for the new group */ +static bool fflg = false; /* if group already exists, do nothing and exit(0) */ +static bool rflg = false; /* create a system account */ +static bool pflg = false; /* new encrypted password */ + +#ifdef SHADOWGRP +static bool is_shadow_grp; +#endif + +/* local function prototypes */ +static /*@noreturn@*/void usage (int status); +static void new_grent (struct group *grent); + +#ifdef SHADOWGRP +static void new_sgent (struct sgrp *sgent); +#endif +static void grp_update (void); +static void check_new_name (void); +static void close_files (void); +static void open_files (void); +static void process_flags (int argc, char **argv); +static void check_flags (void); +static void check_perms (void); + +/* + * usage - display usage message and exit + */ +static /*@noreturn@*/void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options] GROUP\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -f, --force exit successfully if the group already exists,\n" + " and cancel -g if the GID is already used\n"), usageout); + (void) fputs (_(" -g, --gid GID use GID for the new group\n"), usageout); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -K, --key KEY=VALUE override /etc/login.defs defaults\n"), usageout); + (void) fputs (_(" -o, --non-unique allow to create groups with duplicate\n" + " (non-unique) GID\n"), usageout); + (void) fputs (_(" -p, --password PASSWORD use this encrypted password for the new group\n"), usageout); + (void) fputs (_(" -r, --system create a system account\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs ("\n", usageout); + exit (status); +} + +/* + * new_grent - initialize the values in a group file entry + * + * new_grent() takes all of the values that have been entered and fills + * in a (struct group) with them. + */ +static void new_grent (struct group *grent) +{ + memzero (grent, sizeof *grent); + grent->gr_name = group_name; + if (pflg) { + grent->gr_passwd = group_passwd; + } else { + grent->gr_passwd = SHADOW_PASSWD_STRING; /* XXX warning: const */ + } + grent->gr_gid = group_id; + grent->gr_mem = &empty_list; +} + +#ifdef SHADOWGRP +/* + * new_sgent - initialize the values in a shadow group file entry + * + * new_sgent() takes all of the values that have been entered and fills + * in a (struct sgrp) with them. + */ +static void new_sgent (struct sgrp *sgent) +{ + memzero (sgent, sizeof *sgent); + sgent->sg_name = group_name; + if (pflg) { + sgent->sg_passwd = group_passwd; + } else { + sgent->sg_passwd = "!"; /* XXX warning: const */ + } + sgent->sg_adm = &empty_list; + sgent->sg_mem = &empty_list; +} +#endif /* SHADOWGRP */ + +/* + * grp_update - add new group file entries + * + * grp_update() writes the new records to the group files. + */ +static void grp_update (void) +{ + struct group grp; + +#ifdef SHADOWGRP + struct sgrp sgrp; +#endif /* SHADOWGRP */ + + /* + * To add the group, we need to update /etc/group. + * Make sure failures will be reported. + */ + add_cleanup (cleanup_report_add_group_group, group_name); +#ifdef SHADOWGRP + if (is_shadow_grp) { + /* We also need to update /etc/gshadow */ + add_cleanup (cleanup_report_add_group_gshadow, group_name); + } +#endif + + /* + * Create the initial entries for this new group. + */ + new_grent (&grp); +#ifdef SHADOWGRP + new_sgent (&sgrp); + if (is_shadow_grp && pflg) { + grp.gr_passwd = SHADOW_PASSWD_STRING; /* XXX warning: const */ + } +#endif /* SHADOWGRP */ + + /* + * Write out the new group file entry. + */ + if (gr_update (&grp) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, gr_dbname (), grp.gr_name); + exit (E_GRP_UPDATE); + } +#ifdef SHADOWGRP + /* + * Write out the new shadow group entries as well. + */ + if (is_shadow_grp && (sgr_update (&sgrp) == 0)) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, sgr_dbname (), sgrp.sg_name); + exit (E_GRP_UPDATE); + } +#endif /* SHADOWGRP */ +} + +/* + * check_new_name - check the new name for validity + * + * check_new_name() insures that the new name doesn't contain any + * illegal characters. + */ +static void check_new_name (void) +{ + if (is_valid_group_name (group_name)) { + return; + } + + /* + * All invalid group names land here. + */ + + fprintf (stderr, _("%s: '%s' is not a valid group name\n"), + Prog, group_name); + + exit (E_BAD_ARG); +} + +/* + * close_files - close all of the files that were opened + * + * close_files() closes all of the files that were opened for this new + * group. This causes any modified entries to be written out. + */ +static void close_files (void) +{ + /* First, write the changes in the regular group database */ + if (gr_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, gr_dbname ()); + exit (E_GRP_UPDATE); + } +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_GROUP, Prog, + "adding group to /etc/group", + group_name, (unsigned int) group_id, + SHADOW_AUDIT_SUCCESS); +#endif + SYSLOG ((LOG_INFO, "group added to %s: name=%s, GID=%u", + gr_dbname (), group_name, (unsigned int) group_id)); + del_cleanup (cleanup_report_add_group_group); + + cleanup_unlock_group (NULL); + del_cleanup (cleanup_unlock_group); + + /* Now, write the changes in the shadow database */ +#ifdef SHADOWGRP + if (is_shadow_grp) { + if (sgr_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, sgr_dbname ()); + exit (E_GRP_UPDATE); + } +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_GROUP, Prog, + "adding group to /etc/gshadow", + group_name, (unsigned int) group_id, + SHADOW_AUDIT_SUCCESS); +#endif + SYSLOG ((LOG_INFO, "group added to %s: name=%s", + sgr_dbname (), group_name)); + del_cleanup (cleanup_report_add_group_gshadow); + + cleanup_unlock_gshadow (NULL); + del_cleanup (cleanup_unlock_gshadow); + } +#endif /* SHADOWGRP */ + + /* Report success at the system level */ +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_GROUP, Prog, + "", + group_name, (unsigned int) group_id, + SHADOW_AUDIT_SUCCESS); +#endif + SYSLOG ((LOG_INFO, "new group: name=%s, GID=%u", + group_name, (unsigned int) group_id)); + del_cleanup (cleanup_report_add_group); +} + +/* + * open_files - lock and open the group files + * + * open_files() opens the two group files. + */ +static void open_files (void) +{ + /* First, lock the databases */ + if (gr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, gr_dbname ()); + exit (E_GRP_UPDATE); + } + add_cleanup (cleanup_unlock_group, NULL); + +#ifdef SHADOWGRP + if (is_shadow_grp) { + if (sgr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sgr_dbname ()); + exit (E_GRP_UPDATE); + } + add_cleanup (cleanup_unlock_gshadow, NULL); + } +#endif /* SHADOWGRP */ + + /* + * Now if the group is not added, it's our fault. + * Make sure failures will be reported. + */ + add_cleanup (cleanup_report_add_group, group_name); + + /* And now open the databases */ + if (gr_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, _("%s: cannot open %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_WARN, "cannot open %s", gr_dbname ())); + exit (E_GRP_UPDATE); + } + +#ifdef SHADOWGRP + if (is_shadow_grp) { + if (sgr_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, sgr_dbname ()); + SYSLOG ((LOG_WARN, "cannot open %s", sgr_dbname ())); + exit (E_GRP_UPDATE); + } + } +#endif /* SHADOWGRP */ +} + +/* + * process_flags - parse the command line options + * + * It will not return if an error is encountered. + */ +static void process_flags (int argc, char **argv) +{ + /* + * Parse the command line options. + */ + char *cp; + int c; + static struct option long_options[] = { + {"force", no_argument, NULL, 'f'}, + {"gid", required_argument, NULL, 'g'}, + {"help", no_argument, NULL, 'h'}, + {"key", required_argument, NULL, 'K'}, + {"non-unique", no_argument, NULL, 'o'}, + {"password", required_argument, NULL, 'p'}, + {"system", no_argument, NULL, 'r'}, + {"root", required_argument, NULL, 'R'}, + {NULL, 0, NULL, '\0'} + }; + + while ((c = getopt_long (argc, argv, "fg:hK:op:rR:", + long_options, NULL)) != -1) { + switch (c) { + case 'f': + /* + * "force" - do nothing, just exit(0), if the + * specified group already exists. With -g, if + * specified gid already exists, choose another + * (unique) gid (turn off -g). Based on the RedHat's + * patch from shadow-utils-970616-9. + */ + fflg = true; + break; + case 'g': + gflg = true; + if ( (get_gid (optarg, &group_id) == 0) + || (group_id == (gid_t)-1)) { + fprintf (stderr, + _("%s: invalid group ID '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + break; + case 'h': + usage (E_SUCCESS); + /*@notreached@*/break; + case 'K': + /* + * override login.defs defaults (-K name=value) + * example: -K GID_MIN=100 -K GID_MAX=499 + * note: -K GID_MIN=10,GID_MAX=499 doesn't work yet + */ + cp = strchr (optarg, '='); + if (NULL == cp) { + fprintf (stderr, + _("%s: -K requires KEY=VALUE\n"), + Prog); + exit (E_BAD_ARG); + } + /* terminate name, point to value */ + *cp++ = '\0'; + if (putdef_str (optarg, cp) < 0) { + exit (E_BAD_ARG); + } + break; + case 'o': + oflg = true; + break; + case 'p': + pflg = true; + group_passwd = optarg; + break; + case 'r': + rflg = true; + break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + default: + usage (E_USAGE); + } + } + + /* + * Check the flags consistency + */ + if (optind != argc - 1) { + usage (E_USAGE); + } + group_name = argv[optind]; + + check_flags (); +} + +/* + * check_flags - check flags and parameters consistency + * + * It will not return if an error is encountered. + */ +static void check_flags (void) +{ + /* -o does not make sense without -g */ + if (oflg && !gflg) { + usage (E_USAGE); + } + + check_new_name (); + + /* + * Check if the group already exist. + */ + /* local, no need for xgetgrnam */ + if (getgrnam (group_name) != NULL) { + /* The group already exist */ + if (fflg) { + /* OK, no need to do anything */ + exit (E_SUCCESS); + } + fprintf (stderr, + _("%s: group '%s' already exists\n"), + Prog, group_name); + exit (E_NAME_IN_USE); + } + + if (gflg && (getgrgid (group_id) != NULL)) { + /* A GID was specified, and a group already exist with that GID + * - either we will use this GID anyway (-o) + * - either we ignore the specified GID and + * we will use another one (-f) + * - either it is a failure + */ + if (oflg) { + /* Continue with this GID */ + } else if (fflg) { + /* Turn off -g, we can use any GID */ + gflg = false; + } else { + fprintf (stderr, + _("%s: GID '%lu' already exists\n"), + Prog, (unsigned long int) group_id); + exit (E_GID_IN_USE); + } + } +} + +/* + * check_perms - check if the caller is allowed to add a group + * + * With PAM support, the setuid bit can be set on groupadd to allow + * non-root users to groups. + * Without PAM support, only users who can write in the group databases + * can add groups. + * + * It will not return if the user is not allowed. + */ +static void check_perms (void) +{ +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM + pam_handle_t *pamh = NULL; + int retval; + struct passwd *pampw; + + pampw = getpwuid (getuid ()); /* local, no need for xgetpwuid */ + if (NULL == pampw) { + fprintf (stderr, + _("%s: Cannot determine your user name.\n"), + Prog); + exit (1); + } + + retval = pam_start ("groupadd", pampw->pw_name, &conv, &pamh); + + if (PAM_SUCCESS == retval) { + retval = pam_authenticate (pamh, 0); + } + + if (PAM_SUCCESS == retval) { + retval = pam_acct_mgmt (pamh, 0); + } + + if (PAM_SUCCESS != retval) { + fprintf (stderr, _("%s: PAM: %s\n"), + Prog, pam_strerror (pamh, retval)); + SYSLOG((LOG_ERR, "%s", pam_strerror (pamh, retval))); + if (NULL != pamh) { + (void) pam_end (pamh, retval); + } + exit (1); + } + (void) pam_end (pamh, retval); +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ +} + +/* + * main - groupadd command + */ +int main (int argc, char **argv) +{ + /* + * Get my name so that I can use it to report errors. + */ + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + OPENLOG ("groupadd"); +#ifdef WITH_AUDIT + audit_help_open (); +#endif + + if (atexit (do_cleanups) != 0) { + fprintf (stderr, + _("%s: Cannot setup cleanup service.\n"), + Prog); + exit (1); + } + + /* + * Parse the command line options. + */ + process_flags (argc, argv); + + check_perms (); + +#ifdef SHADOWGRP + is_shadow_grp = sgr_file_present (); +#endif + + /* + * Do the hard stuff - open the files, create the group entries, + * then close and update the files. + */ + open_files (); + + if (!gflg) { + if (find_new_gid (rflg, &group_id, NULL) < 0) { + exit (E_GID_IN_USE); + } + } + + grp_update (); + close_files (); + + nscd_flush_cache ("group"); + + return E_SUCCESS; +} + diff --git a/src/groupdel.c b/src/groupdel.c new file mode 100644 index 0000000..11e522b --- /dev/null +++ b/src/groupdel.c @@ -0,0 +1,491 @@ +/* + * Copyright (c) 1991 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2000 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2011, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <ctype.h> +#include <fcntl.h> +#include <grp.h> +#include <pwd.h> +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM +#include "pam_defs.h" +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ +#include <stdio.h> +#include <sys/types.h> +#include <getopt.h> +#include "defines.h" +#include "groupio.h" +#include "nscd.h" +#include "prototypes.h" +#ifdef SHADOWGRP +#include "sgroupio.h" +#endif +/* + * Global variables + */ +const char *Prog; + +static char *group_name; +static gid_t group_id = -1; +static bool check_group_busy = true; + +#ifdef SHADOWGRP +static bool is_shadow_grp; +#endif + +/* + * exit status values + */ +/*@-exitarg@*/ +#define E_SUCCESS 0 /* success */ +#define E_USAGE 2 /* invalid command syntax */ +#define E_NOTFOUND 6 /* specified group doesn't exist */ +#define E_GROUP_BUSY 8 /* can't remove user's primary group */ +#define E_GRP_UPDATE 10 /* can't update group file */ + +/* local function prototypes */ +static /*@noreturn@*/void usage (int status); +static void grp_update (void); +static void close_files (void); +static void open_files (void); +static void group_busy (gid_t gid); +static void process_flags (int argc, char **argv); + +/* + * usage - display usage message and exit + */ +static /*@noreturn@*/void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options] GROUP\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs (_(" -f, --force delete group even if it is the primary group of a user\n"), usageout); + (void) fputs ("\n", usageout); + exit (status); +} + +/* + * grp_update - update group file entries + * + * grp_update() writes the new records to the group files. + */ +static void grp_update (void) +{ + /* + * To add the group, we need to update /etc/group. + * Make sure failures will be reported. + */ + add_cleanup (cleanup_report_del_group_group, group_name); +#ifdef SHADOWGRP + if (is_shadow_grp) { + /* We also need to update /etc/gshadow */ + add_cleanup (cleanup_report_del_group_gshadow, group_name); + } +#endif + + /* + * Delete the group entry. + */ + if (gr_remove (group_name) == 0) { + fprintf (stderr, + _("%s: cannot remove entry '%s' from %s\n"), + Prog, group_name, gr_dbname ()); + exit (E_GRP_UPDATE); + } + +#ifdef SHADOWGRP + /* + * Delete the shadow group entries as well. + */ + if (is_shadow_grp && (sgr_locate (group_name) != NULL)) { + if (sgr_remove (group_name) == 0) { + fprintf (stderr, + _("%s: cannot remove entry '%s' from %s\n"), + Prog, group_name, sgr_dbname ()); + exit (E_GRP_UPDATE); + } + } +#endif /* SHADOWGRP */ +} + +/* + * close_files - close all of the files that were opened + * + * close_files() closes all of the files that were opened for this + * new group. This causes any modified entries to be written out. + */ +static void close_files (void) +{ + /* First, write the changes in the regular group database */ + if (gr_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, gr_dbname ()); + exit (E_GRP_UPDATE); + } + +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_GROUP, Prog, + "removing group from /etc/group", + group_name, (unsigned int) group_id, + SHADOW_AUDIT_SUCCESS); +#endif + SYSLOG ((LOG_INFO, + "group '%s' removed from %s", + group_name, gr_dbname ())); + del_cleanup (cleanup_report_del_group_group); + + cleanup_unlock_group (NULL); + del_cleanup (cleanup_unlock_group); + + + /* Then, write the changes in the shadow database */ +#ifdef SHADOWGRP + if (is_shadow_grp) { + if (sgr_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, sgr_dbname ()); + exit (E_GRP_UPDATE); + } + +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_GROUP, Prog, + "removing group from /etc/gshadow", + group_name, (unsigned int) group_id, + SHADOW_AUDIT_SUCCESS); +#endif + SYSLOG ((LOG_INFO, + "group '%s' removed from %s", + group_name, sgr_dbname ())); + del_cleanup (cleanup_report_del_group_gshadow); + + cleanup_unlock_gshadow (NULL); + del_cleanup (cleanup_unlock_gshadow); + } +#endif /* SHADOWGRP */ + + /* Report success at the system level */ +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_GROUP, Prog, + "", + group_name, (unsigned int) group_id, + SHADOW_AUDIT_SUCCESS); +#endif + SYSLOG ((LOG_INFO, "group '%s' removed\n", group_name)); + del_cleanup (cleanup_report_del_group); +} + +/* + * open_files - lock and open the group files + * + * open_files() opens the two group files. + */ +static void open_files (void) +{ + /* First, lock the databases */ + if (gr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, gr_dbname ()); + exit (E_GRP_UPDATE); + } + add_cleanup (cleanup_unlock_group, NULL); +#ifdef SHADOWGRP + if (is_shadow_grp) { + if (sgr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sgr_dbname ()); + exit (E_GRP_UPDATE); + } + add_cleanup (cleanup_unlock_gshadow, NULL); + } +#endif + + /* + * Now, if the group is not removed, it's our fault. + * Make sure failures will be reported. + */ + add_cleanup (cleanup_report_del_group, group_name); + + /* An now open the databases */ + if (gr_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, gr_dbname ()); + SYSLOG ((LOG_WARN, "cannot open %s", gr_dbname ())); + exit (E_GRP_UPDATE); + } +#ifdef SHADOWGRP + if (is_shadow_grp) { + if (sgr_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, sgr_dbname ()); + SYSLOG ((LOG_WARN, "cannot open %s", sgr_dbname ())); + exit (E_GRP_UPDATE); + } + } +#endif /* SHADOWGRP */ +} + +/* + * group_busy - check if this is any user's primary group + * + * group_busy verifies that this group is not the primary group + * for any user. You must remove all users before you remove + * the group. + */ +static void group_busy (gid_t gid) +{ + struct passwd *pwd; + + /* + * Nice slow linear search. + */ + + setpwent (); + + while ( ((pwd = getpwent ()) != NULL) && (pwd->pw_gid != gid) ); + + endpwent (); + + /* + * If pwd isn't NULL, it stopped because the gid's matched. + */ + + if (pwd == (struct passwd *) 0) { + return; + } + + /* + * Can't remove the group. + */ + fprintf (stderr, + _("%s: cannot remove the primary group of user '%s'\n"), + Prog, pwd->pw_name); + exit (E_GROUP_BUSY); +} + +/* + * process_flags - parse the command line options + * + * It will not return if an error is encountered. + */ +static void process_flags (int argc, char **argv) +{ + /* + * Parse the command line options. + */ + int c; + static struct option long_options[] = { + {"help", no_argument, NULL, 'h'}, + {"root", required_argument, NULL, 'R'}, + {NULL, 0, NULL, '\0'} + }; + + while ((c = getopt_long (argc, argv, "hfR:", + long_options, NULL)) != -1) { + switch (c) { + case 'h': + usage (E_SUCCESS); + /*@notreached@*/break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + case 'f': + check_group_busy = false; + break; + default: + usage (E_USAGE); + } + } + + if (optind != argc - 1) { + usage (E_USAGE); + } + group_name = argv[optind]; +} + +/* + * main - groupdel command + * + * The syntax of the groupdel command is + * + * groupdel group + * + * The named group will be deleted. + */ + +int main (int argc, char **argv) +{ +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM + pam_handle_t *pamh = NULL; + int retval; +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ + + /* + * Get my name so that I can use it to report errors. + */ + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + OPENLOG ("groupdel"); +#ifdef WITH_AUDIT + audit_help_open (); +#endif + + if (atexit (do_cleanups) != 0) { + fprintf (stderr, + _("%s: Cannot setup cleanup service.\n"), + Prog); + exit (1); + } + + process_flags (argc, argv); + +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM + { + struct passwd *pampw; + pampw = getpwuid (getuid ()); /* local, no need for xgetpwuid */ + if (pampw == NULL) { + fprintf (stderr, + _("%s: Cannot determine your user name.\n"), + Prog); + exit (1); + } + + retval = pam_start ("groupdel", pampw->pw_name, &conv, &pamh); + } + + if (PAM_SUCCESS == retval) { + retval = pam_authenticate (pamh, 0); + } + + if (PAM_SUCCESS == retval) { + retval = pam_acct_mgmt (pamh, 0); + } + + if (PAM_SUCCESS != retval) { + fprintf (stderr, _("%s: PAM: %s\n"), + Prog, pam_strerror (pamh, retval)); + SYSLOG((LOG_ERR, "%s", pam_strerror (pamh, retval))); + if (NULL != pamh) { + (void) pam_end (pamh, retval); + } + exit (1); + } + (void) pam_end (pamh, retval); +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ + +#ifdef SHADOWGRP + is_shadow_grp = sgr_file_present (); +#endif + + { + struct group *grp; + /* + * Start with a quick check to see if the group exists. + */ + grp = getgrnam (group_name); /* local, no need for xgetgrnam */ + if (NULL == grp) { + fprintf (stderr, + _("%s: group '%s' does not exist\n"), + Prog, group_name); + exit (E_NOTFOUND); + } + + group_id = grp->gr_gid; + } + +#ifdef USE_NIS + /* + * Make sure this isn't a NIS group + */ + if (__isgrNIS ()) { + char *nis_domain; + char *nis_master; + + fprintf (stderr, + _("%s: group '%s' is a NIS group\n"), + Prog, group_name); + + if (!yp_get_default_domain (&nis_domain) && + !yp_master (nis_domain, "group.byname", &nis_master)) { + fprintf (stderr, + _("%s: %s is the NIS master\n"), + Prog, nis_master); + } + exit (E_NOTFOUND); + } +#endif + + /* + * Make sure this isn't the primary group of anyone. + */ + if (check_group_busy) { + group_busy (group_id); + } + + /* + * Do the hard stuff - open the files, delete the group entries, + * then close and update the files. + */ + open_files (); + + grp_update (); + + close_files (); + + nscd_flush_cache ("group"); + + return E_SUCCESS; +} + diff --git a/src/groupmems.c b/src/groupmems.c new file mode 100644 index 0000000..4a49e10 --- /dev/null +++ b/src/groupmems.c @@ -0,0 +1,652 @@ +/* + * Copyright (c) 2000 , International Business Machines + * George Kraft IV, gk4@us.ibm.com, 03/23/2000 + * Copyright (c) 2000 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2011, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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 <fcntl.h> +#include <getopt.h> +#include <grp.h> +#include <stdio.h> +#include <sys/types.h> +#ifdef USE_PAM +#include "pam_defs.h" +#endif /* USE_PAM */ +#include <pwd.h> +#include "defines.h" +#include "prototypes.h" +#include "groupio.h" +#ifdef SHADOWGRP +#include "sgroupio.h" +#endif + +/* Exit Status Values */ +/*@-exitarg@*/ +#define EXIT_SUCCESS 0 /* success */ +#define EXIT_USAGE 1 /* invalid command syntax */ +#define EXIT_GROUP_FILE 2 /* group file access problems */ +#define EXIT_NOT_ROOT 3 /* not superuser */ +#define EXIT_NOT_EROOT 4 /* not effective superuser */ +#define EXIT_NOT_PRIMARY 5 /* not primary owner of group */ +#define EXIT_NOT_MEMBER 6 /* member of group does not exist */ +#define EXIT_MEMBER_EXISTS 7 /* member of group already exists */ +#define EXIT_INVALID_USER 8 /* specified user does not exist */ +#define EXIT_INVALID_GROUP 9 /* specified group does not exist */ + +/* + * Global variables + */ +const char *Prog; + +static char *adduser = NULL; +static char *deluser = NULL; +static char *thisgroup = NULL; +static bool purge = false; +static bool list = false; +static int exclusive = 0; +static bool gr_locked = false; +#ifdef SHADOWGRP +/* Indicate if shadow groups are enabled on the system + * (/etc/gshadow present) */ +static bool is_shadowgrp; +static bool sgr_locked = false; +#endif + +/* local function prototypes */ +static char *whoami (void); +static void add_user (const char *user, + const struct group *grp); +static void remove_user (const char *user, + const struct group *grp); +static void purge_members (const struct group *grp); +static void display_members (const char *const *members); +static /*@noreturn@*/void usage (int status); +static void process_flags (int argc, char **argv); +static void check_perms (void); +static void fail_exit (int code); +#define isroot() (getuid () == 0) + +static char *whoami (void) +{ + /* local, no need for xgetgrgid */ + struct group *grp = getgrgid (getgid ()); + /* local, no need for xgetpwuid */ + struct passwd *usr = getpwuid (getuid ()); + + if ( (NULL != usr) + && (NULL != grp) + && (0 == strcmp (usr->pw_name, grp->gr_name))) { + return xstrdup (usr->pw_name); + } else { + return NULL; + } +} + +/* + * add_user - Add an user to the specified group + */ +static void add_user (const char *user, + const struct group *grp) +{ + struct group *newgrp; + + /* Make sure the user is not already part of the group */ + if (is_on_list (grp->gr_mem, user)) { + fprintf (stderr, + _("%s: user '%s' is already a member of '%s'\n"), + Prog, user, grp->gr_name); + fail_exit (EXIT_MEMBER_EXISTS); + } + + newgrp = __gr_dup(grp); + if (NULL == newgrp) { + fprintf (stderr, + _("%s: Out of memory. Cannot update %s.\n"), + Prog, gr_dbname ()); + fail_exit (13); + } + + /* Add the user to the /etc/group group */ + newgrp->gr_mem = add_list (newgrp->gr_mem, user); + +#ifdef SHADOWGRP + if (is_shadowgrp) { + const struct sgrp *sg = sgr_locate (newgrp->gr_name); + struct sgrp *newsg; + + if (NULL == sg) { + /* Create a shadow group based on this group */ + static struct sgrp sgrent; + sgrent.sg_name = xstrdup (newgrp->gr_name); + sgrent.sg_mem = dup_list (newgrp->gr_mem); + sgrent.sg_adm = (char **) xmalloc (sizeof (char *)); +#ifdef FIRST_MEMBER_IS_ADMIN + if (sgrent.sg_mem[0]) { + sgrent.sg_adm[0] = xstrdup (sgrent.sg_mem[0]); + sgrent.sg_adm[1] = NULL; + } else +#endif + { + sgrent.sg_adm[0] = NULL; + } + + /* Move any password to gshadow */ + sgrent.sg_passwd = newgrp->gr_passwd; + newgrp->gr_passwd = SHADOW_PASSWD_STRING; + + newsg = &sgrent; + } else { + newsg = __sgr_dup (sg); + if (NULL == newsg) { + fprintf (stderr, + _("%s: Out of memory. Cannot update %s.\n"), + Prog, sgr_dbname ()); + fail_exit (13); + } + /* Add the user to the members */ + newsg->sg_mem = add_list (newsg->sg_mem, user); + /* Do not touch the administrators */ + } + + if (sgr_update (newsg) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, sgr_dbname (), newsg->sg_name); + fail_exit (13); + } + } +#endif + + if (gr_update (newgrp) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, gr_dbname (), newgrp->gr_name); + fail_exit (13); + } +} + +/* + * remove_user - Remove an user from a given group + */ +static void remove_user (const char *user, + const struct group *grp) +{ + struct group *newgrp; + + /* Check if the user is a member of the specified group */ + if (!is_on_list (grp->gr_mem, user)) { + fprintf (stderr, + _("%s: user '%s' is not a member of '%s'\n"), + Prog, user, grp->gr_name); + fail_exit (EXIT_NOT_MEMBER); + } + + newgrp = __gr_dup (grp); + if (NULL == newgrp) { + fprintf (stderr, + _("%s: Out of memory. Cannot update %s.\n"), + Prog, gr_dbname ()); + fail_exit (13); + } + + /* Remove the user from the /etc/group group */ + newgrp->gr_mem = del_list (newgrp->gr_mem, user); + +#ifdef SHADOWGRP + if (is_shadowgrp) { + const struct sgrp *sg = sgr_locate (newgrp->gr_name); + struct sgrp *newsg; + + if (NULL == sg) { + /* Create a shadow group based on this group */ + static struct sgrp sgrent; + sgrent.sg_name = xstrdup (newgrp->gr_name); + sgrent.sg_mem = dup_list (newgrp->gr_mem); + sgrent.sg_adm = (char **) xmalloc (sizeof (char *)); +#ifdef FIRST_MEMBER_IS_ADMIN + if (sgrent.sg_mem[0]) { + sgrent.sg_adm[0] = xstrdup (sgrent.sg_mem[0]); + sgrent.sg_adm[1] = NULL; + } else +#endif + { + sgrent.sg_adm[0] = NULL; + } + + /* Move any password to gshadow */ + sgrent.sg_passwd = newgrp->gr_passwd; + newgrp->gr_passwd = SHADOW_PASSWD_STRING; + + newsg = &sgrent; + } else { + newsg = __sgr_dup (sg); + if (NULL == newsg) { + fprintf (stderr, + _("%s: Out of memory. Cannot update %s.\n"), + Prog, sgr_dbname ()); + fail_exit (13); + } + /* Remove the user from the members */ + newsg->sg_mem = del_list (newsg->sg_mem, user); + /* Remove the user from the administrators */ + newsg->sg_adm = del_list (newsg->sg_adm, user); + } + + if (sgr_update (newsg) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, sgr_dbname (), newsg->sg_name); + fail_exit (13); + } + } +#endif + + if (gr_update (newgrp) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, gr_dbname (), newgrp->gr_name); + fail_exit (13); + } +} + +/* + * purge_members - Rmeove every members of the specified group + */ +static void purge_members (const struct group *grp) +{ + struct group *newgrp = __gr_dup (grp); + + if (NULL == newgrp) { + fprintf (stderr, + _("%s: Out of memory. Cannot update %s.\n"), + Prog, gr_dbname ()); + fail_exit (13); + } + + /* Remove all the members of the /etc/group group */ + newgrp->gr_mem[0] = NULL; + +#ifdef SHADOWGRP + if (is_shadowgrp) { + const struct sgrp *sg = sgr_locate (newgrp->gr_name); + struct sgrp *newsg; + + if (NULL == sg) { + /* Create a shadow group based on this group */ + static struct sgrp sgrent; + sgrent.sg_name = xstrdup (newgrp->gr_name); + sgrent.sg_mem = (char **) xmalloc (sizeof (char *)); + sgrent.sg_mem[0] = NULL; + sgrent.sg_adm = (char **) xmalloc (sizeof (char *)); + sgrent.sg_adm[0] = NULL; + + /* Move any password to gshadow */ + sgrent.sg_passwd = newgrp->gr_passwd; + newgrp->gr_passwd = xstrdup(SHADOW_PASSWD_STRING); + + newsg = &sgrent; + } else { + newsg = __sgr_dup (sg); + if (NULL == newsg) { + fprintf (stderr, + _("%s: Out of memory. Cannot update %s.\n"), + Prog, sgr_dbname ()); + fail_exit (13); + } + /* Remove all the members of the /etc/gshadow + * group */ + newsg->sg_mem[0] = NULL; + /* Remove all the administrators of the + * /etc/gshadow group */ + newsg->sg_adm[0] = NULL; + } + + if (sgr_update (newsg) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, sgr_dbname (), newsg->sg_name); + fail_exit (13); + } + } +#endif + + if (gr_update (newgrp) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, gr_dbname (), newgrp->gr_name); + fail_exit (13); + } +} + +static void display_members (const char *const *members) +{ + int i; + + for (i = 0; NULL != members[i]; i++) { + printf ("%s ", members[i]); + + if (NULL == members[i + 1]) { + printf ("\n"); + } else { + printf (" "); + } + } +} + +static /*@noreturn@*/void usage (int status) +{ + FILE *usageout = (EXIT_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options] [action]\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -g, --group groupname change groupname instead of the user's group\n" + " (root only)\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs (_("\n"), usageout); + (void) fputs (_("Actions:\n"), usageout); + (void) fputs (_(" -a, --add username add username to the members of the group\n"), usageout); + (void) fputs (_(" -d, --delete username remove username from the members of the group\n"), usageout); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -p, --purge purge all members from the group\n"), usageout); + (void) fputs (_(" -l, --list list the members of the group\n"), usageout); + exit (status); +} + +/* + * process_flags - perform command line argument setting + */ +static void process_flags (int argc, char **argv) +{ + int c; + static struct option long_options[] = { + {"add", required_argument, NULL, 'a'}, + {"delete", required_argument, NULL, 'd'}, + {"group", required_argument, NULL, 'g'}, + {"help", no_argument, NULL, 'h'}, + {"list", no_argument, NULL, 'l'}, + {"purge", no_argument, NULL, 'p'}, + {"root", required_argument, NULL, 'R'}, + {NULL, 0, NULL, '\0'} + }; + + while ((c = getopt_long (argc, argv, "a:d:g:hlpR:", + long_options, NULL)) != EOF) { + switch (c) { + case 'a': + adduser = xstrdup (optarg); + ++exclusive; + break; + case 'd': + deluser = xstrdup (optarg); + ++exclusive; + break; + case 'g': + thisgroup = xstrdup (optarg); + break; + case 'h': + usage (EXIT_SUCCESS); + /*@notreached@*/break; + case 'l': + list = true; + ++exclusive; + break; + case 'p': + purge = true; + ++exclusive; + break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + default: + usage (EXIT_USAGE); + } + } + + if ((exclusive > 1) || (optind < argc)) { + usage (EXIT_USAGE); + } + + /* local, no need for xgetpwnam */ + if ( (NULL != adduser) + && (getpwnam (adduser) == NULL)) { + fprintf (stderr, _("%s: user '%s' does not exist\n"), + Prog, adduser); + fail_exit (EXIT_INVALID_USER); + } + +} + +static void check_perms (void) +{ + if (!list) { +#ifdef USE_PAM + pam_handle_t *pamh = NULL; + int retval; + struct passwd *pampw; + + pampw = getpwuid (getuid ()); /* local, no need for xgetpwuid */ + if (NULL == pampw) { + fprintf (stderr, + _("%s: Cannot determine your user name.\n"), + Prog); + fail_exit (1); + } + + retval = pam_start ("groupmems", pampw->pw_name, &conv, &pamh); + + if (PAM_SUCCESS == retval) { + retval = pam_authenticate (pamh, 0); + } + + if (PAM_SUCCESS == retval) { + retval = pam_acct_mgmt (pamh, 0); + } + + if (PAM_SUCCESS != retval) { + fprintf (stderr, _("%s: PAM: %s\n"), + Prog, pam_strerror (pamh, retval)); + SYSLOG((LOG_ERR, "%s", pam_strerror (pamh, retval))); + if (NULL != pamh) { + (void) pam_end (pamh, retval); + } + fail_exit (1); + } + (void) pam_end (pamh, retval); +#endif + } +} + +static void fail_exit (int code) +{ + if (gr_locked) { + if (gr_unlock () == 0) { + fprintf (stderr, + _("%s: failed to unlock %s\n"), + Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", gr_dbname ())); + /* continue */ + } + } + +#ifdef SHADOWGRP + if (sgr_locked) { + if (sgr_unlock () == 0) { + fprintf (stderr, + _("%s: failed to unlock %s\n"), + Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sgr_dbname ())); + /* continue */ + } + } +#endif + + exit (code); +} + +static void open_files (void) +{ + if (!list) { + if (gr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, gr_dbname ()); + fail_exit (EXIT_GROUP_FILE); + } + gr_locked = true; + +#ifdef SHADOWGRP + if (is_shadowgrp) { + if (sgr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sgr_dbname ()); + fail_exit (EXIT_GROUP_FILE); + } + sgr_locked = true; + } +#endif + } + + if (gr_open (list ? O_RDONLY : O_CREAT | O_RDWR) == 0) { + fprintf (stderr, _("%s: cannot open %s\n"), Prog, gr_dbname ()); + fail_exit (EXIT_GROUP_FILE); + } + +#ifdef SHADOWGRP + if (is_shadowgrp) { + if (sgr_open (list ? O_RDONLY : O_CREAT | O_RDWR) == 0) { + fprintf (stderr, _("%s: cannot open %s\n"), Prog, sgr_dbname ()); + fail_exit (EXIT_GROUP_FILE); + } + } +#endif +} + +static void close_files (void) +{ + if ((gr_close () == 0) && !list) { + fprintf (stderr, _("%s: failure while writing changes to %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", gr_dbname ())); + fail_exit (EXIT_GROUP_FILE); + } + if (gr_locked) { + if (gr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", gr_dbname ())); + /* continue */ + } + gr_locked = false; + } + +#ifdef SHADOWGRP + if (is_shadowgrp) { + if ((sgr_close () == 0) && !list) { + fprintf (stderr, _("%s: failure while writing changes to %s\n"), Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", sgr_dbname ())); + fail_exit (EXIT_GROUP_FILE); + } + if (sgr_locked) { + if (sgr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sgr_dbname ())); + /* continue */ + } + sgr_locked = false; + } + } +#endif +} + +int main (int argc, char **argv) +{ + char *name; + const struct group *grp; + + /* + * Get my name so that I can use it to report errors. + */ + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + OPENLOG ("groupmems"); + +#ifdef SHADOWGRP + is_shadowgrp = sgr_file_present (); +#endif + + process_flags (argc, argv); + + if (NULL == thisgroup) { + name = whoami (); + if (!list && (NULL == name)) { + fprintf (stderr, _("%s: your groupname does not match your username\n"), Prog); + fail_exit (EXIT_NOT_PRIMARY); + } + } else { + name = thisgroup; + if (!list && !isroot ()) { + fprintf (stderr, _("%s: only root can use the -g/--group option\n"), Prog); + fail_exit (EXIT_NOT_ROOT); + } + } + + check_perms (); + + open_files (); + + grp = gr_locate (name); + if (NULL == grp) { + fprintf (stderr, _("%s: group '%s' does not exist in %s\n"), + Prog, name, gr_dbname ()); + fail_exit (EXIT_INVALID_GROUP); + } + + if (list) { + display_members ((const char *const *)grp->gr_mem); + } else if (NULL != adduser) { + add_user (adduser, grp); + } else if (NULL != deluser) { + remove_user (deluser, grp); + } else if (purge) { + purge_members (grp); + } + + close_files (); + + exit (EXIT_SUCCESS); +} + diff --git a/src/groupmod.c b/src/groupmod.c new file mode 100644 index 0000000..757c1a4 --- /dev/null +++ b/src/groupmod.c @@ -0,0 +1,871 @@ +/* + * Copyright (c) 1991 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2000 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2011, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <ctype.h> +#include <fcntl.h> +#include <getopt.h> +#include <grp.h> +#include <stdio.h> +#include <sys/types.h> +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM +#include "pam_defs.h" +#include <pwd.h> +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ +#include "chkname.h" +#include "defines.h" +#include "groupio.h" +#include "pwio.h" +#include "nscd.h" +#include "prototypes.h" +#ifdef SHADOWGRP +#include "sgroupio.h" +#endif +/* + * exit status values + */ +/*@-exitarg@*/ +#define E_SUCCESS 0 /* success */ +#define E_USAGE 2 /* invalid command syntax */ +#define E_BAD_ARG 3 /* invalid argument to option */ +#define E_GID_IN_USE 4 /* gid already in use (and no -o) */ +#define E_NOTFOUND 6 /* specified group doesn't exist */ +#define E_NAME_IN_USE 9 /* group name already in use */ +#define E_GRP_UPDATE 10 /* can't update group file */ +/* + * Global variables + */ +const char *Prog; + +#ifdef SHADOWGRP +static bool is_shadow_grp; +#endif /* SHADOWGRP */ +static char *group_name; +static char *group_newname; +static char *group_passwd; +static gid_t group_id; +static gid_t group_newid; + +static struct cleanup_info_mod info_passwd; +static struct cleanup_info_mod info_group; +#ifdef SHADOWGRP +static struct cleanup_info_mod info_gshadow; +#endif + +static bool + oflg = false, /* permit non-unique group ID to be specified with -g */ + gflg = false, /* new ID value for the group */ + nflg = false, /* a new name has been specified for the group */ + pflg = false; /* new encrypted password */ + +/* local function prototypes */ +static void usage (int status); +static void new_grent (struct group *); + +#ifdef SHADOWGRP +static void new_sgent (struct sgrp *); +#endif +static void grp_update (void); +static void check_new_gid (void); +static void check_new_name (void); +static void process_flags (int, char **); +static void lock_files (void); +static void prepare_failure_reports (void); +static void open_files (void); +static void close_files (void); +static void update_primary_groups (gid_t ogid, gid_t ngid); + +/* + * usage - display usage message and exit + */ + +static void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options] GROUP\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -g, --gid GID change the group ID to GID\n"), usageout); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -n, --new-name NEW_GROUP change the name to NEW_GROUP\n"), usageout); + (void) fputs (_(" -o, --non-unique allow to use a duplicate (non-unique) GID\n"), usageout); + (void) fputs (_(" -p, --password PASSWORD change the password to this (encrypted)\n" + " PASSWORD\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs ("\n", usageout); + exit (status); +} + +/* + * new_grent - updates the values in a group file entry + * + * new_grent() takes all of the values that have been entered and fills + * in a (struct group) with them. + */ +static void new_grent (struct group *grent) +{ + if (nflg) { + grent->gr_name = xstrdup (group_newname); + } + + if (gflg) { + grent->gr_gid = group_newid; + } + + if ( pflg +#ifdef SHADOWGRP + && ( (!is_shadow_grp) + || (strcmp (grent->gr_passwd, SHADOW_PASSWD_STRING) != 0)) +#endif + ) { + /* Update the password in group if there is no gshadow + * file or if the password is currently in group + * (gr_passwd != "x"). We do not force the usage of + * shadow passwords if it was not the case before. + */ + grent->gr_passwd = group_passwd; + } +} + +#ifdef SHADOWGRP +/* + * new_sgent - updates the values in a shadow group file entry + * + * new_sgent() takes all of the values that have been entered and fills + * in a (struct sgrp) with them. + */ +static void new_sgent (struct sgrp *sgent) +{ + if (nflg) { + sgent->sg_name = xstrdup (group_newname); + } + + /* Always update the shadowed password if there is a shadow entry + * (even if shadowed passwords might not be enabled for this group + * (gr_passwd != "x")). + * It seems better to update the password in both places in case a + * shadow and a non shadow entry exist. + * This might occur only if there were already both entries. + */ + if (pflg) { + sgent->sg_passwd = group_passwd; + } +} +#endif /* SHADOWGRP */ + +/* + * grp_update - update group file entries + * + * grp_update() updates the new records in the memory databases. + */ +static void grp_update (void) +{ + struct group grp; + const struct group *ogrp; + +#ifdef SHADOWGRP + struct sgrp sgrp; + const struct sgrp *osgrp = NULL; +#endif /* SHADOWGRP */ + + /* + * Get the current settings for this group. + */ + ogrp = gr_locate (group_name); + if (NULL == ogrp) { + fprintf (stderr, + _("%s: group '%s' does not exist in %s\n"), + Prog, group_name, gr_dbname ()); + exit (E_GRP_UPDATE); + } + grp = *ogrp; + new_grent (&grp); +#ifdef SHADOWGRP + if ( is_shadow_grp + && (pflg || nflg)) { + osgrp = sgr_locate (group_name); + if (NULL != osgrp) { + sgrp = *osgrp; + new_sgent (&sgrp); + } else if ( pflg + && (strcmp (grp.gr_passwd, SHADOW_PASSWD_STRING) == 0)) { + static char *empty = NULL; + /* If there is a gshadow file with no entries for + * the group, but the group file indicates a + * shadowed password, we force the creation of a + * gshadow entry when a new password is requested. + */ + memset (&sgrp, 0, sizeof sgrp); + sgrp.sg_name = xstrdup (grp.gr_name); + sgrp.sg_passwd = xstrdup (grp.gr_passwd); + sgrp.sg_adm = ∅ + sgrp.sg_mem = dup_list (grp.gr_mem); + new_sgent (&sgrp); + osgrp = &sgrp; /* entry needs to be committed */ + } + } +#endif /* SHADOWGRP */ + + if (gflg) { + update_primary_groups (ogrp->gr_gid, group_newid); + } + + /* + * Write out the new group file entry. + */ + if (gr_update (&grp) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, gr_dbname (), grp.gr_name); + exit (E_GRP_UPDATE); + } + if (nflg && (gr_remove (group_name) == 0)) { + fprintf (stderr, + _("%s: cannot remove entry '%s' from %s\n"), + Prog, grp.gr_name, gr_dbname ()); + exit (E_GRP_UPDATE); + } + +#ifdef SHADOWGRP + /* + * Make sure there was a shadow entry to begin with. + */ + if (NULL != osgrp) { + /* + * Write out the new shadow group entries as well. + */ + if (sgr_update (&sgrp) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, sgr_dbname (), sgrp.sg_name); + exit (E_GRP_UPDATE); + } + if (nflg && (sgr_remove (group_name) == 0)) { + fprintf (stderr, + _("%s: cannot remove entry '%s' from %s\n"), + Prog, group_name, sgr_dbname ()); + exit (E_GRP_UPDATE); + } + } +#endif /* SHADOWGRP */ +} + +/* + * check_new_gid - check the new GID value for uniqueness + * + * check_new_gid() insures that the new GID value is unique. + */ +static void check_new_gid (void) +{ + /* + * First, the easy stuff. If the ID can be duplicated, or if the ID + * didn't really change, just return. If the ID didn't change, turn + * off those flags. No sense doing needless work. + */ + if (group_id == group_newid) { + gflg = 0; + return; + } + + if (oflg || + (getgrgid (group_newid) == NULL) /* local, no need for xgetgrgid */ + ) { + return; + } + + /* + * Tell the user what they did wrong. + */ + fprintf (stderr, + _("%s: GID '%lu' already exists\n"), + Prog, (unsigned long int) group_newid); + exit (E_GID_IN_USE); +} + +/* + * check_new_name - check the new name for uniqueness + * + * check_new_name() insures that the new name does not exist already. + * You can't have the same name twice, period. + */ +static void check_new_name (void) +{ + /* + * Make sure they are actually changing the name. + */ + if (strcmp (group_name, group_newname) == 0) { + nflg = 0; + return; + } + + if (is_valid_group_name (group_newname)) { + + /* + * If the entry is found, too bad. + */ + /* local, no need for xgetgrnam */ + if (getgrnam (group_newname) != NULL) { + fprintf (stderr, + _("%s: group '%s' already exists\n"), + Prog, group_newname); + exit (E_NAME_IN_USE); + } + return; + } + + /* + * All invalid group names land here. + */ + + fprintf (stderr, + _("%s: invalid group name '%s'\n"), + Prog, group_newname); + exit (E_BAD_ARG); +} + +/* + * process_flags - perform command line argument setting + * + * process_flags() interprets the command line arguments and sets the + * values that the user will be created with accordingly. The values + * are checked for sanity. + */ +static void process_flags (int argc, char **argv) +{ + int c; + static struct option long_options[] = { + {"gid", required_argument, NULL, 'g'}, + {"help", no_argument, NULL, 'h'}, + {"new-name", required_argument, NULL, 'n'}, + {"non-unique", no_argument, NULL, 'o'}, + {"password", required_argument, NULL, 'p'}, + {"root", required_argument, NULL, 'R'}, + {NULL, 0, NULL, '\0'} + }; + while ((c = getopt_long (argc, argv, "g:hn:op:R:", + long_options, NULL)) != -1) { + switch (c) { + case 'g': + gflg = true; + if ( (get_gid (optarg, &group_newid) == 0) + || (group_newid == (gid_t)-1)) { + fprintf (stderr, + _("%s: invalid group ID '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + break; + case 'h': + usage (E_SUCCESS); + break; + case 'n': + nflg = true; + group_newname = optarg; + break; + case 'o': + oflg = true; + break; + case 'p': + group_passwd = optarg; + pflg = true; + break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + default: + usage (E_USAGE); + } + } + + if (oflg && !gflg) { + usage (E_USAGE); + } + + if (optind != (argc - 1)) { + usage (E_USAGE); + } + + group_name = argv[argc - 1]; +} + +/* + * close_files - close all of the files that were opened + * + * close_files() closes all of the files that were opened for this new + * group. This causes any modified entries to be written out. + */ +static void close_files (void) +{ + if (gr_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, gr_dbname ()); + exit (E_GRP_UPDATE); + } +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_ACCT, Prog, + info_group.audit_msg, + group_name, AUDIT_NO_ID, + SHADOW_AUDIT_SUCCESS); +#endif + SYSLOG ((LOG_INFO, + "group changed in %s (%s)", + gr_dbname (), info_group.action)); + del_cleanup (cleanup_report_mod_group); + + cleanup_unlock_group (NULL); + del_cleanup (cleanup_unlock_group); + +#ifdef SHADOWGRP + if ( is_shadow_grp + && (pflg || nflg)) { + if (sgr_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, sgr_dbname ()); + exit (E_GRP_UPDATE); + } +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_ACCT, Prog, + info_gshadow.audit_msg, + group_name, AUDIT_NO_ID, + SHADOW_AUDIT_SUCCESS); +#endif + SYSLOG ((LOG_INFO, + "group changed in %s (%s)", + sgr_dbname (), info_gshadow.action)); + del_cleanup (cleanup_report_mod_gshadow); + + cleanup_unlock_gshadow (NULL); + del_cleanup (cleanup_unlock_gshadow); + } +#endif /* SHADOWGRP */ + + if (gflg) { + if (pw_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, pw_dbname ()); + exit (E_GRP_UPDATE); + } +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_ACCT, Prog, + info_passwd.audit_msg, + group_name, AUDIT_NO_ID, + SHADOW_AUDIT_SUCCESS); +#endif + SYSLOG ((LOG_INFO, + "group changed in %s (%s)", + pw_dbname (), info_passwd.action)); + del_cleanup (cleanup_report_mod_passwd); + + cleanup_unlock_passwd (NULL); + del_cleanup (cleanup_unlock_passwd); + } + +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_ACCT, Prog, + "modifying group", + group_name, AUDIT_NO_ID, + SHADOW_AUDIT_SUCCESS); +#endif +} + +/* + * prepare_failure_reports - Prepare the cleanup_info structure for logging + * of success and failure to syslog or audit. + */ +static void prepare_failure_reports (void) +{ + info_group.name = group_name; +#ifdef SHADOWGRP + info_gshadow.name = group_name; +#endif + info_passwd.name = group_name; + + info_group.audit_msg = xmalloc (512); +#ifdef SHADOWGRP + info_gshadow.audit_msg = xmalloc (512); +#endif + info_passwd.audit_msg = xmalloc (512); + + (void) snprintf (info_group.audit_msg, 511, + "changing %s; ", gr_dbname ()); +#ifdef SHADOWGRP + (void) snprintf (info_gshadow.audit_msg, 511, + "changing %s; ", sgr_dbname ()); +#endif + (void) snprintf (info_passwd.audit_msg, 511, + "changing %s; ", pw_dbname ()); + + info_group.action = info_group.audit_msg + + strlen (info_group.audit_msg); +#ifdef SHADOWGRP + info_gshadow.action = info_gshadow.audit_msg + + strlen (info_gshadow.audit_msg); +#endif + info_passwd.action = info_passwd.audit_msg + + strlen (info_passwd.audit_msg); + + (void) snprintf (info_group.action, + 511 - strlen (info_group.audit_msg), + "group %s/%lu", + group_name, (unsigned long int) group_id); +#ifdef SHADOWGRP + (void) snprintf (info_gshadow.action, + 511 - strlen (info_group.audit_msg), + "group %s", group_name); +#endif + (void) snprintf (info_passwd.action, + 511 - strlen (info_group.audit_msg), + "group %s/%lu", + group_name, (unsigned long int) group_id); + + if (nflg) { + strncat (info_group.action, ", new name: ", + 511 - strlen (info_group.audit_msg)); + strncat (info_group.action, group_newname, + 511 - strlen (info_group.audit_msg)); + +#ifdef SHADOWGRP + strncat (info_gshadow.action, ", new name: ", + 511 - strlen (info_gshadow.audit_msg)); + strncat (info_gshadow.action, group_newname, + 511 - strlen (info_gshadow.audit_msg)); +#endif + + strncat (info_passwd.action, ", new name: ", + 511 - strlen (info_passwd.audit_msg)); + strncat (info_passwd.action, group_newname, + 511 - strlen (info_passwd.audit_msg)); + } + if (pflg) { + strncat (info_group.action, ", new password", + 511 - strlen (info_group.audit_msg)); + +#ifdef SHADOWGRP + strncat (info_gshadow.action, ", new password", + 511 - strlen (info_gshadow.audit_msg)); +#endif + } + if (gflg) { + strncat (info_group.action, ", new gid: ", + 511 - strlen (info_group.audit_msg)); + (void) snprintf (info_group.action+strlen (info_group.action), + 511 - strlen (info_group.audit_msg), + "%lu", (unsigned long int) group_newid); + + strncat (info_passwd.action, ", new gid: ", + 511 - strlen (info_passwd.audit_msg)); + (void) snprintf (info_passwd.action+strlen (info_passwd.action), + 511 - strlen (info_passwd.audit_msg), + "%lu", (unsigned long int) group_newid); + } + info_group.audit_msg[511] = '\0'; +#ifdef SHADOWGRP + info_gshadow.audit_msg[511] = '\0'; +#endif + info_passwd.audit_msg[511] = '\0'; + +// FIXME: add a system cleanup + add_cleanup (cleanup_report_mod_group, &info_group); +#ifdef SHADOWGRP + if ( is_shadow_grp + && (pflg || nflg)) { + add_cleanup (cleanup_report_mod_gshadow, &info_gshadow); + } +#endif + if (gflg) { + add_cleanup (cleanup_report_mod_passwd, &info_passwd); + } + +} + +/* + * lock_files - lock the accounts databases + * + * lock_files() locks the group, gshadow, and passwd databases. + */ +static void lock_files (void) +{ + if (gr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, gr_dbname ()); + exit (E_GRP_UPDATE); + } + add_cleanup (cleanup_unlock_group, NULL); + +#ifdef SHADOWGRP + if ( is_shadow_grp + && (pflg || nflg)) { + if (sgr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sgr_dbname ()); + exit (E_GRP_UPDATE); + } + add_cleanup (cleanup_unlock_gshadow, NULL); + } +#endif + + if (gflg) { + if (pw_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, pw_dbname ()); + exit (E_GRP_UPDATE); + } + add_cleanup (cleanup_unlock_passwd, NULL); + } +} + + +/* + * open_files - open the accounts databases + * + * open_files() opens the group, gshadow, and passwd databases. + */ +static void open_files (void) +{ + if (gr_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, _("%s: cannot open %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_WARN, "cannot open %s", gr_dbname ())); + exit (E_GRP_UPDATE); + } + +#ifdef SHADOWGRP + if ( is_shadow_grp + && (pflg || nflg)) { + if (sgr_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, sgr_dbname ()); + SYSLOG ((LOG_WARN, "cannot open %s", sgr_dbname ())); + exit (E_GRP_UPDATE); + } + } +#endif /* SHADOWGRP */ + + if (gflg) { + if (pw_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, pw_dbname ()); + SYSLOG ((LOG_WARN, "cannot open %s", gr_dbname ())); + exit (E_GRP_UPDATE); + } + } +} + +void update_primary_groups (gid_t ogid, gid_t ngid) +{ + struct passwd *pwd; + + setpwent (); + while ((pwd = getpwent ()) != NULL) { + if (pwd->pw_gid == ogid) { + const struct passwd *lpwd; + struct passwd npwd; + lpwd = pw_locate (pwd->pw_name); + if (NULL == lpwd) { + fprintf (stderr, + _("%s: user '%s' does not exist in %s\n"), + Prog, pwd->pw_name, pw_dbname ()); + exit (E_GRP_UPDATE); + } else { + npwd = *lpwd; + npwd.pw_gid = ngid; + if (pw_update (&npwd) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, pw_dbname (), npwd.pw_name); + exit (E_GRP_UPDATE); + } + } + } + } + endpwent (); +} + +/* + * main - groupmod command + * + */ +int main (int argc, char **argv) +{ +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM + pam_handle_t *pamh = NULL; + int retval; +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ + + /* + * Get my name so that I can use it to report errors. + */ + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + OPENLOG ("groupmod"); +#ifdef WITH_AUDIT + audit_help_open (); +#endif + + if (atexit (do_cleanups) != 0) { + fprintf (stderr, + _("%s: Cannot setup cleanup service.\n"), + Prog); + exit (1); + } + + process_flags (argc, argv); + +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM + { + struct passwd *pampw; + pampw = getpwuid (getuid ()); /* local, no need for xgetpwuid */ + if (NULL == pampw) { + fprintf (stderr, + _("%s: Cannot determine your user name.\n"), + Prog); + exit (1); + } + + retval = pam_start ("groupmod", pampw->pw_name, &conv, &pamh); + } + + if (PAM_SUCCESS == retval) { + retval = pam_authenticate (pamh, 0); + } + + if (PAM_SUCCESS == retval) { + retval = pam_acct_mgmt (pamh, 0); + } + + if (PAM_SUCCESS != retval) { + fprintf (stderr, _("%s: PAM: %s\n"), + Prog, pam_strerror (pamh, retval)); + SYSLOG((LOG_ERR, "%s", pam_strerror (pamh, retval))); + if (NULL != pamh) { + (void) pam_end (pamh, retval); + } + exit (1); + } + (void) pam_end (pamh, retval); +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ + +#ifdef SHADOWGRP + is_shadow_grp = sgr_file_present (); +#endif + { + struct group *grp; + /* + * Start with a quick check to see if the group exists. + */ + grp = getgrnam (group_name); /* local, no need for xgetgrnam */ + if (NULL == grp) { + fprintf (stderr, + _("%s: group '%s' does not exist\n"), + Prog, group_name); + exit (E_NOTFOUND); + } else { + group_id = grp->gr_gid; + } + } + +#ifdef USE_NIS + /* + * Now make sure it isn't an NIS group. + */ + if (__isgrNIS ()) { + char *nis_domain; + char *nis_master; + + fprintf (stderr, + _("%s: group %s is a NIS group\n"), + Prog, group_name); + + if (!yp_get_default_domain (&nis_domain) && + !yp_master (nis_domain, "group.byname", &nis_master)) { + fprintf (stderr, + _("%s: %s is the NIS master\n"), + Prog, nis_master); + } + exit (E_NOTFOUND); + } +#endif + + if (gflg) { + check_new_gid (); + } + + if (nflg) { + check_new_name (); + } + + lock_files (); + + /* + * Now if the group is not changed, it's our fault. + * Make sure failures will be reported. + */ + prepare_failure_reports (); + + /* + * Do the hard stuff - open the files, create the group entries, + * then close and update the files. + */ + open_files (); + + grp_update (); + + close_files (); + + nscd_flush_cache ("group"); + + return E_SUCCESS; +} + diff --git a/src/groups.c b/src/groups.c new file mode 100644 index 0000000..fcd669b --- /dev/null +++ b/src/groups.c @@ -0,0 +1,218 @@ +/* + * Copyright (c) 1991 - 1993, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2001 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2008, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <grp.h> +#include <pwd.h> +#include <stdio.h> +#include "defines.h" +#include "prototypes.h" +/* + * Global variables + */ +const char *Prog; + +/* local function prototypes */ +static void print_groups (const char *member); + +/* + * print_groups - print the groups which the named user is a member of + * + * print_groups() scans the groups file for the list of groups which + * the user is listed as being a member of. + */ +static void print_groups (const char *member) +{ + int groups = 0; + struct group *grp; + struct passwd *pwd; + bool flag = false; + + pwd = getpwnam (member); /* local, no need for xgetpwnam */ + if (NULL == pwd) { + (void) fprintf (stderr, _("%s: unknown user %s\n"), + Prog, member); + exit (EXIT_FAILURE); + } + + setgrent (); + while ((grp = getgrent ()) != NULL) { + if (is_on_list (grp->gr_mem, member)) { + if (0 != groups) { + (void) putchar (' '); + } + groups++; + + (void) printf ("%s", grp->gr_name); + if (grp->gr_gid == pwd->pw_gid) { + flag = true; + } + } + } + endgrent (); + + /* The user may not be in the list of members of its primary group */ + if (!flag) { + grp = getgrgid (pwd->pw_gid); /* local, no need for xgetgrgid */ + if (NULL != grp) { + if (0 != groups) { + (void) putchar (' '); + } + groups++; + + (void) printf ("%s", grp->gr_name); + } + } + + if (0 != groups) { + (void) putchar ('\n'); + } +} + +/* + * groups - print out the groups a process is a member of + */ +int main (int argc, char **argv) +{ +#ifdef HAVE_GETGROUPS + long sys_ngroups; + GETGROUPS_T *groups; +#else + char *logname; + char *getlogin (); +#endif + +#ifdef HAVE_GETGROUPS + sys_ngroups = sysconf (_SC_NGROUPS_MAX); + groups = (GETGROUPS_T *) malloc (sizeof (GETGROUPS_T) * sys_ngroups); +#endif + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + /* + * Get the program name so that error messages can use it. + */ + Prog = Basename (argv[0]); + + if (argc == 1) { + + /* + * Called with no arguments - give the group set for the + * current user. + */ + +#ifdef HAVE_GETGROUPS + int i; + int pri_grp; /* TODO: should be GETGROUPS_T */ + /* + * This system supports concurrent group sets, so I can ask + * the system to tell me which groups are currently set for + * this process. + */ + int ngroups = getgroups (sys_ngroups, groups); + if (ngroups < 0) { + perror ("getgroups"); + exit (EXIT_FAILURE); + } + + /* + * The groupset includes the primary group as well. + */ + pri_grp = getegid (); + for (i = 0; i < ngroups; i++) { + if (pri_grp == (int) groups[i]) { + break; + } + } + + if (i != ngroups) { + pri_grp = -1; + } + + /* + * Print out the name of every group in the current group + * set. Unknown groups are printed as their decimal group ID + * values. + */ + if (-1 != pri_grp) { + struct group *gr; + /* local, no need for xgetgrgid */ + gr = getgrgid (pri_grp); + if (NULL != gr) { + (void) printf ("%s", gr->gr_name); + } else { + (void) printf ("%d", pri_grp); + } + } + + for (i = 0; i < ngroups; i++) { + struct group *gr; + if ((0 != i) || (-1 != pri_grp)) { + (void) putchar (' '); + } + + /* local, no need for xgetgrgid */ + gr = getgrgid (groups[i]); + if (NULL != gr) { + (void) printf ("%s", gr->gr_name); + } else { + (void) printf ("%ld", (long) groups[i]); + } + } + (void) putchar ('\n'); +#else + /* + * This system does not have the getgroups() system call, so + * I must check the groups file directly. + */ + logname = getlogin (); + if (NULL != logname) { + print_groups (logname); + } else { + exit (EXIT_FAILURE); + } +#endif + } else { + + /* + * The invoker wanted to know about some other user. Use + * that name to look up the groups instead. + */ + print_groups (argv[1]); + } + return EXIT_SUCCESS; +} + diff --git a/src/grpck.c b/src/grpck.c new file mode 100644 index 0000000..ea5d3b3 --- /dev/null +++ b/src/grpck.c @@ -0,0 +1,887 @@ +/* + * Copyright (c) 1992 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2001 , Michał Moskal + * Copyright (c) 2001 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2011, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <fcntl.h> +#include <grp.h> +#include <pwd.h> +#include <stdio.h> +#include <getopt.h> +#include "chkname.h" +#include "commonio.h" +#include "defines.h" +#include "groupio.h" +#include "nscd.h" +#include "prototypes.h" + +#ifdef SHADOWGRP +#include "sgroupio.h" +#endif + +/* + * Exit codes + */ +/*@-exitarg@*/ +#define E_OKAY 0 +#define E_SUCCESS 0 +#define E_USAGE 1 +#define E_BAD_ENTRY 2 +#define E_CANT_OPEN 3 +#define E_CANT_LOCK 4 +#define E_CANT_UPDATE 5 + +/* + * Global variables + */ +const char *Prog; + +static const char *grp_file = GROUP_FILE; +static bool use_system_grp_file = true; + +#ifdef SHADOWGRP +static const char *sgr_file = SGROUP_FILE; +static bool use_system_sgr_file = true; +static bool is_shadow = false; +static bool sgr_locked = false; +#endif +static bool gr_locked = false; +/* Options */ +static bool read_only = false; +static bool sort_mode = false; + +/* local function prototypes */ +static void fail_exit (int status); +static /*@noreturn@*/void usage (int status); +static void delete_member (char **, const char *); +static void process_flags (int argc, char **argv); +static void open_files (void); +static void close_files (bool changed); +static int check_members (const char *groupname, + char **members, + const char *fmt_info, + const char *fmt_prompt, + const char *fmt_syslog, + int *errors); +static void check_grp_file (int *errors, bool *changed); +#ifdef SHADOWGRP +static void compare_members_lists (const char *groupname, + char **members, + char **other_members, + const char *file, + const char *other_file); +static void check_sgr_file (int *errors, bool *changed); +#endif + +/* + * fail_exit - exit with an error code after unlocking files + */ +static void fail_exit (int status) +{ + if (gr_locked) { + if (gr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", gr_dbname ())); + /* continue */ + } + } + +#ifdef SHADOWGRP + if (sgr_locked) { + if (sgr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sgr_dbname ())); + /* continue */ + } + } +#endif + + closelog (); + + exit (status); +} + +/* + * usage - print syntax message and exit + */ +static /*@noreturn@*/void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; +#ifdef SHADOWGRP + (void) fprintf (usageout, + _("Usage: %s [options] [group [gshadow]]\n" + "\n" + "Options:\n"), + Prog); +#else /* !SHADOWGRP */ + (void) fprintf (usageout, + _("Usage: %s [options] [group]\n" + "\n" + "Options:\n"), + Prog); +#endif /* !SHADOWGRP */ + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -r, --read-only display errors and warnings\n" + " but do not change files\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs (_(" -s, --sort sort entries by UID\n"), usageout); + (void) fputs ("\n", usageout); + exit (status); +} + +/* + * delete_member - delete an entry in a list of members + * + * It only deletes the first entry with the given name. + * The member is defined by its address, no string comparison are + * performed. + */ +static void delete_member (char **list, const char *member) +{ + int i; + + for (i = 0; NULL != list[i]; i++) { + if (list[i] == member) { + break; + } + } + + for (; NULL != list[i]; i++) { + list[i] = list[i + 1]; + } +} + +/* + * process_flags - parse the command line options + * + * It will not return if an error is encountered. + */ +static void process_flags (int argc, char **argv) +{ + int c; + static struct option long_options[] = { + {"help", no_argument, NULL, 'h'}, + {"quiet", no_argument, NULL, 'q'}, + {"read-only", no_argument, NULL, 'r'}, + {"root", required_argument, NULL, 'R'}, + {"sort", no_argument, NULL, 's'}, + {NULL, 0, NULL, '\0'} + }; + + /* + * Parse the command line arguments + */ + while ((c = getopt_long (argc, argv, "hqrR:s", + long_options, NULL)) != -1) { + switch (c) { + case 'h': + usage (E_SUCCESS); + /*@notreached@*/break; + case 'q': + /* quiet - ignored for now */ + break; + case 'r': + read_only = true; + break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + case 's': + sort_mode = true; + break; + default: + usage (E_USAGE); + } + } + + if (sort_mode && read_only) { + fprintf (stderr, _("%s: -s and -r are incompatible\n"), Prog); + exit (E_USAGE); + } + + /* + * Make certain we have the right number of arguments + */ +#ifdef SHADOWGRP + if (argc > (optind + 2)) +#else + if (argc > (optind + 1)) +#endif + { + usage (E_USAGE); + } + + /* + * If there are two left over filenames, use those as the group and + * group password filenames. + */ + if (optind != argc) { + grp_file = argv[optind]; + gr_setdbname (grp_file); + use_system_grp_file = false; + } +#ifdef SHADOWGRP + if ((optind + 2) == argc) { + sgr_file = argv[optind + 1]; + sgr_setdbname (sgr_file); + is_shadow = true; + use_system_sgr_file = false; + } else if (optind == argc) { + is_shadow = sgr_file_present (); + } +#endif +} + +/* + * open_files - open the shadow database + * + * In read-only mode, the databases are not locked and are opened + * only for reading. + */ +static void open_files (void) +{ + /* + * Lock the files if we aren't in "read-only" mode + */ + if (!read_only) { + if (gr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, grp_file); + fail_exit (E_CANT_LOCK); + } + gr_locked = true; +#ifdef SHADOWGRP + if (is_shadow) { + if (sgr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sgr_file); + fail_exit (E_CANT_LOCK); + } + sgr_locked = true; + } +#endif + } + + /* + * Open the files. Use O_RDONLY if we are in read_only mode, + * O_RDWR otherwise. + */ + if (gr_open (read_only ? O_RDONLY : O_CREAT | O_RDWR) == 0) { + fprintf (stderr, _("%s: cannot open %s\n"), Prog, + grp_file); + if (use_system_grp_file) { + SYSLOG ((LOG_WARN, "cannot open %s", grp_file)); + } + fail_exit (E_CANT_OPEN); + } +#ifdef SHADOWGRP + if (is_shadow && (sgr_open (read_only ? O_RDONLY : O_CREAT | O_RDWR) == 0)) { + fprintf (stderr, _("%s: cannot open %s\n"), Prog, + sgr_file); + if (use_system_sgr_file) { + SYSLOG ((LOG_WARN, "cannot open %s", sgr_file)); + } + fail_exit (E_CANT_OPEN); + } +#endif +} + +/* + * close_files - close and unlock the group/gshadow databases + * + * If changed is not set, the databases are not closed, and no + * changes are committed in the databases. The databases are + * unlocked anyway. + */ +static void close_files (bool changed) +{ + /* + * All done. If there were no change we can just abandon any + * changes to the files. + */ + if (changed) { + if (gr_close () == 0) { + fprintf (stderr, _("%s: failure while writing changes to %s\n"), + Prog, grp_file); + fail_exit (E_CANT_UPDATE); + } +#ifdef SHADOWGRP + if (is_shadow && (sgr_close () == 0)) { + fprintf (stderr, _("%s: failure while writing changes to %s\n"), + Prog, sgr_file); + fail_exit (E_CANT_UPDATE); + } +#endif + } + + /* + * Don't be anti-social - unlock the files when you're done. + */ +#ifdef SHADOWGRP + if (sgr_locked) { + if (sgr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sgr_dbname ())); + /* continue */ + } + sgr_locked = false; + } +#endif + if (gr_locked) { + if (gr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", gr_dbname ())); + /* continue */ + } + gr_locked = false; + } +} + +/* + * check_members - check that every members of a group exist + * + * If an error is detected, *errors is incremented. + * + * The user will be prompted for the removal of the non-existent + * user. + * + * If any changes are performed, the return value will be 1, + * otherwise check_members() returns 0. + * + * fmt_info, fmt_prompt, and fmt_syslog are used for logging. + * * fmt_info must contain two string flags (%s): for the group's + * name and the missing member. + * * fmt_prompt must contain one string flags (%s): the missing + * member. + * * fmt_syslog must contain two string flags (%s): for the + * group's name and the missing member. + */ +static int check_members (const char *groupname, + char **members, + const char *fmt_info, + const char *fmt_prompt, + const char *fmt_syslog, + int *errors) +{ + int i; + int members_changed = 0; + + /* + * Make sure each member exists + */ + for (i = 0; NULL != members[i]; i++) { + /* local, no need for xgetpwnam */ + if (getpwnam (members[i]) != NULL) { + continue; + } + /* + * Can't find this user. Remove them + * from the list. + */ + *errors += 1; + printf (fmt_info, groupname, members[i]); + printf (fmt_prompt, members[i]); + + if (!yes_or_no (read_only)) { + continue; + } + + SYSLOG ((LOG_INFO, fmt_syslog, members[i], groupname)); + members_changed = 1; + delete_member (members, members[i]); + + /* Rewind in case of removal */ + i--; + } + + return members_changed; +} + +#ifdef SHADOWGRP +/* + * compare_members_lists - make sure the list of members is contained in + * another list. + * + * compare_members_lists() checks that all the members of members are + * also in other_members. + * file and other_file are used for logging. + * + * TODO: No changes are performed on the lists. + */ +static void compare_members_lists (const char *groupname, + char **members, + char **other_members, + const char *file, + const char *other_file) +{ + char **pmem, **other_pmem; + + for (pmem = members; NULL != *pmem; pmem++) { + for (other_pmem = other_members; NULL != *other_pmem; other_pmem++) { + if (strcmp (*pmem, *other_pmem) == 0) { + break; + } + } + if (*other_pmem == NULL) { + printf + ("'%s' is a member of the '%s' group in %s but not in %s\n", + *pmem, groupname, file, other_file); + } + } +} +#endif /* SHADOWGRP */ + +/* + * check_grp_file - check the content of the group file + */ +static void check_grp_file (int *errors, bool *changed) +{ + struct commonio_entry *gre, *tgre; + struct group *grp; +#ifdef SHADOWGRP + struct sgrp *sgr; +#endif + + /* + * Loop through the entire group file. + */ + for (gre = __gr_get_head (); NULL != gre; gre = gre->next) { + /* + * Skip all NIS entries. + */ + + if ((gre->line[0] == '+') || (gre->line[0] == '-')) { + continue; + } + + /* + * Start with the entries that are completely corrupt. They + * have no (struct group) entry because they couldn't be + * parsed properly. + */ + if (NULL == gre->eptr) { + + /* + * Tell the user this entire line is bogus and ask + * them to delete it. + */ + (void) puts (_("invalid group file entry")); + printf (_("delete line '%s'? "), gre->line); + *errors += 1; + + /* + * prompt the user to delete the entry or not + */ + if (!yes_or_no (read_only)) { + continue; + } + + /* + * All group file deletions wind up here. This code + * removes the current entry from the linked list. + * When done, it skips back to the top of the loop + * to try out the next list element. + */ + delete_gr: + SYSLOG ((LOG_INFO, "delete group line '%s'", + gre->line)); + *changed = true; + + __gr_del_entry (gre); + continue; + } + + /* + * Group structure is good, start using it. + */ + grp = gre->eptr; + + /* + * Make sure this entry has a unique name. + */ + for (tgre = __gr_get_head (); NULL != tgre; tgre = tgre->next) { + + const struct group *ent = tgre->eptr; + + /* + * Don't check this entry + */ + if (tgre == gre) { + continue; + } + + /* + * Don't check invalid entries. + */ + if (NULL == ent) { + continue; + } + + if (strcmp (grp->gr_name, ent->gr_name) != 0) { + continue; + } + + /* + * Tell the user this entry is a duplicate of + * another and ask them to delete it. + */ + (void) puts (_("duplicate group entry")); + printf (_("delete line '%s'? "), gre->line); + *errors += 1; + + /* + * prompt the user to delete the entry or not + */ + if (yes_or_no (read_only)) { + goto delete_gr; + } + } + + /* + * Check for invalid group names. --marekm + */ + if (!is_valid_group_name (grp->gr_name)) { + *errors += 1; + printf (_("invalid group name '%s'\n"), grp->gr_name); + } + + /* + * Check for invalid group ID. + */ + if (grp->gr_gid == (gid_t)-1) { + printf (_("invalid group ID '%lu'\n"), (long unsigned int)grp->gr_gid); + *errors += 1; + } + + /* + * Workaround for a NYS libc 5.3.12 bug on RedHat 4.2 - + * groups with no members are returned as groups with one + * member "", causing grpck to fail. --marekm + */ + if ( (NULL != grp->gr_mem[0]) + && (NULL == grp->gr_mem[1]) + && ('\0' == grp->gr_mem[0][0])) { + grp->gr_mem[0] = NULL; + } + + if (check_members (grp->gr_name, grp->gr_mem, + _("group %s: no user %s\n"), + _("delete member '%s'? "), + "delete member '%s' from group '%s'", + errors) == 1) { + *changed = true; + gre->changed = true; + __gr_set_changed (); + } + +#ifdef SHADOWGRP + /* + * Make sure this entry exists in the /etc/gshadow file. + */ + + if (is_shadow) { + sgr = (struct sgrp *) sgr_locate (grp->gr_name); + if (sgr == NULL) { + printf (_("no matching group file entry in %s\n"), + sgr_file); + printf (_("add group '%s' in %s? "), + grp->gr_name, sgr_file); + *errors += 1; + if (yes_or_no (read_only)) { + struct sgrp sg; + struct group gr; + static char *empty = NULL; + + sg.sg_name = grp->gr_name; + sg.sg_passwd = grp->gr_passwd; + sg.sg_adm = ∅ + sg.sg_mem = grp->gr_mem; + SYSLOG ((LOG_INFO, + "add group '%s' to '%s'", + grp->gr_name, sgr_file)); + *changed = true; + + if (sgr_update (&sg) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, sgr_dbname (), sg.sg_name); + fail_exit (E_CANT_UPDATE); + } + /* remove password from /etc/group */ + gr = *grp; + gr.gr_passwd = SHADOW_PASSWD_STRING; /* XXX warning: const */ + if (gr_update (&gr) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, gr_dbname (), gr.gr_name); + fail_exit (E_CANT_UPDATE); + } + } + } else { + /** + * Verify that all the members defined in /etc/group are also + * present in /etc/gshadow. + */ + compare_members_lists (grp->gr_name, + grp->gr_mem, sgr->sg_mem, + grp_file, sgr_file); + + /* The group entry has a gshadow counterpart. + * Make sure no passwords are in group. + */ + if (strcmp (grp->gr_passwd, SHADOW_PASSWD_STRING) != 0) { + printf (_("group %s has an entry in %s, but its password field in %s is not set to 'x'\n"), + grp->gr_name, sgr_file, grp_file); + *errors += 1; + } + } + } +#endif + + } +} + +#ifdef SHADOWGRP +/* + * check_sgr_file - check the content of the shadowed group file (gshadow) + */ +static void check_sgr_file (int *errors, bool *changed) +{ + struct group *grp; + struct commonio_entry *sge, *tsge; + struct sgrp *sgr; + + /* + * Loop through the entire shadow group file. + */ + for (sge = __sgr_get_head (); NULL != sge; sge = sge->next) { + + /* + * Start with the entries that are completely corrupt. They + * have no (struct sgrp) entry because they couldn't be + * parsed properly. + */ + if (NULL == sge->eptr) { + + /* + * Tell the user this entire line is bogus and ask + * them to delete it. + */ + (void) puts (_("invalid shadow group file entry")); + printf (_("delete line '%s'? "), sge->line); + *errors += 1; + + /* + * prompt the user to delete the entry or not + */ + if (!yes_or_no (read_only)) { + continue; + } + + /* + * All shadow group file deletions wind up here. + * This code removes the current entry from the + * linked list. When done, it skips back to the top + * of the loop to try out the next list element. + */ + delete_sg: + SYSLOG ((LOG_INFO, "delete shadow line '%s'", + sge->line)); + *changed = true; + + __sgr_del_entry (sge); + continue; + } + + /* + * Shadow group structure is good, start using it. + */ + sgr = sge->eptr; + + /* + * Make sure this entry has a unique name. + */ + for (tsge = __sgr_get_head (); NULL != tsge; tsge = tsge->next) { + + const struct sgrp *ent = tsge->eptr; + + /* + * Don't check this entry + */ + if (tsge == sge) { + continue; + } + + /* + * Don't check invalid entries. + */ + if (NULL == ent) { + continue; + } + + if (strcmp (sgr->sg_name, ent->sg_name) != 0) { + continue; + } + + /* + * Tell the user this entry is a duplicate of + * another and ask them to delete it. + */ + (void) puts (_("duplicate shadow group entry")); + printf (_("delete line '%s'? "), sge->line); + *errors += 1; + + /* + * prompt the user to delete the entry or not + */ + if (yes_or_no (read_only)) { + goto delete_sg; + } + } + + /* + * Make sure this entry exists in the /etc/group file. + */ + grp = (struct group *) gr_locate (sgr->sg_name); + if (grp == NULL) { + printf (_("no matching group file entry in %s\n"), + grp_file); + printf (_("delete line '%s'? "), sge->line); + *errors += 1; + if (yes_or_no (read_only)) { + goto delete_sg; + } + } else { + /** + * Verify that the all members defined in /etc/gshadow are also + * present in /etc/group. + */ + compare_members_lists (sgr->sg_name, + sgr->sg_mem, grp->gr_mem, + sgr_file, grp_file); + } + + /* + * Make sure each administrator exists + */ + if (check_members (sgr->sg_name, sgr->sg_adm, + _("shadow group %s: no administrative user %s\n"), + _("delete administrative member '%s'? "), + "delete admin '%s' from shadow group '%s'", + errors) == 1) { + *changed = true; + sge->changed = true; + __sgr_set_changed (); + } + + /* + * Make sure each member exists + */ + if (check_members (sgr->sg_name, sgr->sg_mem, + _("shadow group %s: no user %s\n"), + _("delete member '%s'? "), + "delete member '%s' from shadow group '%s'", + errors) == 1) { + *changed = true; + sge->changed = true; + __sgr_set_changed (); + } + } +} +#endif /* SHADOWGRP */ + +/* + * grpck - verify group file integrity + */ +int main (int argc, char **argv) +{ + int errors = 0; + bool changed = false; + + /* + * Get my name so that I can use it to report errors. + */ + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + OPENLOG ("grpck"); + + /* Parse the command line arguments */ + process_flags (argc, argv); + + open_files (); + + if (sort_mode) { + gr_sort (); +#ifdef SHADOWGRP + if (is_shadow) { + sgr_sort (); + } + changed = true; +#endif + } else { + check_grp_file (&errors, &changed); +#ifdef SHADOWGRP + if (is_shadow) { + check_sgr_file (&errors, &changed); + } +#endif + } + + /* Commit the change in the database if needed */ + close_files (changed); + + nscd_flush_cache ("group"); + + /* + * Tell the user what we did and exit. + */ + if (0 != errors) { + if (changed) { + printf (_("%s: the files have been updated\n"), Prog); + } else { + printf (_("%s: no changes\n"), Prog); + } + } + + return ((0 != errors) ? E_BAD_ENTRY : E_OKAY); +} + diff --git a/src/grpconv.c b/src/grpconv.c new file mode 100644 index 0000000..f681f07 --- /dev/null +++ b/src/grpconv.c @@ -0,0 +1,286 @@ +/* + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2002 - 2006, Tomasz Kłoczko + * Copyright (c) 2011 , Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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. + */ + +/* + * grpconv - create or update /etc/gshadow with information from + * /etc/group. + * + */ + +#include <config.h> +#ident "$Id$" + +#include <errno.h> +#include <fcntl.h> +#include <grp.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <getopt.h> +#include "nscd.h" +#include "prototypes.h" +/*@-exitarg@*/ +#include "exitcodes.h" +#ifdef SHADOWGRP +#include "groupio.h" +#include "sgroupio.h" +/* + * Global variables + */ +const char *Prog; + +static bool gr_locked = false; +static bool sgr_locked = false; + +/* local function prototypes */ +static void fail_exit (int status); +static void usage (int status); +static void process_flags (int argc, char **argv); + +static void fail_exit (int status) +{ + if (gr_locked) { + if (gr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", gr_dbname ())); + /* continue */ + } + } + + if (sgr_locked) { + if (sgr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sgr_dbname ())); + /* continue */ + } + } + + exit (status); +} + +static void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options]\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs ("\n", usageout); + exit (status); +} + +/* + * process_flags - parse the command line options + * + * It will not return if an error is encountered. + */ +static void process_flags (int argc, char **argv) +{ + /* + * Parse the command line options. + */ + int c; + static struct option long_options[] = { + {"help", no_argument, NULL, 'h'}, + {"root", required_argument, NULL, 'R'}, + {NULL, 0, NULL, '\0'} + }; + + while ((c = getopt_long (argc, argv, "hR:", + long_options, NULL)) != -1) { + switch (c) { + case 'h': + usage (E_SUCCESS); + /*@notreached@*/break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + default: + usage (E_USAGE); + } + } + + if (optind != argc) { + usage (E_USAGE); + } +} + +int main (int argc, char **argv) +{ + const struct group *gr; + struct group grent; + const struct sgrp *sg; + struct sgrp sgent; + + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + OPENLOG ("grpconv"); + + process_flags (argc, argv); + + if (gr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, gr_dbname ()); + fail_exit (5); + } + gr_locked = true; + if (gr_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, _("%s: cannot open %s\n"), Prog, gr_dbname ()); + fail_exit (1); + } + + if (sgr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sgr_dbname ()); + fail_exit (5); + } + sgr_locked = true; + if (sgr_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, _("%s: cannot open %s\n"), Prog, sgr_dbname ()); + fail_exit (1); + } + + /* + * Remove /etc/gshadow entries for groups not in /etc/group. + */ + (void) sgr_rewind (); + while ((sg = sgr_next ()) != NULL) { + if (gr_locate (sg->sg_name) != NULL) { + continue; + } + + if (sgr_remove (sg->sg_name) == 0) { + /* + * This shouldn't happen (the entry exists) but... + */ + fprintf (stderr, + _("%s: cannot remove entry '%s' from %s\n"), + Prog, sg->sg_name, sgr_dbname ()); + fail_exit (3); + } + } + + /* + * Update shadow group passwords if non-shadow password is not "x". + * Add any missing shadow group entries. + */ + (void) gr_rewind (); + while ((gr = gr_next ()) != NULL) { + sg = sgr_locate (gr->gr_name); + if (NULL != sg) { + /* update existing shadow group entry */ + sgent = *sg; + if (strcmp (gr->gr_passwd, SHADOW_PASSWD_STRING) != 0) + sgent.sg_passwd = gr->gr_passwd; + } else { + static char *empty = 0; + + /* add new shadow group entry */ + memset (&sgent, 0, sizeof sgent); + sgent.sg_name = gr->gr_name; + sgent.sg_passwd = gr->gr_passwd; + sgent.sg_adm = ∅ + } + /* + * XXX - sg_mem is redundant, it is currently always a copy + * of gr_mem. Very few programs actually use sg_mem, and all + * of them are in the shadow suite. Maybe this field could + * be used for something else? Any suggestions? + */ + sgent.sg_mem = gr->gr_mem; + + if (sgr_update (&sgent) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, sgr_dbname (), sgent.sg_name); + fail_exit (3); + } + /* remove password from /etc/group */ + grent = *gr; + grent.gr_passwd = SHADOW_PASSWD_STRING; /* XXX warning: const */ + if (gr_update (&grent) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, gr_dbname (), grent.gr_name); + fail_exit (3); + } + } + + if (sgr_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", sgr_dbname ())); + fail_exit (3); + } + if (gr_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", gr_dbname ())); + fail_exit (3); + } + if (sgr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sgr_dbname ())); + /* continue */ + } + if (gr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", gr_dbname ())); + /* continue */ + } + + nscd_flush_cache ("group"); + + return 0; +} +#else /* !SHADOWGRP */ +int main (int unused(argc), char **argv) +{ + fprintf (stderr, + "%s: not configured for shadow group support.\n", argv[0]); + exit (1); +} +#endif /* !SHADOWGRP */ + diff --git a/src/grpunconv.c b/src/grpunconv.c new file mode 100644 index 0000000..253f06f --- /dev/null +++ b/src/grpunconv.c @@ -0,0 +1,250 @@ +/* + * Copyright (c) 1996 , Michael Meskes + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2002 - 2006, Tomasz Kłoczko + * Copyright (c) 2008 - 2011, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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. + */ + +/* + * grpunconv - update /etc/group with information from /etc/gshadow. + * + */ + +#include <config.h> + +#ident "$Id$" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <fcntl.h> +#include <time.h> +#include <unistd.h> +#include <grp.h> +#include <getopt.h> +#include "nscd.h" +#include "prototypes.h" +/*@-exitarg@*/ +#include "exitcodes.h" +#ifdef SHADOWGRP +#include "groupio.h" +#include "sgroupio.h" +/* + * Global variables + */ +const char *Prog; + +static bool gr_locked = false; +static bool sgr_locked = false; + +/* local function prototypes */ +static void fail_exit (int status); +static void usage (int status); +static void process_flags (int argc, char **argv); + +static void fail_exit (int status) +{ + if (gr_locked) { + if (gr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", gr_dbname ())); + /* continue */ + } + } + + if (sgr_locked) { + if (sgr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sgr_dbname ())); + /* continue */ + } + } + + exit (status); +} + +static void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options]\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs ("\n", usageout); + exit (status); +} + +/* + * process_flags - parse the command line options + * + * It will not return if an error is encountered. + */ +static void process_flags (int argc, char **argv) +{ + /* + * Parse the command line options. + */ + int c; + static struct option long_options[] = { + {"help", no_argument, NULL, 'h'}, + {"root", required_argument, NULL, 'R'}, + {NULL, 0, NULL, '\0'} + }; + + while ((c = getopt_long (argc, argv, "hR:", + long_options, NULL)) != -1) { + switch (c) { + case 'h': + usage (E_SUCCESS); + /*@notreached@*/break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + default: + usage (E_USAGE); + } + } + + if (optind != argc) { + usage (E_USAGE); + } +} + +int main (int argc, char **argv) +{ + const struct group *gr; + struct group grent; + const struct sgrp *sg; + + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + OPENLOG ("grpunconv"); + + process_flags (argc, argv); + + if (sgr_file_present () == 0) { + exit (0); /* no /etc/gshadow, nothing to do */ + } + + if (gr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, gr_dbname ()); + fail_exit (5); + } + gr_locked = true; + if (gr_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), Prog, gr_dbname ()); + fail_exit (1); + } + + if (sgr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sgr_dbname ()); + fail_exit (5); + } + sgr_locked = true; + if (sgr_open (O_RDONLY) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), Prog, sgr_dbname ()); + fail_exit (1); + } + + /* + * Update group passwords if non-shadow password is "x". + */ + (void) gr_rewind (); + while ((gr = gr_next ()) != NULL) { + sg = sgr_locate (gr->gr_name); + if ( (NULL != sg) + && (strcmp (gr->gr_passwd, SHADOW_PASSWD_STRING) == 0)) { + /* add password to /etc/group */ + grent = *gr; + grent.gr_passwd = sg->sg_passwd; + if (gr_update (&grent) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, gr_dbname (), grent.gr_name); + fail_exit (3); + } + } + } + + (void) sgr_close (); /* was only open O_RDONLY */ + + if (gr_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", gr_dbname ())); + fail_exit (3); + } + + if (unlink (SGROUP_FILE) != 0) { + fprintf (stderr, + _("%s: cannot delete %s\n"), + Prog, SGROUP_FILE); + SYSLOG ((LOG_ERR, "cannot delete %s", SGROUP_FILE)); + fail_exit (3); + } + + if (gr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", gr_dbname ())); + /* continue */ + } + + if (sgr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sgr_dbname ())); + /* continue */ + } + + nscd_flush_cache ("group"); + + return 0; +} +#else /* !SHADOWGRP */ +int main (int unused(argc), char **argv) +{ + fprintf (stderr, + "%s: not configured for shadow group support.\n", argv[0]); + exit (1); +} +#endif /* !SHADOWGRP */ + diff --git a/src/id.c b/src/id.c new file mode 100644 index 0000000..4462ae0 --- /dev/null +++ b/src/id.c @@ -0,0 +1,207 @@ +/* + * Copyright (c) 1991 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2001 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2008, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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. + */ + +/* + * id - print current process user identification information + * + * Print the current process identifiers. This includes the + * UID, GID, effective-UID and effective-GID. Optionally print + * the concurrent group set if the current system supports it. + */ + +#include <config.h> + +#ident "$Id$" + +#include <grp.h> +#include <pwd.h> +#include <stdio.h> +#include <sys/types.h> +#include "defines.h" +/* local function prototypes */ +static void usage (void); + +static void usage (void) +{ +#ifdef HAVE_GETGROUPS + (void) fputs (_("Usage: id [-a]\n"), stderr); +#else + (void) fputs (_("Usage: id\n"), stderr); +#endif + exit (EXIT_FAILURE); +} + + /*ARGSUSED*/ int main (int argc, char **argv) +{ + uid_t ruid, euid; + gid_t rgid, egid; + long sys_ngroups; + +/* + * This block of declarations is particularly strained because of several + * different ways of doing concurrent groups. Old BSD systems used int for + * gid's, but short for the type passed to getgroups(). Newer systems use + * gid_t for everything. Some systems have a small and fixed NGROUPS, + * usually about 16 or 32. Others use bigger values. + */ +#ifdef HAVE_GETGROUPS + GETGROUPS_T *groups; + int ngroups; + bool aflg = 0; +#endif + struct passwd *pw; + struct group *gr; + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + /* + * Dynamically get the maximum number of groups from system, instead + * of using the symbolic constant NGROUPS_MAX. This ensures that the + * group limit is not hard coded into the binary, so it will still + * work if the system library is recompiled. + */ + sys_ngroups = sysconf (_SC_NGROUPS_MAX); +#ifdef HAVE_GETGROUPS + groups = (GETGROUPS_T *) malloc (sizeof (GETGROUPS_T) * sys_ngroups); + /* + * See if the -a flag has been given to print out the concurrent + * group set. + */ + + if (argc > 1) { + if ((argc > 2) || (strcmp (argv[1], "-a") != 0)) { + usage (); + } else { + aflg = true; + } + } +#else + if (argc > 1) { + usage (); + } +#endif + + ruid = getuid (); + euid = geteuid (); + rgid = getgid (); + egid = getegid (); + + /* + * Print out the real user ID and group ID. If the user or group + * does not exist, just give the numerical value. + */ + + pw = getpwuid (ruid); /* local, no need for xgetpwuid */ + if (NULL != pw) { + (void) printf ("UID=%lu(%s)", + (unsigned long) ruid, pw->pw_name); + } else { + (void) printf ("UID=%lu", (unsigned long) ruid); + } + + gr = getgrgid (rgid);; /* local, no need for xgetgrgid */ + if (NULL != gr) { + (void) printf (" GID=%lu(%s)", + (unsigned long) rgid, gr->gr_name); + } else { + (void) printf (" GID=%lu", (unsigned long) rgid); + } + + /* + * Print out the effective user ID and group ID if they are + * different from the real values. + */ + + if (ruid != euid) { + pw = getpwuid (euid); /* local, no need for xgetpwuid */ + if (NULL != pw) { + (void) printf (" EUID=%lu(%s)", + (unsigned long) euid, pw->pw_name); + } else { + (void) printf (" EUID=%lu", (unsigned long) euid); + } + } + if (rgid != egid) { + gr = getgrgid (egid); /* local, no need for xgetgrgid */ + if (NULL != gr) { + (void) printf (" EGID=%lu(%s)", + (unsigned long) egid, gr->gr_name); + } else { + (void) printf (" EGID=%lu", (unsigned long) egid); + } + } +#ifdef HAVE_GETGROUPS + /* + * Print out the concurrent group set if the user has requested it. + * The group numbers will be printed followed by their names. + */ + if (aflg && (ngroups = getgroups (sys_ngroups, groups)) != -1) { + int i; + + /* + * Start off the group message. It will be of the format + * + * groups=###(aaa),###(aaa),###(aaa) + * + * where "###" is a numerical value and "aaa" is the + * corresponding name for each respective numerical value. + */ + (void) puts (_(" groups=")); + for (i = 0; i < ngroups; i++) { + if (0 != i) + (void) putchar (','); + + /* local, no need for xgetgrgid */ + gr = getgrgid (groups[i]); + if (NULL != gr) { + (void) printf ("%lu(%s)", + (unsigned long) groups[i], + gr->gr_name); + } else { + (void) printf ("%lu", + (unsigned long) groups[i]); + } + } + } + free (groups); +#endif + + /* + * Finish off the line. + */ + (void) putchar ('\n'); + + return EXIT_SUCCESS; +} + diff --git a/src/lastlog.c b/src/lastlog.c new file mode 100644 index 0000000..965691d --- /dev/null +++ b/src/lastlog.c @@ -0,0 +1,428 @@ +/* + * Copyright (c) 1989 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2000 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2011, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <getopt.h> +#include <lastlog.h> +#include <pwd.h> +#include <stdio.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <time.h> +#include <assert.h> +#include "defines.h" +#include "prototypes.h" +/*@-exitarg@*/ +#include "exitcodes.h" + +/* + * Needed for MkLinux DR1/2/2.1 - J. + */ +#ifndef LASTLOG_FILE +#define LASTLOG_FILE "/var/log/lastlog" +#endif + +/* + * Global variables + */ +const char *Prog; /* Program name */ +static FILE *lastlogfile; /* lastlog file stream */ +static unsigned long umin; /* if uflg and has_umin, only display users with uid >= umin */ +static bool has_umin = false; +static unsigned long umax; /* if uflg and has_umax, only display users with uid <= umax */ +static bool has_umax = false; +static time_t seconds; /* that number of days in seconds */ +static time_t inverse_seconds; /* that number of days in seconds */ +static struct stat statbuf; /* fstat buffer for file size */ + + +static bool uflg = false; /* print only an user of range of users */ +static bool tflg = false; /* print is restricted to most recent days */ +static bool bflg = false; /* print excludes most recent days */ +static bool Cflg = false; /* clear record for user */ +static bool Sflg = false; /* set record for user */ + +#define NOW (time ((time_t *) 0)) + +static /*@noreturn@*/void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options]\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -b, --before DAYS print only lastlog records older than DAYS\n"), usageout); + (void) fputs (_(" -C, --clear clear lastlog record of an user (usable only with -u)\n"), usageout); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs (_(" -S, --set set lastlog record to current time (usable only with -u)\n"), usageout); + (void) fputs (_(" -t, --time DAYS print only lastlog records more recent than DAYS\n"), usageout); + (void) fputs (_(" -u, --user LOGIN print lastlog record of the specified LOGIN\n"), usageout); + (void) fputs ("\n", usageout); + exit (status); +} + +static void print_one (/*@null@*/const struct passwd *pw) +{ + static bool once = false; + char *cp; + struct tm *tm; + time_t ll_time; + off_t offset; + struct lastlog ll; + +#ifdef HAVE_STRFTIME + char ptime[80]; +#endif + + if (NULL == pw) { + return; + } + + + offset = (off_t) pw->pw_uid * sizeof (ll); + if (offset + sizeof (ll) <= statbuf.st_size) { + /* fseeko errors are not really relevant for us. */ + int err = fseeko (lastlogfile, offset, SEEK_SET); + assert (0 == err); + /* lastlog is a sparse file. Even if no entries were + * entered for this user, which should be able to get the + * empty entry in this case. + */ + if (fread ((char *) &ll, sizeof (ll), 1, lastlogfile) != 1) { + fprintf (stderr, + _("%s: Failed to get the entry for UID %lu\n"), + Prog, (unsigned long int)pw->pw_uid); + exit (EXIT_FAILURE); + } + } else { + /* Outsize of the lastlog file. + * Behave as if there were a missing entry (same behavior + * as if we were reading an non existing entry in the + * sparse lastlog file). + */ + memzero (&ll, sizeof (ll)); + } + + /* Filter out entries that do not match with the -t or -b options */ + if (tflg && ((NOW - ll.ll_time) > seconds)) { + return; + } + + if (bflg && ((NOW - ll.ll_time) < inverse_seconds)) { + return; + } + + /* Print the header only once */ + if (!once) { +#ifdef HAVE_LL_HOST + puts (_("Username Port From Latest")); +#else + puts (_("Username Port Latest")); +#endif + once = true; + } + + ll_time = ll.ll_time; + tm = localtime (&ll_time); +#ifdef HAVE_STRFTIME + strftime (ptime, sizeof (ptime), "%a %b %e %H:%M:%S %z %Y", tm); + cp = ptime; +#else + cp = asctime (tm); + cp[24] = '\0'; +#endif + + if (ll.ll_time == (time_t) 0) { + cp = _("**Never logged in**\0"); + } + +#ifdef HAVE_LL_HOST + printf ("%-16s %-8.8s %-16.16s %s\n", + pw->pw_name, ll.ll_line, ll.ll_host, cp); +#else + printf ("%-16s\t%-8.8s %s\n", + pw->pw_name, ll.ll_line, cp); +#endif +} + +static void print (void) +{ + const struct passwd *pwent; + if (uflg && has_umin && has_umax && (umin == umax)) { + print_one (getpwuid ((uid_t)umin)); + } else { + setpwent (); + while ( (pwent = getpwent ()) != NULL ) { + if ( uflg + && ( (has_umin && (pwent->pw_uid < (uid_t)umin)) + || (has_umax && (pwent->pw_uid > (uid_t)umax)))) { + continue; + } + print_one (pwent); + } + endpwent (); + } +} + +static void update_one (/*@null@*/const struct passwd *pw) +{ + off_t offset; + struct lastlog ll; + int err; + + if (NULL == pw) { + return; + } + + offset = (off_t) pw->pw_uid * sizeof (ll); + /* fseeko errors are not really relevant for us. */ + err = fseeko (lastlogfile, offset, SEEK_SET); + assert (0 == err); + + memzero (&ll, sizeof (ll)); + + if (Sflg) { + ll.ll_time = NOW; +#ifdef HAVE_LL_HOST + strcpy (ll.ll_host, "localhost"); +#endif + strcpy (ll.ll_line, "lastlog"); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ACCT_UNLOCK, Prog, + "clearing-lastlog", + pw->pw_name, (unsigned int) pw->pw_uid, SHADOW_AUDIT_SUCCESS); +#endif + } +#ifdef WITH_AUDIT + else { + audit_logger (AUDIT_ACCT_UNLOCK, Prog, + "refreshing-lastlog", + pw->pw_name, (unsigned int) pw->pw_uid, SHADOW_AUDIT_SUCCESS); + } +#endif + + if (fwrite (&ll, sizeof(ll), 1, lastlogfile) != 1) { + fprintf (stderr, + _("%s: Failed to update the entry for UID %lu\n"), + Prog, (unsigned long int)pw->pw_uid); + exit (EXIT_FAILURE); + } +} + +static void update (void) +{ + const struct passwd *pwent; + + if (!uflg) /* safety measure */ + return; + + if (has_umin && has_umax && (umin == umax)) { + update_one (getpwuid ((uid_t)umin)); + } else { + setpwent (); + while ( (pwent = getpwent ()) != NULL ) { + if ((has_umin && (pwent->pw_uid < (uid_t)umin)) + || (has_umax && (pwent->pw_uid > (uid_t)umax))) { + continue; + } + update_one (pwent); + } + endpwent (); + } + + if (fflush (lastlogfile) != 0 || fsync (fileno (lastlogfile)) != 0) { + fprintf (stderr, + _("%s: Failed to update the lastlog file\n"), + Prog); + exit (EXIT_FAILURE); + } +} + +int main (int argc, char **argv) +{ + /* + * Get the program name. The program name is used as a prefix to + * most error messages. + */ + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + +#ifdef WITH_AUDIT + audit_help_open (); +#endif + + { + int c; + static struct option const longopts[] = { + {"before", required_argument, NULL, 'b'}, + {"clear", no_argument, NULL, 'C'}, + {"help", no_argument, NULL, 'h'}, + {"root", required_argument, NULL, 'R'}, + {"set", no_argument, NULL, 'S'}, + {"time", required_argument, NULL, 't'}, + {"user", required_argument, NULL, 'u'}, + {NULL, 0, NULL, '\0'} + }; + + while ((c = getopt_long (argc, argv, "b:ChR:St:u:", longopts, + NULL)) != -1) { + switch (c) { + case 'b': + { + unsigned long inverse_days; + if (getulong (optarg, &inverse_days) == 0) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + exit (EXIT_FAILURE); + } + inverse_seconds = (time_t) inverse_days * DAY; + bflg = true; + break; + } + case 'C': + { + Cflg = true; + break; + } + case 'h': + usage (EXIT_SUCCESS); + /*@notreached@*/break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + case 'S': + { + Sflg = true; + break; + } + case 't': + { + unsigned long days; + if (getulong (optarg, &days) == 0) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + exit (EXIT_FAILURE); + } + seconds = (time_t) days * DAY; + tflg = true; + break; + } + case 'u': + { + const struct passwd *pwent; + /* + * The user can be: + * - a login name + * - numerical + * - a numerical login ID + * - a range (-x, x-, x-y) + */ + uflg = true; + /* local, no need for xgetpwnam */ + pwent = getpwnam (optarg); + if (NULL != pwent) { + umin = (unsigned long) pwent->pw_uid; + has_umin = true; + umax = umin; + has_umax = true; + } else { + if (getrange (optarg, + &umin, &has_umin, + &umax, &has_umax) == 0) { + fprintf (stderr, + _("%s: Unknown user or range: %s\n"), + Prog, optarg); + exit (EXIT_FAILURE); + } + } + break; + } + default: + usage (EXIT_FAILURE); + /*@notreached@*/break; + } + } + if (argc > optind) { + fprintf (stderr, + _("%s: unexpected argument: %s\n"), + Prog, argv[optind]); + usage (EXIT_FAILURE); + } + if (Cflg && Sflg) { + fprintf (stderr, + _("%s: Option -C cannot be used together with option -S\n"), + Prog); + usage (EXIT_FAILURE); + } + if ((Cflg || Sflg) && !uflg) { + fprintf (stderr, + _("%s: Options -C and -S require option -u to specify the user\n"), + Prog); + usage (EXIT_FAILURE); + } + } + + lastlogfile = fopen (LASTLOG_FILE, (Cflg || Sflg)?"r+":"r"); + if (NULL == lastlogfile) { + perror (LASTLOG_FILE); + exit (EXIT_FAILURE); + } + + /* Get the lastlog size */ + if (fstat (fileno (lastlogfile), &statbuf) != 0) { + fprintf (stderr, + _("%s: Cannot get the size of %s: %s\n"), + Prog, LASTLOG_FILE, strerror (errno)); + exit (EXIT_FAILURE); + } + + if (Cflg || Sflg) + update (); + else + print (); + + (void) fclose (lastlogfile); + + return EXIT_SUCCESS; +} + diff --git a/src/login.c b/src/login.c new file mode 100644 index 0000000..2d2e704 --- /dev/null +++ b/src/login.c @@ -0,0 +1,1339 @@ +/* + * Copyright (c) 1989 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2001, Marek Michałkiewicz + * Copyright (c) 2001 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2012, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <errno.h> +#include <grp.h> +#ifndef USE_PAM +#include <lastlog.h> +#endif /* !USE_PAM */ +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <sys/stat.h> +#include <sys/ioctl.h> +#include <assert.h> +#include "defines.h" +#include "faillog.h" +#include "failure.h" +#include "getdef.h" +#include "prototypes.h" +#include "pwauth.h" +/*@-exitarg@*/ +#include "exitcodes.h" + +#ifdef USE_PAM +#include "pam_defs.h" + +static pam_handle_t *pamh = NULL; + +#define PAM_FAIL_CHECK if (retcode != PAM_SUCCESS) { \ + fprintf(stderr,"\n%s\n",pam_strerror(pamh, retcode)); \ + SYSLOG((LOG_ERR,"%s",pam_strerror(pamh, retcode))); \ + (void) pam_end(pamh, retcode); \ + exit(1); \ + } +#define PAM_END { retcode = pam_close_session(pamh,0); \ + (void) pam_end(pamh,retcode); } + +#endif /* USE_PAM */ + +#ifndef USE_PAM +/* + * Needed for MkLinux DR1/2/2.1 - J. + */ +#ifndef LASTLOG_FILE +#define LASTLOG_FILE "/var/log/lastlog" +#endif +#endif /* !USE_PAM */ + +/* + * Global variables + */ +const char *Prog; + +static const char *hostname = ""; +static /*@null@*/ /*@only@*/char *username = NULL; +static int reason = PW_LOGIN; + +#ifndef USE_PAM +static struct lastlog ll; +#endif /* !USE_PAM */ +static bool pflg = false; +static bool fflg = false; + +#ifdef RLOGIN +static bool rflg = false; +#else /* RLOGIN */ +#define rflg false +#endif /* !RLOGIN */ +static bool hflg = false; +static bool preauth_flag = false; + +static bool amroot; +static char tmsg[256]; + +/* + * External identifiers. + */ + +extern char **newenvp; +extern size_t newenvc; +extern char **environ; + +#ifndef ALARM +#define ALARM 60 +#endif + +#ifndef RETRIES +#define RETRIES 3 +#endif + +/* local function prototypes */ +static void usage (void); +static void setup_tty (void); +static void process_flags (int argc, char *const *argv); +static /*@observer@*/const char *get_failent_user (/*@returned@*/const char *user); +static void update_utmp (const char *user, + const char *tty, + const char *host, + /*@null@*/const struct utmp *utent); + +#ifndef USE_PAM +static struct faillog faillog; + +static void bad_time_notify (void); +static void check_nologin (bool login_to_root); +#else +static void get_pam_user (char **ptr_pam_user); +#endif + +static void init_env (void); +static RETSIGTYPE alarm_handler (int); + +/* + * usage - print login command usage and exit + * + * login [ name ] + * login -r hostname (for rlogind) + * login -h hostname (for telnetd, etc.) + * login -f name (for pre-authenticated login: datakit, xterm, etc.) + */ +static void usage (void) +{ + fprintf (stderr, _("Usage: %s [-p] [name]\n"), Prog); + if (!amroot) { + exit (1); + } + fprintf (stderr, _(" %s [-p] [-h host] [-f name]\n"), Prog); +#ifdef RLOGIN + fprintf (stderr, _(" %s [-p] -r host\n"), Prog); +#endif /* RLOGIN */ + exit (1); +} + +static void setup_tty (void) +{ + TERMIO termio; + + if (GTTY (0, &termio) == 0) { /* get terminal characteristics */ + int erasechar; + int killchar; + + /* + * Add your favorite terminal modes here ... + */ + termio.c_lflag |= ISIG | ICANON | ECHO | ECHOE; + termio.c_iflag |= ICRNL; + +#if defined(ECHOKE) && defined(ECHOCTL) + termio.c_lflag |= ECHOKE | ECHOCTL; +#endif +#if defined(ECHOPRT) && defined(NOFLSH) && defined(TOSTOP) + termio.c_lflag &= ~(ECHOPRT | NOFLSH | TOSTOP); +#endif +#ifdef ONLCR + termio.c_oflag |= ONLCR; +#endif + + /* leave these values unchanged if not specified in login.defs */ + erasechar = getdef_num ("ERASECHAR", (int) termio.c_cc[VERASE]); + killchar = getdef_num ("KILLCHAR", (int) termio.c_cc[VKILL]); + termio.c_cc[VERASE] = (cc_t) erasechar; + termio.c_cc[VKILL] = (cc_t) killchar; + /* Make sure the values were valid. + * getdef_num cannot validate this. + */ + if (erasechar != (int) termio.c_cc[VERASE]) { + fprintf (stderr, + _("configuration error - cannot parse %s value: '%d'"), + "ERASECHAR", erasechar); + exit (1); + } + if (killchar != (int) termio.c_cc[VKILL]) { + fprintf (stderr, + _("configuration error - cannot parse %s value: '%d'"), + "KILLCHAR", killchar); + exit (1); + } + + /* + * ttymon invocation prefers this, but these settings + * won't come into effect after the first username login + */ + (void) STTY (0, &termio); + } +} + + +#ifndef USE_PAM +/* + * Tell the user that this is not the right time to login at this tty + */ +static void bad_time_notify (void) +{ + (void) puts (_("Invalid login time")); + (void) fflush (stdout); +} + +static void check_nologin (bool login_to_root) +{ + char *fname; + + /* + * Check to see if system is turned off for non-root users. + * This would be useful to prevent users from logging in + * during system maintenance. We make sure the message comes + * out for root so she knows to remove the file if she's + * forgotten about it ... + */ + fname = getdef_str ("NOLOGINS_FILE"); + if ((NULL != fname) && (access (fname, F_OK) == 0)) { + FILE *nlfp; + + /* + * Cat the file if it can be opened, otherwise just + * print a default message + */ + nlfp = fopen (fname, "r"); + if (NULL != nlfp) { + int c; + while ((c = getc (nlfp)) != EOF) { + if (c == '\n') { + (void) putchar ('\r'); + } + + (void) putchar (c); + } + (void) fflush (stdout); + (void) fclose (nlfp); + } else { + (void) puts (_("\nSystem closed for routine maintenance")); + } + /* + * Non-root users must exit. Root gets the message, but + * gets to login. + */ + + if (!login_to_root) { + closelog (); + exit (0); + } + (void) puts (_("\n[Disconnect bypassed -- root login allowed.]")); + } +} +#endif /* !USE_PAM */ + +static void process_flags (int argc, char *const *argv) +{ + int arg; + int flag; + + /* + * Check the flags for proper form. Every argument starting with + * "-" must be exactly two characters long. This closes all the + * clever rlogin, telnet, and getty holes. + */ + for (arg = 1; arg < argc; arg++) { + if (argv[arg][0] == '-' && strlen (argv[arg]) > 2) { + usage (); + } + if (strcmp(argv[arg], "--") == 0) { + break; /* stop checking on a "--" */ + } + } + + /* + * Process options. + */ + while ((flag = getopt (argc, argv, "d:fh:pr:")) != EOF) { + switch (flag) { + case 'd': + /* "-d device" ignored for compatibility */ + break; + case 'f': + fflg = true; + break; + case 'h': + hflg = true; + hostname = optarg; + reason = PW_TELNET; + break; +#ifdef RLOGIN + case 'r': + rflg = true; + hostname = optarg; + reason = PW_RLOGIN; + break; +#endif /* RLOGIN */ + case 'p': + pflg = true; + break; + default: + usage (); + } + } + +#ifdef RLOGIN + /* + * Neither -h nor -f should be combined with -r. + */ + + if (rflg && (hflg || fflg)) { + usage (); + } +#endif /* RLOGIN */ + + /* + * Allow authentication bypass only if real UID is zero. + */ + + if ((rflg || fflg || hflg) && !amroot) { + fprintf (stderr, _("%s: Permission denied.\n"), Prog); + exit (1); + } + + /* + * Get the user name. + */ + if (optind < argc) { + assert (NULL == username); + username = xstrdup (argv[optind]); + strzero (argv[optind]); + ++optind; + } + +#ifdef RLOGIN + if (rflg && (NULL != username)) { + usage (); + } +#endif /* RLOGIN */ + if (fflg && (NULL == username)) { + usage (); + } + +} + + +static void init_env (void) +{ +#ifndef USE_PAM + char *cp; +#endif + char *tmp; + + tmp = getenv ("LANG"); + if (NULL != tmp) { + addenv ("LANG", tmp); + } + + /* + * Add the timezone environmental variable so that time functions + * work correctly. + */ + tmp = getenv ("TZ"); + if (NULL != tmp) { + addenv ("TZ", tmp); + } +#ifndef USE_PAM + else { + cp = getdef_str ("ENV_TZ"); + if (NULL != cp) { + addenv (('/' == *cp) ? tz (cp) : cp, NULL); + } + } +#endif /* !USE_PAM */ + /* + * Add the clock frequency so that profiling commands work + * correctly. + */ + tmp = getenv ("HZ"); + if (NULL != tmp) { + addenv ("HZ", tmp); + } +#ifndef USE_PAM + else { + cp = getdef_str ("ENV_HZ"); + if (NULL != cp) { + addenv (cp, NULL); + } + } +#endif /* !USE_PAM */ +} + + +static RETSIGTYPE alarm_handler (unused int sig) +{ + write (STDERR_FILENO, tmsg, strlen (tmsg)); + _exit (0); +} + +#ifdef USE_PAM +/* + * get_pam_user - Get the username according to PAM + * + * ptr_pam_user shall point to a malloc'ed string (or NULL). + */ +static void get_pam_user (char **ptr_pam_user) +{ + int retcode; + void *ptr_user; + + assert (NULL != ptr_pam_user); + + retcode = pam_get_item (pamh, PAM_USER, (const void **)&ptr_user); + PAM_FAIL_CHECK; + + if (NULL != *ptr_pam_user) { + free (*ptr_pam_user); + } + if (NULL != ptr_user) { + *ptr_pam_user = xstrdup ((const char *)ptr_user); + } else { + *ptr_pam_user = NULL; + } +} +#endif + +/* + * get_failent_user - Return a string that can be used to log failure + * from an user. + * + * This will be either the user argument, or "UNKNOWN". + * + * It is quite common to mistyped the password for username, and passwords + * should not be logged. + */ +static /*@observer@*/const char *get_failent_user (/*@returned@*/const char *user) +{ + const char *failent_user = "UNKNOWN"; + bool log_unkfail_enab = getdef_bool("LOG_UNKFAIL_ENAB"); + + if ((NULL != user) && ('\0' != user[0])) { + if ( log_unkfail_enab + || (getpwnam (user) != NULL)) { + failent_user = user; + } + } + + return failent_user; +} + +/* + * update_utmp - Update or create an utmp entry in utmp, wtmp, utmpw, and + * wtmpx + * + * utent should be the utmp entry returned by get_current_utmp (or + * NULL). + */ +static void update_utmp (const char *user, + const char *tty, + const char *host, + /*@null@*/const struct utmp *utent) +{ + struct utmp *ut = prepare_utmp (user, tty, host, utent); +#ifdef USE_UTMPX + struct utmpx *utx = prepare_utmpx (user, tty, host, utent); +#endif /* USE_UTMPX */ + + (void) setutmp (ut); /* make entry in the utmp & wtmp files */ + free (ut); + +#ifdef USE_UTMPX + (void) setutmpx (utx); /* make entry in the utmpx & wtmpx files */ + free (utx); +#endif /* USE_UTMPX */ +} + +/* + * login - create a new login session for a user + * + * login is typically called by getty as the second step of a + * new user session. getty is responsible for setting the line + * characteristics to a reasonable set of values and getting + * the name of the user to be logged in. login may also be + * called to create a new user session on a pty for a variety + * of reasons, such as X servers or network logins. + * + * the flags which login supports are + * + * -p - preserve the environment + * -r - perform autologin protocol for rlogin + * -f - do not perform authentication, user is preauthenticated + * -h - the name of the remote host + */ +int main (int argc, char **argv) +{ + const char *tmptty; + char tty[BUFSIZ]; + +#ifdef RLOGIN + char term[128] = ""; +#endif /* RLOGIN */ +#if defined(HAVE_STRFTIME) && !defined(USE_PAM) + char ptime[80]; +#endif + unsigned int delay; + unsigned int retries; + bool subroot = false; +#ifndef USE_PAM + bool is_console; +#endif + int err; + unsigned int timeout; + const char *cp; + const char *tmp; + char fromhost[512]; + struct passwd *pwd = NULL; + char **envp = environ; + const char *failent_user; + /*@null@*/struct utmp *utent; + +#ifdef USE_PAM + int retcode; + pid_t child; + char *pam_user = NULL; +#else + struct spwd *spwd = NULL; +#endif + /* + * Some quick initialization. + */ + + sanitize_env (); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + initenv (); + + amroot = (getuid () == 0); + Prog = Basename (argv[0]); + + if (geteuid() != 0) { + fprintf (stderr, _("%s: Cannot possibly work without effective root\n"), Prog); + exit (1); + } + + process_flags (argc, argv); + + if ((isatty (0) == 0) || (isatty (1) == 0) || (isatty (2) == 0)) { + exit (1); /* must be a terminal */ + } + + utent = get_current_utmp (); + /* + * Be picky if run by normal users (possible if installed setuid + * root), but not if run by root. This way it still allows logins + * even if your getty is broken, or if something corrupts utmp, + * but users must "exec login" which will use the existing utmp + * entry (will not overwrite remote hostname). --marekm + */ + if (!amroot && (NULL == utent)) { + (void) puts (_("No utmp entry. You must exec \"login\" from the lowest level \"sh\"")); + exit (1); + } + /* NOTE: utent might be NULL afterwards */ + + tmptty = ttyname (0); + if (NULL == tmptty) { + tmptty = "UNKNOWN"; + } + STRFCPY (tty, tmptty); + +#ifndef USE_PAM + is_console = console (tty); +#endif + + if (rflg || hflg) { + /* + * Add remote hostname to the environment. I think + * (not sure) I saw it once on Irix. --marekm + */ + addenv ("REMOTEHOST", hostname); + } + if (fflg) { + preauth_flag = true; + } + if (hflg) { + reason = PW_RLOGIN; + } +#ifdef RLOGIN + if (rflg) { + assert (NULL == username); + username = xmalloc (USER_NAME_MAX_LENGTH + 1); + username[USER_NAME_MAX_LENGTH] = '\0'; + if (do_rlogin (hostname, username, USER_NAME_MAX_LENGTH, term, sizeof term)) { + preauth_flag = true; + } else { + free (username); + username = NULL; + } + } +#endif /* RLOGIN */ + + OPENLOG ("login"); + + setup_tty (); + +#ifndef USE_PAM + (void) umask (getdef_num ("UMASK", GETDEF_DEFAULT_UMASK)); + + { + /* + * Use the ULIMIT in the login.defs file, and if + * there isn't one, use the default value. The + * user may have one for themselves, but otherwise, + * just take what you get. + */ + long limit = getdef_long ("ULIMIT", -1L); + + if (limit != -1) { + set_filesize_limit (limit); + } + } + +#endif + /* + * The entire environment will be preserved if the -p flag + * is used. + */ + if (pflg) { + while (NULL != *envp) { /* add inherited environment, */ + addenv (*envp, NULL); /* some variables change later */ + envp++; + } + } + +#ifdef RLOGIN + if (term[0] != '\0') { + addenv ("TERM", term); + } else +#endif /* RLOGIN */ + { + /* preserve TERM from getty */ + if (!pflg) { + tmp = getenv ("TERM"); + if (NULL != tmp) { + addenv ("TERM", tmp); + } + } + } + + init_env (); + + if (optind < argc) { /* now set command line variables */ + set_env (argc - optind, &argv[optind]); + } + + if (rflg || hflg) { + cp = hostname; +#ifdef HAVE_STRUCT_UTMP_UT_HOST + } else if ((NULL != utent) && ('\0' != utent->ut_host[0])) { + cp = utent->ut_host; +#endif /* HAVE_STRUCT_UTMP_UT_HOST */ + } else { + cp = ""; + } + + if ('\0' != *cp) { + snprintf (fromhost, sizeof fromhost, + " on '%.100s' from '%.200s'", tty, cp); + } else { + snprintf (fromhost, sizeof fromhost, + " on '%.100s'", tty); + } + + top: + /* only allow ALARM sec. for login */ + timeout = getdef_unum ("LOGIN_TIMEOUT", ALARM); + snprintf (tmsg, sizeof tmsg, + _("\nLogin timed out after %u seconds.\n"), timeout); + (void) signal (SIGALRM, alarm_handler); + if (timeout > 0) { + (void) alarm (timeout); + } + + environ = newenvp; /* make new environment active */ + delay = getdef_unum ("FAIL_DELAY", 1); + retries = getdef_unum ("LOGIN_RETRIES", RETRIES); + +#ifdef USE_PAM + retcode = pam_start ("login", username, &conv, &pamh); + if (retcode != PAM_SUCCESS) { + fprintf (stderr, + _("login: PAM Failure, aborting: %s\n"), + pam_strerror (pamh, retcode)); + SYSLOG ((LOG_ERR, "Couldn't initialize PAM: %s", + pam_strerror (pamh, retcode))); + exit (99); + } + + /* + * hostname & tty are either set to NULL or their correct values, + * depending on how much we know. We also set PAM's fail delay to + * ours. + * + * PAM_RHOST and PAM_TTY are used for authentication, only use + * information coming from login or from the caller (e.g. no utmp) + */ + retcode = pam_set_item (pamh, PAM_RHOST, hostname); + PAM_FAIL_CHECK; + retcode = pam_set_item (pamh, PAM_TTY, tty); + PAM_FAIL_CHECK; +#ifdef HAS_PAM_FAIL_DELAY + retcode = pam_fail_delay (pamh, 1000000 * delay); + PAM_FAIL_CHECK; +#endif + /* if fflg, then the user has already been authenticated */ + if (!fflg) { + unsigned int failcount = 0; + char hostn[256]; + char loginprompt[256]; /* That's one hell of a prompt :) */ + + /* Make the login prompt look like we want it */ + if (gethostname (hostn, sizeof (hostn)) == 0) { + snprintf (loginprompt, + sizeof (loginprompt), + _("%s login: "), hostn); + } else { + strncpy (loginprompt, _("login: "), + sizeof (loginprompt)); + } + + retcode = pam_set_item (pamh, PAM_USER_PROMPT, loginprompt); + PAM_FAIL_CHECK; + + /* if we didn't get a user on the command line, + set it to NULL */ + get_pam_user (&pam_user); + if ((NULL != pam_user) && ('\0' == pam_user[0])) { + retcode = pam_set_item (pamh, PAM_USER, NULL); + PAM_FAIL_CHECK; + } + + /* + * There may be better ways to deal with some of + * these conditions, but at least this way I don't + * think we'll be giving away information. Perhaps + * someday we can trust that all PAM modules will + * pay attention to failure count and get rid of + * MAX_LOGIN_TRIES? + */ + failcount = 0; + while (true) { + bool failed = false; + + failcount++; +#ifdef HAS_PAM_FAIL_DELAY + if (delay > 0) { + retcode = pam_fail_delay(pamh, 1000000*delay); + PAM_FAIL_CHECK; + } +#endif + + retcode = pam_authenticate (pamh, 0); + + get_pam_user (&pam_user); + failent_user = get_failent_user (pam_user); + + if (retcode == PAM_MAXTRIES) { + SYSLOG ((LOG_NOTICE, + "TOO MANY LOGIN TRIES (%u)%s FOR '%s'", + failcount, fromhost, failent_user)); + fprintf (stderr, + _("Maximum number of tries exceeded (%u)\n"), + failcount); + PAM_END; + exit(0); + } else if (retcode == PAM_ABORT) { + /* Serious problems, quit now */ + (void) fputs (_("login: abort requested by PAM\n"), stderr); + SYSLOG ((LOG_ERR,"PAM_ABORT returned from pam_authenticate()")); + PAM_END; + exit(99); + } else if (retcode != PAM_SUCCESS) { + SYSLOG ((LOG_NOTICE,"FAILED LOGIN (%u)%s FOR '%s', %s", + failcount, fromhost, failent_user, + pam_strerror (pamh, retcode))); + failed = true; + } + + if (!failed) { + break; + } + +#ifdef WITH_AUDIT + audit_fd = audit_open (); + audit_log_acct_message (audit_fd, + AUDIT_USER_LOGIN, + NULL, /* Prog. name */ + "login", + failent_user, + AUDIT_NO_ID, + hostname, + NULL, /* addr */ + tty, + 0); /* result */ + close (audit_fd); +#endif /* WITH_AUDIT */ + + (void) puts (""); + (void) puts (_("Login incorrect")); + + if (failcount >= retries) { + SYSLOG ((LOG_NOTICE, + "TOO MANY LOGIN TRIES (%u)%s FOR '%s'", + failcount, fromhost, failent_user)); + fprintf (stderr, + _("Maximum number of tries exceeded (%u)\n"), + failcount); + PAM_END; + exit(0); + } + + /* + * Let's give it another go around. + * Even if a username was given on the command + * line, prompt again for the username. + */ + retcode = pam_set_item (pamh, PAM_USER, NULL); + PAM_FAIL_CHECK; + } + + /* We don't get here unless they were authenticated above */ + (void) alarm (0); + } + + /* Check the account validity */ + retcode = pam_acct_mgmt (pamh, 0); + if (retcode == PAM_NEW_AUTHTOK_REQD) { + retcode = pam_chauthtok (pamh, PAM_CHANGE_EXPIRED_AUTHTOK); + } + PAM_FAIL_CHECK; + + /* Open the PAM session */ + get_pam_user (&pam_user); + retcode = pam_open_session (pamh, hushed (pam_user) ? PAM_SILENT : 0); + PAM_FAIL_CHECK; + + /* Grab the user information out of the password file for future usage + * First get the username that we are actually using, though. + * + * From now on, we will discard changes of the user (PAM_USER) by + * PAM APIs. + */ + get_pam_user (&pam_user); + if (NULL != username) { + free (username); + } + username = xstrdup (pam_user); + failent_user = get_failent_user (username); + + pwd = xgetpwnam (username); + if (NULL == pwd) { + SYSLOG ((LOG_ERR, "cannot find user %s", failent_user)); + fprintf (stderr, + _("Cannot find user (%s)\n"), + username); + exit (1); + } + + /* This set up the process credential (group) and initialize the + * supplementary group access list. + * This has to be done before pam_setcred + */ + if (setup_groups (pwd) != 0) { + exit (1); + } + + retcode = pam_setcred (pamh, PAM_ESTABLISH_CRED); + PAM_FAIL_CHECK; + /* NOTE: If pam_setcred changes PAM_USER, this will not be taken + * into account. + */ + +#else /* ! USE_PAM */ + while (true) { /* repeatedly get login/password pairs */ + bool failed; + /* user_passwd is always a pointer to this constant string + * or a passwd or shadow password that will be memzero by + * pw_free / spw_free. + * Do not free() user_passwd. */ + const char *user_passwd = "!"; + + /* Do some cleanup to avoid keeping entries we do not need + * anymore. */ + if (NULL != pwd) { + pw_free (pwd); + pwd = NULL; + } + if (NULL != spwd) { + spw_free (spwd); + spwd = NULL; + } + + failed = false; /* haven't failed authentication yet */ + if (NULL == username) { /* need to get a login id */ + if (subroot) { + closelog (); + exit (1); + } + preauth_flag = false; + username = xmalloc (USER_NAME_MAX_LENGTH + 1); + username[USER_NAME_MAX_LENGTH] = '\0'; + login_prompt (_("\n%s login: "), username, USER_NAME_MAX_LENGTH); + + if ('\0' == username[0]) { + /* Prompt for a new login */ + free (username); + username = NULL; + continue; + } + } + /* Get the username to be used to log failures */ + failent_user = get_failent_user (username); + + pwd = xgetpwnam (username); + if (NULL == pwd) { + preauth_flag = false; + failed = true; + } else { + user_passwd = pwd->pw_passwd; + /* + * If the encrypted password begins with a "!", + * the account is locked and the user cannot + * login, even if they have been + * "pre-authenticated." + */ + if ( ('!' == user_passwd[0]) + || ('*' == user_passwd[0])) { + failed = true; + } + } + + if (strcmp (user_passwd, SHADOW_PASSWD_STRING) == 0) { + spwd = xgetspnam (username); + if (NULL != spwd) { + user_passwd = spwd->sp_pwdp; + } else { + /* The user exists in passwd, but not in + * shadow. SHADOW_PASSWD_STRING indicates + * that the password shall be in shadow. + */ + SYSLOG ((LOG_WARN, + "no shadow password for '%s'%s", + username, fromhost)); + } + } + + /* + * The -r and -f flags provide a name which has already + * been authenticated by some server. + */ + if (preauth_flag) { + goto auth_ok; + } + + if (pw_auth (user_passwd, username, reason, (char *) 0) == 0) { + goto auth_ok; + } + + SYSLOG ((LOG_WARN, "invalid password for '%s' %s", + failent_user, fromhost)); + failed = true; + + auth_ok: + /* + * This is the point where all authenticated users wind up. + * If you reach this far, your password has been + * authenticated and so on. + */ + if ( !failed + && (NULL != pwd) + && (0 == pwd->pw_uid) + && !is_console) { + SYSLOG ((LOG_CRIT, "ILLEGAL ROOT LOGIN %s", fromhost)); + failed = true; + } + if ( !failed + && !login_access (username, ('\0' != *hostname) ? hostname : tty)) { + SYSLOG ((LOG_WARN, "LOGIN '%s' REFUSED %s", + username, fromhost)); + failed = true; + } + if ( (NULL != pwd) + && getdef_bool ("FAILLOG_ENAB") + && !failcheck (pwd->pw_uid, &faillog, failed)) { + SYSLOG ((LOG_CRIT, + "exceeded failure limit for '%s' %s", + username, fromhost)); + failed = true; + } + if (!failed) { + break; + } + + /* don't log non-existent users */ + if ((NULL != pwd) && getdef_bool ("FAILLOG_ENAB")) { + failure (pwd->pw_uid, tty, &faillog); + } + if (getdef_str ("FTMP_FILE") != NULL) { +#ifdef USE_UTMPX + struct utmpx *failent = + prepare_utmpx (failent_user, + tty, + /* FIXME: or fromhost? */hostname, + utent); +#else /* !USE_UTMPX */ + struct utmp *failent = + prepare_utmp (failent_user, + tty, + hostname, + utent); +#endif /* !USE_UTMPX */ + failtmp (failent_user, failent); + free (failent); + } + + retries--; + if (retries <= 0) { + SYSLOG ((LOG_CRIT, "REPEATED login failures%s", + fromhost)); + } + + /* + * If this was a passwordless account and we get here, login + * was denied (securetty, faillog, etc.). There was no + * password prompt, so do it now (will always fail - the bad + * guys won't see that the passwordless account exists at + * all). --marekm + */ + if (user_passwd[0] == '\0') { + pw_auth ("!", username, reason, (char *) 0); + } + + /* + * Authentication of this user failed. + * The username must be confirmed in the next try. + */ + free (username); + username = NULL; + + /* + * Wait a while (a la SVR4 /usr/bin/login) before attempting + * to login the user again. If the earlier alarm occurs + * before the sleep() below completes, login will exit. + */ + if (delay > 0) { + (void) sleep (delay); + } + + (void) puts (_("Login incorrect")); + + /* allow only one attempt with -r or -f */ + if (rflg || fflg || (retries <= 0)) { + closelog (); + exit (1); + } + } /* while (true) */ +#endif /* ! USE_PAM */ + assert (NULL != username); + assert (NULL != pwd); + + (void) alarm (0); /* turn off alarm clock */ + +#ifndef USE_PAM /* PAM does this */ + /* + * porttime checks moved here, after the user has been + * authenticated. now prints a message, as suggested + * by Ivan Nejgebauer <ian@unsux.ns.ac.yu>. --marekm + */ + if ( getdef_bool ("PORTTIME_CHECKS_ENAB") + && !isttytime (username, tty, time ((time_t *) 0))) { + SYSLOG ((LOG_WARN, "invalid login time for '%s'%s", + username, fromhost)); + closelog (); + bad_time_notify (); + exit (1); + } + + check_nologin (pwd->pw_uid == 0); +#endif + + if (getenv ("IFS")) { /* don't export user IFS ... */ + addenv ("IFS= \t\n", NULL); /* ... instead, set a safe IFS */ + } + + if (pwd->pw_shell[0] == '*') { /* subsystem root */ + pwd->pw_shell++; /* skip the '*' */ + subsystem (pwd); /* figure out what to execute */ + subroot = true; /* say I was here again */ + endpwent (); /* close all of the file which were */ + endgrent (); /* open in the original rooted file */ + endspent (); /* system. they will be re-opened */ +#ifdef SHADOWGRP + endsgent (); /* in the new rooted file system */ +#endif + goto top; /* go do all this all over again */ + } + +#ifdef WITH_AUDIT + audit_fd = audit_open (); + audit_log_acct_message (audit_fd, + AUDIT_USER_LOGIN, + NULL, /* Prog. name */ + "login", + username, + AUDIT_NO_ID, + hostname, + NULL, /* addr */ + tty, + 1); /* result */ + close (audit_fd); +#endif /* WITH_AUDIT */ + +#ifndef USE_PAM /* pam_lastlog handles this */ + if (getdef_bool ("LASTLOG_ENAB")) { /* give last login and log this one */ + dolastlog (&ll, pwd, tty, hostname); + } +#endif + +#ifndef USE_PAM /* PAM handles this as well */ + /* + * Have to do this while we still have root privileges, otherwise we + * don't have access to /etc/shadow. + */ + if (NULL != spwd) { /* check for age of password */ + if (expire (pwd, spwd)) { + /* The user updated her password, get the new + * entries. + * Use the x variants because we need to keep the + * entry for a long time, and there might be other + * getxxyy in between. + */ + pw_free (pwd); + pwd = xgetpwnam (username); + if (NULL == pwd) { + SYSLOG ((LOG_ERR, + "cannot find user %s after update of expired password", + username)); + exit (1); + } + spw_free (spwd); + spwd = xgetspnam (username); + } + } + setup_limits (pwd); /* nice, ulimit etc. */ +#endif /* ! USE_PAM */ + chown_tty (pwd); + +#ifdef USE_PAM + /* + * We must fork before setuid() because we need to call + * pam_close_session() as root. + */ + (void) signal (SIGINT, SIG_IGN); + child = fork (); + if (child < 0) { + /* error in fork() */ + fprintf (stderr, _("%s: failure forking: %s"), + Prog, strerror (errno)); + PAM_END; + exit (0); + } else if (child != 0) { + /* + * parent - wait for child to finish, then cleanup + * session + */ + wait (NULL); + PAM_END; + exit (0); + } + /* child */ +#endif + + /* If we were init, we need to start a new session */ + if (getppid() == 1) { + setsid(); + if (ioctl(0, TIOCSCTTY, 1) != 0) { + fprintf (stderr, _("TIOCSCTTY failed on %s"), tty); + } + } + + /* + * The utmp entry needs to be updated to indicate the new status + * of the session, the new PID and SID. + */ + update_utmp (username, tty, hostname, utent); + + /* The pwd and spwd entries for the user have been copied. + * + * Close all the files so that unauthorized access won't occur. + */ + endpwent (); /* stop access to password file */ + endgrent (); /* stop access to group file */ + endspent (); /* stop access to shadow passwd file */ +#ifdef SHADOWGRP + endsgent (); /* stop access to shadow group file */ +#endif + + /* Drop root privileges */ +#ifndef USE_PAM + if (setup_uid_gid (pwd, is_console)) +#else + /* The group privileges were already dropped. + * See setup_groups() above. + */ + if (change_uid (pwd)) +#endif + { + exit (1); + } + + setup_env (pwd); /* set env vars, cd to the home dir */ + +#ifdef USE_PAM + { + const char *const *env; + + env = (const char *const *) pam_getenvlist (pamh); + while ((NULL != env) && (NULL != *env)) { + addenv (*env, NULL); + env++; + } + } +#endif + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + if (!hushed (username)) { + addenv ("HUSHLOGIN=FALSE", NULL); + /* + * pam_unix, pam_mail and pam_lastlog should take care of + * this + */ +#ifndef USE_PAM + motd (); /* print the message of the day */ + if ( getdef_bool ("FAILLOG_ENAB") + && (0 != faillog.fail_cnt)) { + failprint (&faillog); + /* Reset the lockout times if logged in */ + if ( (0 != faillog.fail_max) + && (faillog.fail_cnt >= faillog.fail_max)) { + (void) puts (_("Warning: login re-enabled after temporary lockout.")); + SYSLOG ((LOG_WARN, + "login '%s' re-enabled after temporary lockout (%d failures)", + username, (int) faillog.fail_cnt)); + } + } + if ( getdef_bool ("LASTLOG_ENAB") + && (ll.ll_time != 0)) { + time_t ll_time = ll.ll_time; + +#ifdef HAVE_STRFTIME + (void) strftime (ptime, sizeof (ptime), + "%a %b %e %H:%M:%S %z %Y", + localtime (&ll_time)); + printf (_("Last login: %s on %s"), + ptime, ll.ll_line); +#else + printf (_("Last login: %.19s on %s"), + ctime (&ll_time), ll.ll_line); +#endif +#ifdef HAVE_LL_HOST /* __linux__ || SUN4 */ + if ('\0' != ll.ll_host[0]) { + printf (_(" from %.*s"), + (int) sizeof ll.ll_host, ll.ll_host); + } +#endif + printf (".\n"); + } + agecheck (spwd); + + mailcheck (); /* report on the status of mail */ +#endif /* !USE_PAM */ + } else { + addenv ("HUSHLOGIN=TRUE", NULL); + } + + ttytype (tty); + + (void) signal (SIGQUIT, SIG_DFL); /* default quit signal */ + (void) signal (SIGTERM, SIG_DFL); /* default terminate signal */ + (void) signal (SIGALRM, SIG_DFL); /* default alarm signal */ + (void) signal (SIGHUP, SIG_DFL); /* added this. --marekm */ + (void) signal (SIGINT, SIG_DFL); /* default interrupt signal */ + + if (0 == pwd->pw_uid) { + SYSLOG ((LOG_NOTICE, "ROOT LOGIN %s", fromhost)); + } else if (getdef_bool ("LOG_OK_LOGINS")) { + SYSLOG ((LOG_INFO, "'%s' logged in %s", username, fromhost)); + } + closelog (); + tmp = getdef_str ("FAKE_SHELL"); + if (NULL != tmp) { + err = shell (tmp, pwd->pw_shell, newenvp); /* fake shell */ + } else { + /* exec the shell finally */ + err = shell (pwd->pw_shell, (char *) 0, newenvp); + } + + return ((err == ENOENT) ? E_CMD_NOTFOUND : E_CMD_NOEXEC); +} + diff --git a/src/login_nopam.c b/src/login_nopam.c new file mode 100644 index 0000000..68a3775 --- /dev/null +++ b/src/login_nopam.c @@ -0,0 +1,336 @@ +/* Taken from logdaemon-5.0, only minimal changes. --marekm */ + +/************************************************************************ +* Copyright 1995 by Wietse Venema. All rights reserved. Individual files +* may be covered by other copyrights (as noted in the file itself.) +* +* This material was originally written and compiled by Wietse Venema at +* Eindhoven University of Technology, The Netherlands, in 1990, 1991, +* 1992, 1993, 1994 and 1995. +* +* Redistribution and use in source and binary forms are permitted +* provided that this entire copyright notice is duplicated in all such +* copies. +* +* This software is provided "as is" and without any expressed or implied +* warranties, including, without limitation, the implied warranties of +* merchantibility and fitness for any particular purpose. +************************************************************************/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#ifndef USE_PAM +#ident "$Id$" + +#include "prototypes.h" + /* + * This module implements a simple but effective form of login access + * control based on login names and on host (or domain) names, internet + * addresses (or network numbers), or on terminal line names in case of + * non-networked logins. Diagnostics are reported through syslog(3). + * + * Author: Wietse Venema, Eindhoven University of Technology, The Netherlands. + */ +#include <sys/types.h> +#include <stdio.h> +#include <syslog.h> +#include <ctype.h> +#ifdef HAVE_NETDB_H +#include <netdb.h> +#endif +#include <grp.h> +#ifdef PRIMARY_GROUP_MATCH +#include <pwd.h> +#endif +#include <errno.h> +#include <string.h> +#include <unistd.h> +#include <stdlib.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> /* for inet_ntoa() */ + +#if !defined(MAXHOSTNAMELEN) || (MAXHOSTNAMELEN < 64) +#undef MAXHOSTNAMELEN +#define MAXHOSTNAMELEN 256 +#endif + + /* Path name of the access control file. */ +#ifndef TABLE +#define TABLE "/etc/login.access" +#endif + +/* Delimiters for fields and for lists of users, ttys or hosts. */ +static char fs[] = ":"; /* field separator */ +static char sep[] = ", \t"; /* list-element separator */ + +static bool list_match (char *list, const char *item, bool (*match_fn) (const char *, const char *)); +static bool user_match (const char *tok, const char *string); +static bool from_match (const char *tok, const char *string); +static bool string_match (const char *tok, const char *string); +static const char *resolve_hostname (const char *string); + +/* login_access - match username/group and host/tty with access control file */ +int login_access (const char *user, const char *from) +{ + FILE *fp; + char line[BUFSIZ]; + char *perm; /* becomes permission field */ + char *users; /* becomes list of login names */ + char *froms; /* becomes list of terminals or hosts */ + bool match = false; + + /* + * Process the table one line at a time and stop at the first match. + * Blank lines and lines that begin with a '#' character are ignored. + * Non-comment lines are broken at the ':' character. All fields are + * mandatory. The first field should be a "+" or "-" character. A + * non-existing table means no access control. + */ + fp = fopen (TABLE, "r"); + if (NULL != fp) { + int lineno = 0; /* for diagnostics */ + while ( !match + && (fgets (line, (int) sizeof (line), fp) == line)) { + int end; + lineno++; + end = (int) strlen (line) - 1; + if (line[end] != '\n') { + SYSLOG ((LOG_ERR, + "%s: line %d: missing newline or line too long", + TABLE, lineno)); + continue; + } + if (line[0] == '#') { + continue; /* comment line */ + } + while (end > 0 && isspace (line[end - 1])) { + end--; + } + line[end] = '\0'; /* strip trailing whitespace */ + if (line[0] == '\0') { /* skip blank lines */ + continue; + } + if ( ((perm = strtok (line, fs)) == NULL) + || ((users = strtok ((char *) 0, fs)) == NULL) + || ((froms = strtok ((char *) 0, fs)) == NULL) + || (strtok ((char *) 0, fs) != NULL)) { + SYSLOG ((LOG_ERR, + "%s: line %d: bad field count", + TABLE, lineno)); + continue; + } + if (perm[0] != '+' && perm[0] != '-') { + SYSLOG ((LOG_ERR, + "%s: line %d: bad first field", + TABLE, lineno)); + continue; + } + match = ( list_match (froms, from, from_match) + && list_match (users, user, user_match)); + } + (void) fclose (fp); + } else if (errno != ENOENT) { + int err = errno; + SYSLOG ((LOG_ERR, "cannot open %s: %s", TABLE, strerror (err))); + } + return (!match || (line[0] == '+'))?1:0; +} + +/* list_match - match an item against a list of tokens with exceptions */ +static bool list_match (char *list, const char *item, bool (*match_fn) (const char *, const char*)) +{ + char *tok; + bool match = false; + + /* + * Process tokens one at a time. We have exhausted all possible matches + * when we reach an "EXCEPT" token or the end of the list. If we do find + * a match, look for an "EXCEPT" list and recurse to determine whether + * the match is affected by any exceptions. + */ + for (tok = strtok (list, sep); tok != NULL; tok = strtok ((char *) 0, sep)) { + if (strcasecmp (tok, "EXCEPT") == 0) { /* EXCEPT: give up */ + break; + } + match = (*match_fn) (tok, item); + if (match) { + break; + } + } + + /* Process exceptions to matches. */ + if (match) { + while ( ((tok = strtok ((char *) 0, sep)) != NULL) + && (strcasecmp (tok, "EXCEPT") != 0)) + /* VOID */ ; + if (tok == 0 || !list_match ((char *) 0, item, match_fn)) { + return (match); + } + } + return false; +} + +/* myhostname - figure out local machine name */ +static char *myhostname (void) +{ + static char name[MAXHOSTNAMELEN + 1] = ""; + + if (name[0] == '\0') { + gethostname (name, sizeof (name)); + name[MAXHOSTNAMELEN] = '\0'; + } + return (name); +} + +#if HAVE_INNETGR +/* netgroup_match - match group against machine or user */ +static bool +netgroup_match (const char *group, const char *machine, const char *user) +{ + static char *mydomain = (char *)0; + + if (mydomain == (char *)0) { + static char domain[MAXHOSTNAMELEN + 1]; + + getdomainname (domain, MAXHOSTNAMELEN); + mydomain = domain; + } + + return (innetgr (group, machine, user, mydomain) != 0); +} +#endif + +/* user_match - match a username against one token */ +static bool user_match (const char *tok, const char *string) +{ + struct group *group; + +#ifdef PRIMARY_GROUP_MATCH + struct passwd *userinf; +#endif + char *at; + + /* + * If a token has the magic value "ALL" the match always succeeds. + * Otherwise, return true if the token fully matches the username, or if + * the token is a group that contains the username. + */ + at = strchr (tok + 1, '@'); + if (NULL != at) { /* split user@host pattern */ + *at = '\0'; + return ( user_match (tok, string) + && from_match (at + 1, myhostname ())); +#if HAVE_INNETGR + } else if (tok[0] == '@') { /* netgroup */ + return (netgroup_match (tok + 1, (char *) 0, string)); +#endif + } else if (string_match (tok, string)) { /* ALL or exact match */ + return true; + /* local, no need for xgetgrnam */ + } else if ((group = getgrnam (tok)) != NULL) { /* try group membership */ + int i; + for (i = 0; NULL != group->gr_mem[i]; i++) { + if (strcasecmp (string, group->gr_mem[i]) == 0) { + return true; + } + } +#ifdef PRIMARY_GROUP_MATCH + /* + * If the string is an user whose initial GID matches the token, + * accept it. May avoid excessively long lines in /etc/group. + * Radu-Adrian Feurdean <raf@licj.soroscj.ro> + * + * XXX - disabled by default for now. Need to verify that + * getpwnam() doesn't have some nasty side effects. --marekm + */ + /* local, no need for xgetpwnam */ + userinf = getpwnam (string); + if (NULL != userinf) { + if (userinf->pw_gid == group->gr_gid) { + return true; + } + } +#endif + } + return false; +} + +static const char *resolve_hostname (const char *string) +{ + /* + * Resolve hostname to numeric IP address, as suggested + * by Dave Hagewood <admin@arrowweb.com>. --marekm + */ + struct hostent *hp; + + hp = gethostbyname (string); + if (NULL != hp) { + return inet_ntoa (*((struct in_addr *) *(hp->h_addr_list))); + } + + SYSLOG ((LOG_ERR, "%s - unknown host", string)); + return string; +} + +/* from_match - match a host or tty against a list of tokens */ + +static bool from_match (const char *tok, const char *string) +{ + size_t tok_len; + + /* + * If a token has the magic value "ALL" the match always succeeds. Return + * true if the token fully matches the string. If the token is a domain + * name, return true if it matches the last fields of the string. If the + * token has the magic value "LOCAL", return true if the string does not + * contain a "." character. If the token is a network number, return true + * if it matches the head of the string. + */ +#if HAVE_INNETGR + if (tok[0] == '@') { /* netgroup */ + return (netgroup_match (tok + 1, string, (char *) 0)); + } else +#endif + if (string_match (tok, string)) { /* ALL or exact match */ + return true; + } else if (tok[0] == '.') { /* domain: match last fields */ + size_t str_len; + str_len = strlen (string); + tok_len = strlen (tok); + if ( (str_len > tok_len) + && (strcasecmp (tok, string + str_len - tok_len) == 0)) { + return true; + } + } else if (strcasecmp (tok, "LOCAL") == 0) { /* local: no dots */ + if (strchr (string, '.') == NULL) { + return true; + } + } else if ( (tok[(tok_len = strlen (tok)) - 1] == '.') /* network */ + && (strncmp (tok, resolve_hostname (string), tok_len) == 0)) { + return true; + } + return false; +} + +/* string_match - match a string against one token */ +static bool string_match (const char *tok, const char *string) +{ + + /* + * If the token has the magic value "ALL" the match always succeeds. + * Otherwise, return true if the token fully matches the string. + */ + if (strcasecmp (tok, "ALL") == 0) { /* all: always matches */ + return true; + } else if (strcasecmp (tok, string) == 0) { /* try exact match */ + return true; + } + return false; +} + +#else /* !USE_PAM */ +extern int errno; /* warning: ANSI C forbids an empty source file */ +#endif /* !USE_PAM */ diff --git a/src/logoutd.c b/src/logoutd.c new file mode 100644 index 0000000..1503a74 --- /dev/null +++ b/src/logoutd.c @@ -0,0 +1,299 @@ +/* + * Copyright (c) 1991 - 1993, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2001 - 2006, Tomasz Kłoczko + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <fcntl.h> +#include <signal.h> +#include <stdio.h> +#include <sys/stat.h> +#include <sys/types.h> +#include "defines.h" +#include "prototypes.h" +/* + * Global variables + */ +const char *Prog; + +#ifndef DEFAULT_HUP_MESG +#define DEFAULT_HUP_MESG _("login time exceeded\n\n") +#endif + +#ifndef HUP_MESG_FILE +#define HUP_MESG_FILE "/etc/logoutd.mesg" +#endif + +/* local function prototypes */ +#ifdef USE_UTMPX +static int check_login (const struct utmpx *ut); +#else /* !USE_UTMPX */ +static int check_login (const struct utmp *ut); +#endif /* !USE_UTMPX */ +static void send_mesg_to_tty (int tty_fd); + +/* + * check_login - check if user (struct utmpx/utmp) allowed to stay logged in + */ +#ifdef USE_UTMPX +static int check_login (const struct utmpx *ut) +#else /* !USE_UTMPX */ +static int check_login (const struct utmp *ut) +#endif /* !USE_UTMPX */ +{ + char user[sizeof (ut->ut_user) + 1]; + time_t now; + + /* + * ut_user may not have the terminating NUL. + */ + strncpy (user, ut->ut_user, sizeof (ut->ut_user)); + user[sizeof (ut->ut_user)] = '\0'; + + (void) time (&now); + + /* + * Check if they are allowed to be logged in right now. + */ + if (!isttytime (user, ut->ut_line, now)) { + return 0; + } + return 1; +} + + +static void send_mesg_to_tty (int tty_fd) +{ + TERMIO oldt, newt; + FILE *mesg_file, *tty_file; + bool is_tty; + + tty_file = fdopen (tty_fd, "w"); + if (NULL == tty_file) { + return; + } + + is_tty = (GTTY (tty_fd, &oldt) == 0); + if (is_tty) { + /* Suggested by Ivan Nejgebauar <ian@unsux.ns.ac.yu>: + set OPOST before writing the message. */ + newt = oldt; + newt.c_oflag |= OPOST; + STTY (tty_fd, &newt); + } + + mesg_file = fopen (HUP_MESG_FILE, "r"); + if (NULL != mesg_file) { + int c; + while ((c = getc (mesg_file)) != EOF) { + if (c == '\n') { + putc ('\r', tty_file); + } + putc (c, tty_file); + } + fclose (mesg_file); + } else { + fputs (DEFAULT_HUP_MESG, tty_file); + } + fflush (tty_file); + fclose (tty_file); + + if (is_tty) { + STTY (tty_fd, &oldt); + } +} + + +/* + * logoutd - logout daemon to enforce /etc/porttime file policy + * + * logoutd is started at system boot time and enforces the login + * time and port restrictions specified in /etc/porttime. The + * utmpx/utmp file is periodically scanned and offending users are logged + * off from the system. + */ +int main (int argc, char **argv) +{ + int i; + int status; + pid_t pid; + +#ifdef USE_UTMPX + struct utmpx *ut; +#else /* !USE_UTMPX */ + struct utmp *ut; +#endif /* !USE_UTMPX */ + char user[sizeof (ut->ut_user) + 1]; /* terminating NUL */ + char tty_name[sizeof (ut->ut_line) + 6]; /* /dev/ + NUL */ + int tty_fd; + + if (1 != argc) { + (void) fputs (_("Usage: logoutd\n"), stderr); + } + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + +#ifndef DEBUG + for (i = 0; close (i) == 0; i++); + + setpgrp (); + + /* + * Put this process in the background. + */ + pid = fork (); + if (pid > 0) { + /* parent */ + exit (EXIT_SUCCESS); + } else if (pid < 0) { + /* error */ + perror ("fork"); + exit (EXIT_FAILURE); + } +#endif /* !DEBUG */ + + /* + * Start syslogging everything + */ + Prog = Basename (argv[0]); + + OPENLOG ("logoutd"); + + /* + * Scan the utmpx/utmp file once per minute looking for users that + * are not supposed to still be logged in. + */ + while (true) { + + /* + * Attempt to re-open the utmpx/utmp file. The file is only + * open while it is being used. + */ +#ifdef USE_UTMPX + setutxent (); +#else /* !USE_UTMPX */ + setutent (); +#endif /* !USE_UTMPX */ + + /* + * Read all of the entries in the utmpx/utmp file. The entries + * for login sessions will be checked to see if the user + * is permitted to be signed on at this time. + */ +#ifdef USE_UTMPX + while ((ut = getutxent ()) != NULL) +#else /* !USE_UTMPX */ + while ((ut = getutent ()) != NULL) +#endif /* !USE_UTMPX */ + { + if (ut->ut_type != USER_PROCESS) { + continue; + } + if (ut->ut_user[0] == '\0') { + continue; + } + if (check_login (ut)) { + continue; + } + + /* + * Put the rest of this in a child process. This + * keeps the scan from waiting on other ports to die. + */ + + pid = fork (); + if (pid > 0) { + /* parent */ + continue; + } else if (pid < 0) { + /* failed - give up until the next scan */ + break; + } + /* child */ + + if (strncmp (ut->ut_line, "/dev/", 5) != 0) { + strcpy (tty_name, "/dev/"); + } else { + tty_name[0] = '\0'; + } + + strcat (tty_name, ut->ut_line); +#ifndef O_NOCTTY +#define O_NOCTTY 0 +#endif + tty_fd = + open (tty_name, O_WRONLY | O_NDELAY | O_NOCTTY); + if (tty_fd != -1) { + send_mesg_to_tty (tty_fd); + close (tty_fd); + sleep (10); + } + + if (ut->ut_pid > 1) { + kill (-ut->ut_pid, SIGHUP); + sleep (10); + kill (-ut->ut_pid, SIGKILL); + } + + strncpy (user, ut->ut_user, sizeof (user) - 1); + user[sizeof (user) - 1] = '\0'; + + SYSLOG ((LOG_NOTICE, + "logged off user '%s' on '%s'", user, + tty_name)); + + /* + * This child has done all it can, drop dead. + */ + exit (EXIT_SUCCESS); + } + +#ifdef USE_UTMPX + endutxent (); +#else /* !USE_UTMPX */ + endutent (); +#endif /* !USE_UTMPX */ + +#ifndef DEBUG + sleep (60); +#endif + /* + * Reap any dead babies ... + */ + while (wait (&status) != -1); + } + + return EXIT_FAILURE; +} + diff --git a/src/newgidmap.c b/src/newgidmap.c new file mode 100644 index 0000000..b1e3351 --- /dev/null +++ b/src/newgidmap.c @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2013 Eric Biederman + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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 <stdio.h> +#include <string.h> +#include <errno.h> +#include <stdbool.h> +#include <stdlib.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include "defines.h" +#include "prototypes.h" +#include "subordinateio.h" +#include "idmapping.h" + +/* + * Global variables + */ +const char *Prog; + +static bool verify_range(struct passwd *pw, struct map_range *range) +{ + /* An empty range is invalid */ + if (range->count == 0) + return false; + + /* Test /etc/subgid */ + if (have_sub_gids(pw->pw_name, range->lower, range->count)) + return true; + + /* Allow a process to map its own gid */ + if ((range->count == 1) && (pw->pw_gid == range->lower)) + return true; + + return false; +} + +static void verify_ranges(struct passwd *pw, int ranges, + struct map_range *mappings) +{ + struct map_range *mapping; + int idx; + + mapping = mappings; + for (idx = 0; idx < ranges; idx++, mapping++) { + if (!verify_range(pw, mapping)) { + fprintf(stderr, _( "%s: gid range [%lu-%lu) -> [%lu-%lu) not allowed\n"), + Prog, + mapping->upper, + mapping->upper + mapping->count, + mapping->lower, + mapping->lower + mapping->count); + exit(EXIT_FAILURE); + } + } +} + +static void usage(void) +{ + fprintf(stderr, _("usage: %s <pid> <gid> <lowergid> <count> [ <gid> <lowergid> <count> ] ... \n"), Prog); + exit(EXIT_FAILURE); +} + +/* + * newgidmap - Set the gid_map for the specified process + */ +int main(int argc, char **argv) +{ + char proc_dir_name[32]; + char *target_str; + pid_t target, parent; + int proc_dir_fd; + int ranges; + struct map_range *mappings; + struct stat st; + struct passwd *pw; + int written; + + Prog = Basename (argv[0]); + + /* + * The valid syntax are + * newgidmap target_pid + */ + if (argc < 2) + usage(); + + /* Find the process that needs its user namespace + * gid mapping set. + */ + target_str = argv[1]; + if (!get_pid(target_str, &target)) + usage(); + + /* max string length is 6 + 10 + 1 + 1 = 18, allocate 32 bytes */ + written = snprintf(proc_dir_name, sizeof(proc_dir_name), "/proc/%u/", + target); + if ((written <= 0) || (written >= sizeof(proc_dir_name))) { + fprintf(stderr, "%s: snprintf of proc path failed: %s\n", + Prog, strerror(errno)); + } + + proc_dir_fd = open(proc_dir_name, O_DIRECTORY); + if (proc_dir_fd < 0) { + fprintf(stderr, _("%s: Could not open proc directory for target %u\n"), + Prog, target); + return EXIT_FAILURE; + } + + /* Who am i? */ + pw = get_my_pwent (); + if (NULL == pw) { + fprintf (stderr, + _("%s: Cannot determine your user name.\n"), + Prog); + SYSLOG ((LOG_WARN, "Cannot determine the user name of the caller (UID %lu)", + (unsigned long) getuid ())); + return EXIT_FAILURE; + } + + /* Get the effective uid and effective gid of the target process */ + if (fstat(proc_dir_fd, &st) < 0) { + fprintf(stderr, _("%s: Could not stat directory for target %u\n"), + Prog, target); + return EXIT_FAILURE; + } + + /* Verify real user and real group matches the password entry + * and the effective user and group of the program whose + * mappings we have been asked to set. + */ + if ((getuid() != pw->pw_uid) || + (getgid() != pw->pw_gid) || + (pw->pw_uid != st.st_uid) || + (pw->pw_gid != st.st_gid)) { + fprintf(stderr, _( "%s: Target %u is owned by a different user: uid:%lu pw_uid:%lu st_uid:%lu, gid:%lu pw_gid:%lu st_gid:%lu\n" ), + Prog, target, + (unsigned long int)getuid(), (unsigned long int)pw->pw_uid, (unsigned long int)st.st_uid, + (unsigned long int)getgid(), (unsigned long int)pw->pw_gid, (unsigned long int)st.st_gid); + return EXIT_FAILURE; + } + + if (!sub_gid_open(O_RDONLY)) { + return EXIT_FAILURE; + } + + ranges = ((argc - 2) + 2) / 3; + mappings = get_map_ranges(ranges, argc - 2, argv + 2); + if (!mappings) + usage(); + + verify_ranges(pw, ranges, mappings); + + write_mapping(proc_dir_fd, ranges, mappings, "gid_map"); + sub_gid_close(); + + return EXIT_SUCCESS; +} diff --git a/src/newgrp.c b/src/newgrp.c new file mode 100644 index 0000000..b8d3ddc --- /dev/null +++ b/src/newgrp.c @@ -0,0 +1,853 @@ +/* + * Copyright (c) 1990 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2001 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2008, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <errno.h> +#include <grp.h> +#include <pwd.h> +#include <stdio.h> +#include <assert.h> +#include "defines.h" +#include "getdef.h" +#include "prototypes.h" +/*@-exitarg@*/ +#include "exitcodes.h" + +/* + * Global variables + */ +const char *Prog; + +extern char **newenvp; +extern char **environ; + +#ifdef HAVE_SETGROUPS +static int ngroups; +static /*@null@*/ /*@only@*/GETGROUPS_T *grouplist; +#endif + +static bool is_newgrp; + +#ifdef WITH_AUDIT +static char audit_buf[80]; +#endif + +/* local function prototypes */ +static void usage (void); +static void check_perms (const struct group *grp, + struct passwd *pwd, + const char *groupname); +static void syslog_sg (const char *name, const char *group); + +/* + * usage - print command usage message + */ +static void usage (void) +{ + if (is_newgrp) { + (void) fputs (_("Usage: newgrp [-] [group]\n"), stderr); + } else { + (void) fputs (_("Usage: sg group [[-c] command]\n"), stderr); + } +} + +/* + * find_matching_group - search all groups of a given group id for + * membership of a given username + */ +static /*@null@*/struct group *find_matching_group (const char *name, gid_t gid) +{ + struct group *gr; + char **look; + bool notfound = true; + + setgrent (); + while ((gr = getgrent ()) != NULL) { + if (gr->gr_gid != gid) { + continue; + } + + /* + * A group with matching GID was found. + * Test for membership of 'name'. + */ + look = gr->gr_mem; + while ((NULL != *look) && notfound) { + notfound = (strcmp (*look, name) != 0); + look++; + } + if (!notfound) { + break; + } + } + endgrent (); + return gr; +} + +/* + * check_perms - check if the user is allowed to switch to this group + * + * If needed, the user will be authenticated. + * + * It will not return if the user could not be authenticated. + */ +static void check_perms (const struct group *grp, + struct passwd *pwd, + const char *groupname) +{ + bool needspasswd = false; + struct spwd *spwd; + char *cp; + const char *cpasswd; + + /* + * see if she is a member of this group (i.e. in the list of + * members of the group, or if the group is her primary group). + * + * If she isn't a member, she needs to provide the group password. + * If there is no group password, she will be denied access + * anyway. + * + */ + if ( (grp->gr_gid != pwd->pw_gid) + && !is_on_list (grp->gr_mem, pwd->pw_name)) { + needspasswd = true; + } + + /* + * If she does not have either a shadowed password, or a regular + * password, and the group has a password, she needs to give the + * group password. + */ + spwd = xgetspnam (pwd->pw_name); + if (NULL != spwd) { + pwd->pw_passwd = spwd->sp_pwdp; + } + + if ((pwd->pw_passwd[0] == '\0') && (grp->gr_passwd[0] != '\0')) { + needspasswd = true; + } + + /* + * Now I see about letting her into the group she requested. If she + * is the root user, I'll let her in without having to prompt for + * the password. Otherwise I ask for a password if she flunked one + * of the tests above. + */ + if ((getuid () != 0) && needspasswd) { + /* + * get the password from her, and set the salt for + * the decryption from the group file. + */ + cp = getpass (_("Password: ")); + if (NULL == cp) { + goto failure; + } + + /* + * encrypt the key she gave us using the salt from the + * password in the group file. The result of this encryption + * must match the previously encrypted value in the file. + */ + cpasswd = pw_encrypt (cp, grp->gr_passwd); + strzero (cp); + + if (NULL == cpasswd) { + fprintf (stderr, + _("%s: failed to crypt password with previous salt: %s\n"), + Prog, strerror (errno)); + SYSLOG ((LOG_INFO, + "Failed to crypt password with previous salt of group '%s'", + groupname)); + goto failure; + } + + if (grp->gr_passwd[0] == '\0' || + strcmp (cpasswd, grp->gr_passwd) != 0) { +#ifdef WITH_AUDIT + snprintf (audit_buf, sizeof(audit_buf), + "authentication new-gid=%lu", + (unsigned long) grp->gr_gid); + audit_logger (AUDIT_GRP_AUTH, Prog, + audit_buf, NULL, + (unsigned int) getuid (), 0); +#endif + SYSLOG ((LOG_INFO, + "Invalid password for group '%s' from '%s'", + groupname, pwd->pw_name)); + (void) sleep (1); + (void) fputs (_("Invalid password.\n"), stderr); + goto failure; + } +#ifdef WITH_AUDIT + snprintf (audit_buf, sizeof(audit_buf), + "authentication new-gid=%lu", + (unsigned long) grp->gr_gid); + audit_logger (AUDIT_GRP_AUTH, Prog, + audit_buf, NULL, + (unsigned int) getuid (), 1); +#endif + } + + return; + +failure: + /* The closelog is probably unnecessary, but it does no + * harm. -- JWP + */ + closelog (); +#ifdef WITH_AUDIT + if (groupname) { + snprintf (audit_buf, sizeof(audit_buf), + "changing new-group=%s", groupname); + audit_logger (AUDIT_CHGRP_ID, Prog, + audit_buf, NULL, + (unsigned int) getuid (), 0); + } else { + audit_logger (AUDIT_CHGRP_ID, Prog, + "changing", NULL, + (unsigned int) getuid (), 0); + } +#endif + exit (EXIT_FAILURE); +} + +#ifdef USE_SYSLOG +/* + * syslog_sg - log the change of group to syslog + * + * The loggout will also be logged when the user will quit the + * sg/newgrp session. + */ +static void syslog_sg (const char *name, const char *group) +{ + const char *loginname = getlogin (); + const char *tty = ttyname (0); + char *free_login = NULL, *free_tty = NULL; + + if (loginname != NULL) { + free_login = xstrdup (loginname); + loginname = free_login; + } + if (tty != NULL) { + free_tty = xstrdup (tty); + tty = free_tty; + } + + if (loginname == NULL) { + loginname = "???"; + } + if (tty == NULL) { + tty = "???"; + } else if (strncmp (tty, "/dev/", 5) == 0) { + tty += 5; + } + SYSLOG ((LOG_INFO, + "user '%s' (login '%s' on %s) switched to group '%s'", + name, loginname, tty, group)); +#ifdef USE_PAM + /* + * We want to fork and exec the new shell in the child, leaving the + * parent waiting to log the session close. + * + * The parent must ignore signals generated from the console + * (SIGINT, SIGQUIT, SIGHUP) which might make the parent terminate + * before its child. When bash is exec'ed as the subshell, it + * generates a new process group id for itself, and consequently + * only SIGHUP, which is sent to all process groups in the session, + * can reach the parent. However, since arbitrary programs can be + * specified as login shells, there is no such guarantee in general. + * For the same reason, we must also ignore stop signals generated + * from the console (SIGTSTP, SIGTTIN, and SIGTTOU) in order to + * avoid any possibility of the parent being stopped when it + * receives SIGCHLD from the terminating subshell. -- JWP + */ + { + pid_t child; + + /* Ignore these signals. The signal handlers will later be + * restored to the default handlers. */ + (void) signal (SIGINT, SIG_IGN); + (void) signal (SIGQUIT, SIG_IGN); + (void) signal (SIGHUP, SIG_IGN); + (void) signal (SIGTSTP, SIG_IGN); + (void) signal (SIGTTIN, SIG_IGN); + (void) signal (SIGTTOU, SIG_IGN); + child = fork (); + if ((pid_t)-1 == child) { + /* error in fork() */ + fprintf (stderr, _("%s: failure forking: %s\n"), + is_newgrp ? "newgrp" : "sg", strerror (errno)); +#ifdef WITH_AUDIT + if (group) { + snprintf (audit_buf, sizeof(audit_buf), + "changing new-group=%s", group); + audit_logger (AUDIT_CHGRP_ID, Prog, + audit_buf, NULL, + (unsigned int) getuid (), 0); + } else { + audit_logger (AUDIT_CHGRP_ID, Prog, + "changing", NULL, + (unsigned int) getuid (), 0); + } +#endif + exit (EXIT_FAILURE); + } else if (child != 0) { + /* parent - wait for child to finish, then log session close */ + int cst = 0; + gid_t gid = getgid(); + struct group *grp = getgrgid (gid); + pid_t pid; + + do { + errno = 0; + pid = waitpid (child, &cst, WUNTRACED); + if ((pid == child) && (WIFSTOPPED (cst) != 0)) { + /* The child (shell) was suspended. + * Suspend sg/newgrp. */ + kill (getpid (), SIGSTOP); + /* wake child when resumed */ + kill (child, SIGCONT); + } + } while ( ((pid == child) && (WIFSTOPPED (cst) != 0)) + || ((pid != child) && (errno == EINTR))); + /* local, no need for xgetgrgid */ + if (NULL != grp) { + SYSLOG ((LOG_INFO, + "user '%s' (login '%s' on %s) returned to group '%s'", + name, loginname, tty, grp->gr_name)); + } else { + SYSLOG ((LOG_INFO, + "user '%s' (login '%s' on %s) returned to group '%lu'", + name, loginname, tty, + (unsigned long) gid)); + /* Either the user's passwd entry has a + * GID that does not match with any group, + * or the group was deleted while the user + * was in a newgrp session.*/ + SYSLOG ((LOG_WARN, + "unknown GID '%lu' used by user '%s'", + (unsigned long) gid, name)); + } + closelog (); + exit ((0 != WIFEXITED (cst)) ? WEXITSTATUS (cst) + : WTERMSIG (cst) + 128); + } + + /* child - restore signals to their default state */ + (void) signal (SIGINT, SIG_DFL); + (void) signal (SIGQUIT, SIG_DFL); + (void) signal (SIGHUP, SIG_DFL); + (void) signal (SIGTSTP, SIG_DFL); + (void) signal (SIGTTIN, SIG_DFL); + (void) signal (SIGTTOU, SIG_DFL); + } +#endif /* USE_PAM */ + free(free_login); + free(free_tty); +} +#endif /* USE_SYSLOG */ + +/* + * newgrp - change the invokers current real and effective group id + */ +int main (int argc, char **argv) +{ + bool initflag = false; + int i; + bool cflag = false; + int err = 0; + gid_t gid; + char *cp; + const char *name, *prog; + char *group = NULL; + char *command = NULL; + char **envp = environ; + struct passwd *pwd; + /*@null@*/struct group *grp; + +#ifdef SHADOWGRP + struct sgrp *sgrp; +#endif + +#ifdef WITH_AUDIT + audit_help_open (); +#endif + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + /* + * Save my name for error messages and save my real gid incase of + * errors. If there is an error i have to exec a new login shell for + * the user since her old shell won't have fork'd to create the + * process. Skip over the program name to the next command line + * argument. + * + * This historical comment, and the code itself, suggest that the + * behavior of the system/shell on which it was written differed + * significantly from the one I am using. If this process was + * started from a shell (including the login shell), it was fork'ed + * and exec'ed as a child by that shell. In order to get the user + * back to that shell, it is only necessary to exit from this + * process which terminates the child of the fork. The parent shell, + * which is blocked waiting for a signal, will then receive a + * SIGCHLD and will continue; any changes made to the process + * persona or the environment after the fork never occurred in the + * parent process. + * + * Bottom line: we want to save the name and real gid for messages, + * but we do not need to restore the previous process persona and we + * don't need to re-exec anything. -- JWP + */ + Prog = Basename (argv[0]); + is_newgrp = (strcmp (Prog, "newgrp") == 0); + OPENLOG (is_newgrp ? "newgrp" : "sg"); + gid = getgid (); + argc--; + argv++; + + initenv (); + + pwd = get_my_pwent (); + if (NULL == pwd) { + fprintf (stderr, _("%s: Cannot determine your user name.\n"), + Prog); +#ifdef WITH_AUDIT + audit_logger (AUDIT_CHGRP_ID, Prog, + "changing", NULL, + (unsigned int) getuid (), 0); +#endif + SYSLOG ((LOG_WARN, "Cannot determine the user name of the caller (UID %lu)", + (unsigned long) getuid ())); + closelog (); + exit (EXIT_FAILURE); + } + name = pwd->pw_name; + + /* + * Parse the command line. There are two accepted flags. The first + * is "-", which for newgrp means to re-create the entire + * environment as though a login had been performed, and "-c", which + * for sg causes a command string to be executed. + * + * The next argument, if present, must be the new group name. Any + * remaining remaining arguments will be used to execute a command + * as the named group. If the group name isn't present, I just use + * the login group ID of the current user. + * + * The valid syntax are + * newgrp [-] [groupid] + * newgrp [-l] [groupid] + * sg [-] + * sg [-] groupid [[-c command] + */ + if ( (argc > 0) + && ( (strcmp (argv[0], "-") == 0) + || (strcmp (argv[0], "-l") == 0))) { + argc--; + argv++; + initflag = true; + } + if (!is_newgrp) { + /* + * Do the command line for everything that is + * not "newgrp". + */ + if ((argc > 0) && (argv[0][0] != '-')) { + group = argv[0]; + argc--; + argv++; + } else { + usage (); + closelog (); + exit (EXIT_FAILURE); + } + if (argc > 0) { + + /* + * skip -c if specified so both forms work: + * "sg group -c command" (as in the man page) or + * "sg group command" (as in the usage message). + */ + if ((argc > 1) && (strcmp (argv[0], "-c") == 0)) { + command = argv[1]; + } else { + command = argv[0]; + } + cflag = true; + } + } else { + /* + * Do the command line for "newgrp". It's just making sure + * there aren't any flags and getting the new group name. + */ + if ((argc > 0) && (argv[0][0] == '-')) { + usage (); + goto failure; + } else if (argv[0] != (char *) 0) { + group = argv[0]; + } else { + /* + * get the group file entry for her login group id. + * the entry must exist, simply to be annoying. + * + * Perhaps in the past, but the default behavior now depends on the + * group entry, so it had better exist. -- JWP + */ + grp = xgetgrgid (pwd->pw_gid); + if (NULL == grp) { + fprintf (stderr, + _("%s: GID '%lu' does not exist\n"), + Prog, (unsigned long) pwd->pw_gid); + SYSLOG ((LOG_CRIT, "GID '%lu' does not exist", + (unsigned long) pwd->pw_gid)); + goto failure; + } else { + group = grp->gr_name; + } + } + } + +#ifdef HAVE_SETGROUPS + /* + * get the current users groupset. The new group will be added to + * the concurrent groupset if there is room, otherwise you get a + * nasty message but at least your real and effective group id's are + * set. + */ + /* don't use getgroups(0, 0) - it doesn't work on some systems */ + i = 16; + for (;;) { + grouplist = (GETGROUPS_T *) xmalloc (i * sizeof (GETGROUPS_T)); + ngroups = getgroups (i, grouplist); + if (i > ngroups && !(ngroups == -1 && errno == EINVAL)) { + break; + } + /* not enough room, so try allocating a larger buffer */ + free (grouplist); + i *= 2; + } + if (ngroups < 0) { + perror ("getgroups"); +#ifdef WITH_AUDIT + if (group) { + snprintf (audit_buf, sizeof(audit_buf), + "changing new-group=%s", group); + audit_logger (AUDIT_CHGRP_ID, Prog, + audit_buf, NULL, + (unsigned int) getuid (), 0); + } else { + audit_logger (AUDIT_CHGRP_ID, Prog, + "changing", NULL, + (unsigned int) getuid (), 0); + } +#endif + exit (EXIT_FAILURE); + } +#endif /* HAVE_SETGROUPS */ + + /* + * now we put her in the new group. The password file entry for her + * current user id has been gotten. If there was no optional group + * argument she will have her real and effective group id set to the + * set to the value from her password file entry. + * + * If run as newgrp, or as sg with no command, this process exec's + * an interactive subshell with the effective GID of the new group. + * If run as sg with a command, that command is exec'ed in this + * subshell. When this process terminates, either because the user + * exits, or the command completes, the parent of this process + * resumes with the current GID. + * + * If a group is explicitly specified on the command line, the + * interactive shell or command is run with that effective GID. + * Access will be denied if no entry for that group can be found in + * /etc/group. If the current user name appears in the members list + * for that group, access will be granted immediately; if not, the + * user will be challenged for that group's password. If the + * password response is incorrect, if the specified group does not + * have a password, or if that group has been locked by gpasswd -R, + * access will be denied. This is true even if the group specified + * has the user's login GID (as shown in /etc/passwd). If no group + * is explicitly specified on the command line, the effect is + * exactly the same as if a group name matching the user's login GID + * had been explicitly specified. Root, however, is never + * challenged for passwords, and is always allowed access. + * + * The previous behavior was to allow access to the login group if + * no explicit group was specified, irrespective of the group + * control file(s). This behavior is usually not desirable. A user + * wishing to return to the login group has only to exit back to the + * login shell. Generating yet more shell levels in order to + * provide a convenient "return" to the default group has the + * undesirable side effects of confusing the user, scrambling the + * history file, and consuming system resources. The default now is + * to lock out such behavior. A sys admin can allow it by explicitly + * including the user's name in the member list of the user's login + * group. -- JWP + */ + grp = getgrnam (group); /* local, no need for xgetgrnam */ + if (NULL == grp) { + fprintf (stderr, _("%s: group '%s' does not exist\n"), Prog, group); + goto failure; + } + + /* + * For splitted groups (due to limitations of NIS), check all + * groups of the same GID like the requested group for + * membership of the current user. + */ + grp = find_matching_group (name, grp->gr_gid); + if (NULL == grp) { + /* + * No matching group found. As we already know that + * the group exists, this happens only in the case + * of a requested group where the user is not member. + * + * Re-read the group entry for further processing. + */ + grp = xgetgrnam (group); + assert (NULL != grp); + } +#ifdef SHADOWGRP + sgrp = getsgnam (group); + if (NULL != sgrp) { + grp->gr_passwd = sgrp->sg_passwd; + grp->gr_mem = sgrp->sg_mem; + } +#endif + + /* + * Check if the user is allowed to access this group. + */ + check_perms (grp, pwd, group); + + /* + * all successful validations pass through this point. The group id + * will be set, and the group added to the concurrent groupset. + */ +#ifdef USE_SYSLOG + if (getdef_bool ("SYSLOG_SG_ENAB")) { + syslog_sg (name, group); + } +#endif /* USE_SYSLOG */ + + gid = grp->gr_gid; + +#ifdef HAVE_SETGROUPS + /* + * I am going to try to add her new group id to her concurrent group + * set. If the group id is already present i'll just skip this part. + * If the group doesn't fit, i'll complain loudly and skip this + * part. + */ + for (i = 0; i < ngroups; i++) { + if (gid == grouplist[i]) { + break; + } + } + if (i == ngroups) { + if (ngroups >= sysconf (_SC_NGROUPS_MAX)) { + (void) fputs (_("too many groups\n"), stderr); + } else { + grouplist[ngroups++] = gid; + if (setgroups (ngroups, grouplist) != 0) { + perror ("setgroups"); + } + } + } +#endif + + /* + * Close all files before changing the user/group IDs. + * + * The needed structure should have been copied before, or + * permission to read the database will be required. + */ + endspent (); +#ifdef SHADOWGRP + endsgent (); +#endif + endpwent (); + endgrent (); + + /* + * Set the effective GID to the new group id and the effective UID + * to the real UID. For root, this also sets the real GID to the + * new group id. + */ + if (setgid (gid) != 0) { + perror ("setgid"); +#ifdef WITH_AUDIT + snprintf (audit_buf, sizeof(audit_buf), + "changing new-gid=%lu", (unsigned long) gid); + audit_logger (AUDIT_CHGRP_ID, Prog, + audit_buf, NULL, + (unsigned int) getuid (), 0); +#endif + exit (EXIT_FAILURE); + } + + if (setuid (getuid ()) != 0) { + perror ("setuid"); +#ifdef WITH_AUDIT + snprintf (audit_buf, sizeof(audit_buf), + "changing new-gid=%lu", (unsigned long) gid); + audit_logger (AUDIT_CHGRP_ID, Prog, + audit_buf, NULL, + (unsigned int) getuid (), 0); +#endif + exit (EXIT_FAILURE); + } + + /* + * See if the "-c" flag was used. If it was, i just create a shell + * command for her using the argument that followed the "-c" flag. + */ + if (cflag) { + closelog (); + execl (SHELL, "sh", "-c", command, (char *) 0); +#ifdef WITH_AUDIT + snprintf (audit_buf, sizeof(audit_buf), + "changing new-gid=%lu", (unsigned long) gid); + audit_logger (AUDIT_CHGRP_ID, Prog, + audit_buf, NULL, + (unsigned int) getuid (), 0); +#endif + perror (SHELL); + exit ((errno == ENOENT) ? E_CMD_NOTFOUND : E_CMD_NOEXEC); + } + + /* + * I have to get the pathname of her login shell. As a favor, i'll + * try her environment for a $SHELL value first, and then try the + * password file entry. Obviously this shouldn't be in the + * restricted command directory since it could be used to leave the + * restricted environment. + * + * Note that the following assumes this user's entry in /etc/passwd + * does not have a chroot * prefix. If it does, the * will be copied + * verbatim into the exec path. This is probably not an issue + * because if this user is operating in a chroot jail, her entry in + * the version of /etc/passwd that is accessible here should + * probably never have a chroot shell entry (but entries for other + * users might). If I have missed something, and this causes you a + * problem, try using $SHELL as a workaround; also please notify me + * at jparmele@wildbear.com -- JWP + */ + cp = getenv ("SHELL"); + if (!initflag && (NULL != cp)) { + prog = cp; + } else if ((NULL != pwd->pw_shell) && ('\0' != pwd->pw_shell[0])) { + prog = pwd->pw_shell; + } else { + prog = SHELL; + } + + /* + * Now I try to find the basename of the login shell. This will + * become argv[0] of the spawned command. + */ + cp = Basename ((char *) prog); + + /* + * Switch back to her home directory if i am doing login + * initialization. + */ + if (initflag) { + if (chdir (pwd->pw_dir) != 0) { + perror ("chdir"); + } + + while (NULL != *envp) { + if (strncmp (*envp, "PATH=", 5) == 0 || + strncmp (*envp, "HOME=", 5) == 0 || + strncmp (*envp, "SHELL=", 6) == 0 || + strncmp (*envp, "TERM=", 5) == 0) + addenv (*envp, NULL); + + envp++; + } + } else { + while (NULL != *envp) { + addenv (*envp, NULL); + envp++; + } + } + +#ifdef WITH_AUDIT + snprintf (audit_buf, sizeof(audit_buf), "changing new-gid=%lu", + (unsigned long) gid); + audit_logger (AUDIT_CHGRP_ID, Prog, + audit_buf, NULL, + (unsigned int) getuid (), 1); +#endif + /* + * Exec the login shell and go away. We are trying to get back to + * the previous environment which should be the user's login shell. + */ + err = shell (prog, initflag ? (char *) 0 : cp, newenvp); + exit ((err == ENOENT) ? E_CMD_NOTFOUND : E_CMD_NOEXEC); + /*@notreached@*/ + failure: + + /* + * The previous code, when run as newgrp, re-exec'ed the shell in + * the current process with the original gid on error conditions. + * See the comment above. This historical behavior now has the + * effect of creating unlogged extraneous shell layers when the + * command line has an error or there is an authentication failure. + * We now just want to exit with error status back to the parent + * process. The closelog is probably unnecessary, but it does no + * harm. -- JWP + */ + closelog (); +#ifdef WITH_AUDIT + if (NULL != group) { + snprintf (audit_buf, sizeof(audit_buf), + "changing new-group=%s", group); + audit_logger (AUDIT_CHGRP_ID, Prog, + audit_buf, NULL, + (unsigned int) getuid (), 0); + } else { + audit_logger (AUDIT_CHGRP_ID, Prog, + "changing", NULL, + (unsigned int) getuid (), 0); + } +#endif + exit (EXIT_FAILURE); +} + diff --git a/src/newuidmap.c b/src/newuidmap.c new file mode 100644 index 0000000..1ba25e7 --- /dev/null +++ b/src/newuidmap.c @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2013 Eric Biederman + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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 <stdio.h> +#include <string.h> +#include <errno.h> +#include <stdbool.h> +#include <stdlib.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include "defines.h" +#include "prototypes.h" +#include "subordinateio.h" +#include "idmapping.h" + +/* + * Global variables + */ +const char *Prog; + +static bool verify_range(struct passwd *pw, struct map_range *range) +{ + /* An empty range is invalid */ + if (range->count == 0) + return false; + + /* Test /etc/subuid */ + if (have_sub_uids(pw->pw_name, range->lower, range->count)) + return true; + + /* Allow a process to map its own uid */ + if ((range->count == 1) && (pw->pw_uid == range->lower)) + return true; + + return false; +} + +static void verify_ranges(struct passwd *pw, int ranges, + struct map_range *mappings) +{ + struct map_range *mapping; + int idx; + + mapping = mappings; + for (idx = 0; idx < ranges; idx++, mapping++) { + if (!verify_range(pw, mapping)) { + fprintf(stderr, _( "%s: uid range [%lu-%lu) -> [%lu-%lu) not allowed\n"), + Prog, + mapping->upper, + mapping->upper + mapping->count, + mapping->lower, + mapping->lower + mapping->count); + exit(EXIT_FAILURE); + } + } +} + +void usage(void) +{ + fprintf(stderr, _("usage: %s <pid> <uid> <loweruid> <count> [ <uid> <loweruid> <count> ] ... \n"), Prog); + exit(EXIT_FAILURE); +} + +/* + * newuidmap - Set the uid_map for the specified process + */ +int main(int argc, char **argv) +{ + char proc_dir_name[32]; + char *target_str; + pid_t target, parent; + int proc_dir_fd; + int ranges; + struct map_range *mappings; + struct stat st; + struct passwd *pw; + int written; + + Prog = Basename (argv[0]); + + /* + * The valid syntax are + * newuidmap target_pid + */ + if (argc < 2) + usage(); + + /* Find the process that needs its user namespace + * uid mapping set. + */ + target_str = argv[1]; + if (!get_pid(target_str, &target)) + usage(); + + /* max string length is 6 + 10 + 1 + 1 = 18, allocate 32 bytes */ + written = snprintf(proc_dir_name, sizeof(proc_dir_name), "/proc/%u/", + target); + if ((written <= 0) || (written >= sizeof(proc_dir_name))) { + fprintf(stderr, "%s: snprintf of proc path failed: %s\n", + Prog, strerror(errno)); + } + + proc_dir_fd = open(proc_dir_name, O_DIRECTORY); + if (proc_dir_fd < 0) { + fprintf(stderr, _("%s: Could not open proc directory for target %u\n"), + Prog, target); + return EXIT_FAILURE; + } + + /* Who am i? */ + pw = get_my_pwent (); + if (NULL == pw) { + fprintf (stderr, + _("%s: Cannot determine your user name.\n"), + Prog); + SYSLOG ((LOG_WARN, "Cannot determine the user name of the caller (UID %lu)", + (unsigned long) getuid ())); + return EXIT_FAILURE; + } + + /* Get the effective uid and effective gid of the target process */ + if (fstat(proc_dir_fd, &st) < 0) { + fprintf(stderr, _("%s: Could not stat directory for target %u\n"), + Prog, target); + return EXIT_FAILURE; + } + + /* Verify real user and real group matches the password entry + * and the effective user and group of the program whose + * mappings we have been asked to set. + */ + if ((getuid() != pw->pw_uid) || + (getgid() != pw->pw_gid) || + (pw->pw_uid != st.st_uid) || + (pw->pw_gid != st.st_gid)) { + fprintf(stderr, _( "%s: Target process %u is owned by a different user: uid:%lu pw_uid:%lu st_uid:%lu, gid:%lu pw_gid:%lu st_gid:%lu\n" ), + Prog, target, + (unsigned long int)getuid(), (unsigned long int)pw->pw_uid, (unsigned long int)st.st_uid, + (unsigned long int)getgid(), (unsigned long int)pw->pw_gid, (unsigned long int)st.st_gid); + return EXIT_FAILURE; + } + + if (!sub_uid_open(O_RDONLY)) { + return EXIT_FAILURE; + } + + ranges = ((argc - 2) + 2) / 3; + mappings = get_map_ranges(ranges, argc - 2, argv + 2); + if (!mappings) + usage(); + + verify_ranges(pw, ranges, mappings); + + write_mapping(proc_dir_fd, ranges, mappings, "uid_map"); + sub_uid_close(); + + return EXIT_SUCCESS; +} diff --git a/src/newusers.c b/src/newusers.c new file mode 100644 index 0000000..c38aec4 --- /dev/null +++ b/src/newusers.c @@ -0,0 +1,1251 @@ +/* + * Copyright (c) 1990 - 1993, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2000 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2011, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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. + */ + +/* + * newusers - create users from a batch file + * + * newusers creates a collection of entries in /etc/passwd + * and related files by reading a passwd-format file and + * adding entries in the related directories. + */ + +#include <config.h> + +#ident "$Id$" + +#include <sys/types.h> +#include <sys/stat.h> +#include <stdio.h> +#include <pwd.h> +#include <grp.h> +#include <fcntl.h> +#include <getopt.h> +#include <ctype.h> +#include <errno.h> +#include <string.h> +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM +#include "pam_defs.h" +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ +#include "prototypes.h" +#include "defines.h" +#include "getdef.h" +#include "groupio.h" +#include "nscd.h" +#include "pwio.h" +#include "sgroupio.h" +#include "shadowio.h" +#ifdef ENABLE_SUBIDS +#include "subordinateio.h" +#endif /* ENABLE_SUBIDS */ +#include "chkname.h" + +/* + * Global variables + */ +const char *Prog; + +static bool rflg = false; /* create a system account */ +#ifndef USE_PAM +static /*@null@*//*@observer@*/char *crypt_method = NULL; +#define cflg (NULL != crypt_method) +#ifdef USE_SHA_CRYPT +static bool sflg = false; +static long sha_rounds = 5000; +#endif /* USE_SHA_CRYPT */ +#endif /* !USE_PAM */ + +static bool is_shadow; +#ifdef SHADOWGRP +static bool is_shadow_grp; +static bool sgr_locked = false; +#endif +static bool pw_locked = false; +static bool gr_locked = false; +static bool spw_locked = false; +#ifdef ENABLE_SUBIDS +static bool is_sub_uid = false; +static bool is_sub_gid = false; +static bool sub_uid_locked = false; +static bool sub_gid_locked = false; +#endif /* ENABLE_SUBIDS */ + +/* local function prototypes */ +static void usage (int status); +static void fail_exit (int); +static int add_group (const char *, const char *, gid_t *, gid_t); +static int get_user_id (const char *, uid_t *); +static int add_user (const char *, uid_t, gid_t); +#ifndef USE_PAM +static int update_passwd (struct passwd *, const char *); +#endif /* !USE_PAM */ +static int add_passwd (struct passwd *, const char *); +static void process_flags (int argc, char **argv); +static void check_flags (void); +static void check_perms (void); +static void open_files (void); +static void close_files (void); + +/* + * usage - display usage message and exit + */ +static void usage (int status) +{ + FILE *usageout = (EXIT_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options]\n" + "\n" + "Options:\n"), + Prog); +#ifndef USE_PAM + (void) fprintf (usageout, + _(" -c, --crypt-method METHOD the crypt method (one of %s)\n"), +#ifndef USE_SHA_CRYPT + "NONE DES MD5" +#else /* USE_SHA_CRYPT */ + "NONE DES MD5 SHA256 SHA512" +#endif /* USE_SHA_CRYPT */ + ); +#endif /* !USE_PAM */ + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -r, --system create system accounts\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); +#ifndef USE_PAM +#ifdef USE_SHA_CRYPT + (void) fputs (_(" -s, --sha-rounds number of SHA rounds for the SHA*\n" + " crypt algorithms\n"), + usageout); +#endif /* USE_SHA_CRYPT */ +#endif /* !USE_PAM */ + (void) fputs ("\n", usageout); + + exit (status); +} + +/* + * fail_exit - undo as much as possible + */ +static void fail_exit (int code) +{ + if (spw_locked) { + if (spw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", spw_dbname ())); + /* continue */ + } + } + if (pw_locked) { + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + } + if (gr_locked) { + if (gr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", gr_dbname ())); + /* continue */ + } + } +#ifdef SHADOWGRP + if (sgr_locked) { + if (sgr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sgr_dbname ())); + /* continue */ + } + } +#endif +#ifdef ENABLE_SUBIDS + if (sub_uid_locked) { + if (sub_uid_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sub_uid_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sub_uid_dbname ())); + /* continue */ + } + } + if (sub_gid_locked) { + if (sub_gid_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sub_gid_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sub_gid_dbname ())); + /* continue */ + } + } +#endif /* ENABLE_SUBIDS */ + + exit (code); +} + +/* + * add_group - create a new group or add a user to an existing group + */ +static int add_group (const char *name, const char *gid, gid_t *ngid, uid_t uid) +{ + const struct group *grp; + struct group grent; + char *members[1]; +#ifdef SHADOWGRP + const struct sgrp *sg; +#endif + + /* + * Start by seeing if the named group already exists. This will be + * very easy to deal with if it does. + */ + grp = getgrnam (gid); + if (NULL == grp) { + grp = gr_locate (gid); + } + if (NULL != grp) { + /* The user will use this ID for her primary group */ + *ngid = grp->gr_gid; + /* Don't check gshadow */ + return 0; + } + + if (isdigit (gid[0])) { + /* + * The GID is a number, which means either this is a brand + * new group, or an existing group. + */ + + if (get_gid (gid, &grent.gr_gid) == 0) { + fprintf (stderr, + _("%s: invalid group ID '%s'\n"), + Prog, gid); + return -1; + } + + /* Look in both the system database (getgrgid) and in the + * internal database (gr_locate_gid), which may contain + * uncommitted changes */ + if ( (getgrgid ((gid_t) grent.gr_gid) != NULL) + || (gr_locate_gid ((gid_t) grent.gr_gid) != NULL)) { + /* The user will use this ID for her + * primary group */ + *ngid = (gid_t) grent.gr_gid; + return 0; + } + + /* Do not create groups with GID == (gid_t)-1 */ + if (grent.gr_gid == (gid_t)-1) { + fprintf (stderr, + _("%s: invalid group ID '%s'\n"), + Prog, gid); + return -1; + } + } else { + /* The gid parameter can be "" or a name which is not + * already the name of an existing group. + * In both cases, figure out what group ID can be used. + */ + if (find_new_gid(rflg, &grent.gr_gid, &uid) < 0) { + return -1; + } + } + + /* + * Now I have all of the fields required to create the new group. + */ + if (('\0' != gid[0]) && (!isdigit (gid[0]))) { + grent.gr_name = xstrdup (gid); + } else { + grent.gr_name = xstrdup (name); +/* FIXME: check if the group exists */ + } + + /* Check if this is a valid group name */ + if (!is_valid_group_name (grent.gr_name)) { + fprintf (stderr, + _("%s: invalid group name '%s'\n"), + Prog, grent.gr_name); + free (grent.gr_name); + return -1; + } + + grent.gr_passwd = "*"; /* XXX warning: const */ + members[0] = NULL; + grent.gr_mem = members; + + *ngid = grent.gr_gid; + +#ifdef SHADOWGRP + if (is_shadow_grp) { + sg = sgr_locate (grent.gr_name); + + if (NULL != sg) { + fprintf (stderr, + _("%s: group '%s' is a shadow group, but does not exist in /etc/group\n"), + Prog, grent.gr_name); + return -1; + } + } +#endif + +#ifdef SHADOWGRP + if (is_shadow_grp) { + struct sgrp sgrent; + char *admins[1]; + sgrent.sg_name = grent.gr_name; + sgrent.sg_passwd = "*"; /* XXX warning: const */ + grent.gr_passwd = "x"; /* XXX warning: const */ + admins[0] = NULL; + sgrent.sg_adm = admins; + sgrent.sg_mem = members; + + if (sgr_update (&sgrent) == 0) { + return -1; + } + } +#endif + + if (gr_update (&grent) == 0) { + return -1; + } + + return 0; +} + +static int get_user_id (const char *uid, uid_t *nuid) { + + /* + * The first guess for the UID is either the numerical UID that the + * caller provided, or the next available UID. + */ + if (isdigit (uid[0])) { + if ((get_uid (uid, nuid) == 0) || (*nuid == (uid_t)-1)) { + fprintf (stderr, + _("%s: invalid user ID '%s'\n"), + Prog, uid); + return -1; + } + } else { + if ('\0' != uid[0]) { + const struct passwd *pwd; + /* local, no need for xgetpwnam */ + pwd = getpwnam (uid); + if (NULL == pwd) { + pwd = pw_locate (uid); + } + + if (NULL != pwd) { + *nuid = pwd->pw_uid; + } else { + fprintf (stderr, + _("%s: user '%s' does not exist\n"), + Prog, uid); + return -1; + } + } else { + if (find_new_uid (rflg, nuid, NULL) < 0) { + return -1; + } + } + } + + return 0; +} + +/* + * add_user - create a new user ID + */ +static int add_user (const char *name, uid_t uid, gid_t gid) +{ + struct passwd pwent; + + /* Check if this is a valid user name */ + if (!is_valid_user_name (name)) { + fprintf (stderr, + _("%s: invalid user name '%s'\n"), + Prog, name); + return -1; + } + + /* + * I don't want to fill in the entire password structure members + * JUST YET, since there is still more data to be added. So, I fill + * in the parts that I have. + */ + pwent.pw_name = xstrdup (name); + pwent.pw_uid = uid; + pwent.pw_passwd = "x"; /* XXX warning: const */ + pwent.pw_gid = gid; + pwent.pw_gecos = ""; /* XXX warning: const */ + pwent.pw_dir = ""; /* XXX warning: const */ + pwent.pw_shell = ""; /* XXX warning: const */ + + return (pw_update (&pwent) == 0) ? -1 : 0; +} + +#ifndef USE_PAM +/* + * update_passwd - update the password in the passwd entry + * + * Return 0 if successful. + */ +static int update_passwd (struct passwd *pwd, const char *password) +{ + void *crypt_arg = NULL; + char *cp; + if (crypt_method != NULL) { +#ifdef USE_SHA_CRYPT + if (sflg) { + crypt_arg = &sha_rounds; + } +#endif + } + + if ((crypt_method != NULL) && (0 == strcmp(crypt_method, "NONE"))) { + pwd->pw_passwd = (char *)password; + } else { + const char *salt = crypt_make_salt (crypt_method, crypt_arg); + cp = pw_encrypt (password, salt); + if (NULL == cp) { + fprintf (stderr, + _("%s: failed to crypt password with salt '%s': %s\n"), + Prog, salt, strerror (errno)); + return 1; + } + pwd->pw_passwd = cp; + } + + return 0; +} +#endif /* !USE_PAM */ + +/* + * add_passwd - add or update the encrypted password + */ +static int add_passwd (struct passwd *pwd, const char *password) +{ + const struct spwd *sp; + struct spwd spent; + char *cp; + +#ifndef USE_PAM + void *crypt_arg = NULL; + if (crypt_method != NULL) { +#ifdef USE_SHA_CRYPT + if (sflg) { + crypt_arg = &sha_rounds; + } +#endif /* USE_SHA_CRYPT */ + } + + /* + * In the case of regular password files, this is real easy - pwd + * points to the entry in the password file. Shadow files are + * harder since there are zillions of things to do ... + */ + if (!is_shadow) { + return update_passwd (pwd, password); + } +#endif /* USE_PAM */ + + /* + * Do the first and easiest shadow file case. The user already + * exists in the shadow password file. + */ + sp = spw_locate (pwd->pw_name); +#ifndef USE_PAM + if (NULL != sp) { + spent = *sp; + if ( (NULL != crypt_method) + && (0 == strcmp(crypt_method, "NONE"))) { + spent.sp_pwdp = (char *)password; + } else { + const char *salt = crypt_make_salt (crypt_method, + crypt_arg); + cp = pw_encrypt (password, salt); + if (NULL == cp) { + fprintf (stderr, + _("%s: failed to crypt password with salt '%s': %s\n"), + Prog, salt, strerror (errno)); + return 1; + } + spent.sp_pwdp = cp; + } + spent.sp_lstchg = (long) gettime () / SCALE; + if (0 == spent.sp_lstchg) { + /* Better disable aging than requiring a password + * change */ + spent.sp_lstchg = -1; + } + return (spw_update (&spent) == 0); + } + + /* + * Pick the next easiest case - the user has an encrypted password + * which isn't equal to "x". The password was set to "x" earlier + * when the entry was created, so this user would have to have had + * the password set someplace else. + */ + if (strcmp (pwd->pw_passwd, "x") != 0) { + return update_passwd (pwd, password); + } +#else /* USE_PAM */ + /* + * If there is already a shadow entry, do not touch it. + * If there is already a passwd entry with a password, do not + * touch it. + * The password will be updated later for all users using PAM. + */ + if ( (NULL != sp) + || (strcmp (pwd->pw_passwd, "x") != 0)) { + return 0; + } +#endif /* USE_PAM */ + + /* + * Now the really hard case - I need to create an entirely new + * shadow password file entry. + */ + spent.sp_namp = pwd->pw_name; +#ifndef USE_PAM + if ((crypt_method != NULL) && (0 == strcmp(crypt_method, "NONE"))) { + spent.sp_pwdp = (char *)password; + } else { + const char *salt = crypt_make_salt (crypt_method, crypt_arg); + cp = pw_encrypt (password, salt); + if (NULL == cp) { + fprintf (stderr, + _("%s: failed to crypt password with salt '%s': %s\n"), + Prog, salt, strerror (errno)); + return 1; + } + spent.sp_pwdp = cp; + } +#else + /* + * Lock the password. + * The password will be updated later for all users using PAM. + */ + spent.sp_pwdp = "!"; +#endif + spent.sp_lstchg = (long) gettime () / SCALE; + if (0 == spent.sp_lstchg) { + /* Better disable aging than requiring a password change */ + spent.sp_lstchg = -1; + } + spent.sp_min = getdef_num ("PASS_MIN_DAYS", 0); + /* 10000 is infinity this week */ + spent.sp_max = getdef_num ("PASS_MAX_DAYS", 10000); + spent.sp_warn = getdef_num ("PASS_WARN_AGE", -1); + spent.sp_inact = -1; + spent.sp_expire = -1; + spent.sp_flag = SHADOW_SP_FLAG_UNSET; + + return (spw_update (&spent) == 0); +} + +/* + * process_flags - parse the command line options + * + * It will not return if an error is encountered. + */ +static void process_flags (int argc, char **argv) +{ + int c; + static struct option long_options[] = { +#ifndef USE_PAM + {"crypt-method", required_argument, NULL, 'c'}, +#endif /* !USE_PAM */ + {"help", no_argument, NULL, 'h'}, + {"system", no_argument, NULL, 'r'}, + {"root", required_argument, NULL, 'R'}, +#ifndef USE_PAM +#ifdef USE_SHA_CRYPT + {"sha-rounds", required_argument, NULL, 's'}, +#endif /* USE_SHA_CRYPT */ +#endif /* !USE_PAM */ + {NULL, 0, NULL, '\0'} + }; + + while ((c = getopt_long (argc, argv, +#ifndef USE_PAM +#ifdef USE_SHA_CRYPT + "c:hrs:", +#else /* !USE_SHA_CRYPT */ + "c:hr", +#endif /* !USE_SHA_CRYPT */ +#else /* USE_PAM */ + "hr", +#endif + long_options, NULL)) != -1) { + switch (c) { +#ifndef USE_PAM + case 'c': + crypt_method = optarg; + break; +#endif /* !USE_PAM */ + case 'h': + usage (EXIT_SUCCESS); + break; + case 'r': + rflg = true; + break; + case 'R': /* no-op, handled in process_root_flag () */ + break; +#ifndef USE_PAM +#ifdef USE_SHA_CRYPT + case 's': + sflg = true; + if (getlong(optarg, &sha_rounds) == 0) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + usage (EXIT_FAILURE); + } + break; +#endif /* USE_SHA_CRYPT */ +#endif /* !USE_PAM */ + default: + usage (EXIT_FAILURE); + break; + } + } + + if ( (optind != argc) + && (optind + 1 != argc)) { + usage (EXIT_FAILURE); + } + + if (argv[optind] != NULL) { + if (freopen (argv[optind], "r", stdin) == NULL) { + char buf[BUFSIZ]; + snprintf (buf, sizeof buf, "%s: %s", Prog, argv[1]); + perror (buf); + fail_exit (EXIT_FAILURE); + } + } + + /* validate options */ + check_flags (); +} + +/* + * check_flags - check flags and parameters consistency + * + * It will not return if an error is encountered. + */ +static void check_flags (void) +{ +#ifndef USE_PAM +#ifdef USE_SHA_CRYPT + if (sflg && !cflg) { + fprintf (stderr, + _("%s: %s flag is only allowed with the %s flag\n"), + Prog, "-s", "-c"); + usage (EXIT_FAILURE); + } +#endif /* USE_SHA_CRYPT */ + + if (cflg) { + if ( (0 != strcmp (crypt_method, "DES")) + && (0 != strcmp (crypt_method, "MD5")) + && (0 != strcmp (crypt_method, "NONE")) +#ifdef USE_SHA_CRYPT + && (0 != strcmp (crypt_method, "SHA256")) + && (0 != strcmp (crypt_method, "SHA512")) +#endif /* USE_SHA_CRYPT */ + ) { + fprintf (stderr, + _("%s: unsupported crypt method: %s\n"), + Prog, crypt_method); + usage (EXIT_FAILURE); + } + } +#endif /* !USE_PAM */ +} + +/* + * check_perms - check if the caller is allowed to add a group + * + * With PAM support, the setuid bit can be set on groupadd to allow + * non-root users to groups. + * Without PAM support, only users who can write in the group databases + * can add groups. + * + * It will not return if the user is not allowed. + */ +static void check_perms (void) +{ +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM + pam_handle_t *pamh = NULL; + int retval; + struct passwd *pampw; + + pampw = getpwuid (getuid ()); /* local, no need for xgetpwuid */ + if (NULL == pampw) { + fprintf (stderr, + _("%s: Cannot determine your user name.\n"), + Prog); + fail_exit (EXIT_FAILURE); + } + + retval = pam_start ("newusers", pampw->pw_name, &conv, &pamh); + + if (PAM_SUCCESS == retval) { + retval = pam_authenticate (pamh, 0); + } + + if (PAM_SUCCESS == retval) { + retval = pam_acct_mgmt (pamh, 0); + } + + if (PAM_SUCCESS != retval) { + fprintf (stderr, _("%s: PAM: %s\n"), + Prog, pam_strerror (pamh, retval)); + SYSLOG((LOG_ERR, "%s", pam_strerror (pamh, retval))); + if (NULL != pamh) { + (void) pam_end (pamh, retval); + } + fail_exit (EXIT_FAILURE); + } + (void) pam_end (pamh, retval); +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ +} + +/* + * open_files - lock and open the password, group and shadow databases + */ +static void open_files (void) +{ + /* + * Lock the password files and open them for update. This will bring + * all of the entries into memory where they may be searched for an + * modified, or new entries added. The password file is the key - if + * it gets locked, assume the others can be locked right away. + */ + if (pw_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, pw_dbname ()); + fail_exit (EXIT_FAILURE); + } + pw_locked = true; + if (is_shadow) { + if (spw_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, spw_dbname ()); + fail_exit (EXIT_FAILURE); + } + spw_locked = true; + } + if (gr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, gr_dbname ()); + fail_exit (EXIT_FAILURE); + } + gr_locked = true; +#ifdef SHADOWGRP + if (is_shadow_grp) { + if (sgr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sgr_dbname ()); + fail_exit (EXIT_FAILURE); + } + sgr_locked = true; + } +#endif +#ifdef ENABLE_SUBIDS + if (is_sub_uid) { + if (sub_uid_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sub_uid_dbname ()); + fail_exit (EXIT_FAILURE); + } + sub_uid_locked = true; + } + if (is_sub_gid) { + if (sub_gid_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sub_gid_dbname ()); + fail_exit (EXIT_FAILURE); + } + sub_gid_locked = true; + } +#endif /* ENABLE_SUBIDS */ + + if (pw_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, _("%s: cannot open %s\n"), Prog, pw_dbname ()); + fail_exit (EXIT_FAILURE); + } + if (is_shadow && (spw_open (O_CREAT | O_RDWR) == 0)) { + fprintf (stderr, _("%s: cannot open %s\n"), Prog, spw_dbname ()); + fail_exit (EXIT_FAILURE); + } + if (gr_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, _("%s: cannot open %s\n"), Prog, gr_dbname ()); + fail_exit (EXIT_FAILURE); + } +#ifdef SHADOWGRP + if (is_shadow_grp && (sgr_open (O_CREAT | O_RDWR) == 0)) { + fprintf (stderr, _("%s: cannot open %s\n"), Prog, sgr_dbname ()); + fail_exit (EXIT_FAILURE); + } +#endif +#ifdef ENABLE_SUBIDS + if (is_sub_uid) { + if (sub_uid_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, sub_uid_dbname ()); + fail_exit (EXIT_FAILURE); + } + } + if (is_sub_gid) { + if (sub_gid_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, sub_gid_dbname ()); + fail_exit (EXIT_FAILURE); + } + } +#endif /* ENABLE_SUBIDS */ +} + +/* + * close_files - close and unlock the password, group and shadow databases + */ +static void close_files (void) +{ + if (pw_close () == 0) { + fprintf (stderr, _("%s: failure while writing changes to %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", pw_dbname ())); + fail_exit (EXIT_FAILURE); + } + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + pw_locked = false; + + if (is_shadow) { + if (spw_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", spw_dbname ())); + fail_exit (EXIT_FAILURE); + } + if (spw_unlock () == 0) { + fprintf (stderr, + _("%s: failed to unlock %s\n"), + Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", spw_dbname ())); + /* continue */ + } + spw_locked = false; + } + + if (gr_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", gr_dbname ())); + fail_exit (EXIT_FAILURE); + } +#ifdef ENABLE_SUBIDS + if (is_sub_uid && (sub_uid_close () == 0)) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), Prog, sub_uid_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", sub_uid_dbname ())); + fail_exit (EXIT_FAILURE); + } + if (is_sub_gid && (sub_gid_close () == 0)) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), Prog, sub_gid_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", sub_gid_dbname ())); + fail_exit (EXIT_FAILURE); + } +#endif /* ENABLE_SUBIDS */ + + if (gr_unlock () == 0) { + fprintf (stderr, + _("%s: failed to unlock %s\n"), + Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", gr_dbname ())); + /* continue */ + } + gr_locked = false; + +#ifdef SHADOWGRP + if (is_shadow_grp) { + if (sgr_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", sgr_dbname ())); + fail_exit (EXIT_FAILURE); + } + if (sgr_unlock () == 0) { + fprintf (stderr, + _("%s: failed to unlock %s\n"), + Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sgr_dbname ())); + /* continue */ + } + sgr_locked = false; + } +#endif +#ifdef ENABLE_SUBIDS + if (is_sub_uid) { + if (sub_uid_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sub_uid_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sub_uid_dbname ())); + /* continue */ + } + sub_uid_locked = false; + } + if (is_sub_gid) { + if (sub_gid_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sub_gid_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sub_gid_dbname ())); + /* continue */ + } + sub_gid_locked = false; + } +#endif /* ENABLE_SUBIDS */ +} + +int main (int argc, char **argv) +{ + char buf[BUFSIZ]; + char *fields[8]; + int nfields; + char *cp; + const struct passwd *pw; + struct passwd newpw; + int errors = 0; + int line = 0; + uid_t uid; + gid_t gid; +#ifdef USE_PAM + int *lines = NULL; + char **usernames = NULL; + char **passwords = NULL; + unsigned int nusers = 0; +#endif /* USE_PAM */ + + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + /* FIXME: will not work with an input file */ + process_root_flag ("-R", argc, argv); + + OPENLOG ("newusers"); + + process_flags (argc, argv); + + check_perms (); + + is_shadow = spw_file_present (); + +#ifdef SHADOWGRP + is_shadow_grp = sgr_file_present (); +#endif +#ifdef ENABLE_SUBIDS + is_sub_uid = sub_uid_file_present () && !rflg; + is_sub_gid = sub_gid_file_present () && !rflg; +#endif /* ENABLE_SUBIDS */ + + open_files (); + + /* + * Read each line. The line has the same format as a password file + * entry, except that certain fields are not constrained to be + * numerical values. If a group ID is entered which does not already + * exist, an attempt is made to allocate the same group ID as the + * numerical user ID. Should that fail, the next available group ID + * over 100 is allocated. The pw_gid field will be updated with that + * value. + */ + while (fgets (buf, (int) sizeof buf, stdin) != (char *) 0) { + line++; + cp = strrchr (buf, '\n'); + if (NULL != cp) { + *cp = '\0'; + } else { + if (feof (stdin) == 0) { + fprintf (stderr, + _("%s: line %d: line too long\n"), + Prog, line); + errors++; + continue; + } + } + + /* + * Break the string into fields and screw around with them. + * There MUST be 7 colon separated fields, although the + * values aren't that particular. + */ + for (cp = buf, nfields = 0; nfields < 7; nfields++) { + fields[nfields] = cp; + cp = strchr (cp, ':'); + if (NULL != cp) { + *cp = '\0'; + cp++; + } else { + break; + } + } + if (nfields != 6) { + fprintf (stderr, _("%s: line %d: invalid line\n"), + Prog, line); + errors++; + continue; + } + + /* + * First check if we have to create or update an user + */ + pw = pw_locate (fields[0]); + /* local, no need for xgetpwnam */ + if ( (NULL == pw) + && (getpwnam (fields[0]) != NULL)) { + fprintf (stderr, _("%s: cannot update the entry of user %s (not in the passwd database)\n"), Prog, fields[0]); + errors++; + continue; + } + + if ( (NULL == pw) + && (get_user_id (fields[2], &uid) != 0)) { + fprintf (stderr, + _("%s: line %d: can't create user\n"), + Prog, line); + errors++; + continue; + } + + /* + * Processed is the group name. A new group will be + * created if the group name is non-numeric and does not + * already exist. If the group name is a number (which is not + * an existing GID), a group with the same name as the user + * will be created, with the given GID. The given or created + * group will be the primary group of the user. If + * there is no named group to be a member of, the UID will + * be figured out and that value will be a candidate for a + * new group, if that group ID exists, a whole new group ID + * will be made up. + */ + if ( (NULL == pw) + && (add_group (fields[0], fields[3], &gid, uid) != 0)) { + fprintf (stderr, + _("%s: line %d: can't create group\n"), + Prog, line); + errors++; + continue; + } + + /* + * Now we work on the user ID. It has to be specified either + * as a numerical value, or left blank. If it is a numerical + * value, that value will be used, otherwise the next + * available user ID is computed and used. After this there + * will at least be a (struct passwd) for the user. + */ + if ( (NULL == pw) + && (add_user (fields[0], uid, gid) != 0)) { + fprintf (stderr, + _("%s: line %d: can't create user\n"), + Prog, line); + errors++; + continue; + } + + /* + * The password, gecos field, directory, and shell fields + * all come next. + */ + pw = pw_locate (fields[0]); + if (NULL == pw) { + fprintf (stderr, + _("%s: line %d: user '%s' does not exist in %s\n"), + Prog, line, fields[0], pw_dbname ()); + errors++; + continue; + } + newpw = *pw; + +#ifdef USE_PAM + /* keep the list of user/password for later update by PAM */ + nusers++; + lines = realloc (lines, sizeof (lines[0]) * nusers); + usernames = realloc (usernames, sizeof (usernames[0]) * nusers); + passwords = realloc (passwords, sizeof (passwords[0]) * nusers); + lines[nusers-1] = line; + usernames[nusers-1] = strdup (fields[0]); + passwords[nusers-1] = strdup (fields[1]); +#endif /* USE_PAM */ + if (add_passwd (&newpw, fields[1]) != 0) { + fprintf (stderr, + _("%s: line %d: can't update password\n"), + Prog, line); + errors++; + continue; + } + if ('\0' != fields[4][0]) { + newpw.pw_gecos = fields[4]; + } + + if ('\0' != fields[5][0]) { + newpw.pw_dir = fields[5]; + } + + if ('\0' != fields[6][0]) { + newpw.pw_shell = fields[6]; + } + + if ( ('\0' != fields[5][0]) + && (access (newpw.pw_dir, F_OK) != 0)) { +/* FIXME: should check for directory */ + mode_t msk = 0777 & ~getdef_num ("UMASK", + GETDEF_DEFAULT_UMASK); + if (mkdir (newpw.pw_dir, msk) != 0) { + fprintf (stderr, + _("%s: line %d: mkdir %s failed: %s\n"), + Prog, line, newpw.pw_dir, + strerror (errno)); + } else if (chown (newpw.pw_dir, + newpw.pw_uid, + newpw.pw_gid) != 0) { + fprintf (stderr, + _("%s: line %d: chown %s failed: %s\n"), + Prog, line, newpw.pw_dir, + strerror (errno)); + } + } + + /* + * Update the password entry with the new changes made. + */ + if (pw_update (&newpw) == 0) { + fprintf (stderr, + _("%s: line %d: can't update entry\n"), + Prog, line); + errors++; + continue; + } + +#ifdef ENABLE_SUBIDS + /* + * Add subordinate uids if the user does not have them. + */ + if (is_sub_uid && !sub_uid_assigned(fields[0])) { + uid_t sub_uid_start = 0; + unsigned long sub_uid_count = 0; + if (find_new_sub_uids(fields[0], &sub_uid_start, &sub_uid_count) == 0) { + if (sub_uid_add(fields[0], sub_uid_start, sub_uid_count) == 0) { + fprintf (stderr, + _("%s: failed to prepare new %s entry\n"), + Prog, sub_uid_dbname ()); + } + } else { + fprintf (stderr, + _("%s: can't find subordinate user range\n"), + Prog); + errors++; + } + } + + /* + * Add subordinate gids if the user does not have them. + */ + if (is_sub_gid && !sub_gid_assigned(fields[0])) { + gid_t sub_gid_start = 0; + unsigned long sub_gid_count = 0; + if (find_new_sub_gids(fields[0], &sub_gid_start, &sub_gid_count) == 0) { + if (sub_gid_add(fields[0], sub_gid_start, sub_gid_count) == 0) { + fprintf (stderr, + _("%s: failed to prepare new %s entry\n"), + Prog, sub_uid_dbname ()); + } + } else { + fprintf (stderr, + _("%s: can't find subordinate group range\n"), + Prog); + errors++; + } + } +#endif /* ENABLE_SUBIDS */ + } + + /* + * Any detected errors will cause the entire set of changes to be + * aborted. Unlocking the password file will cause all of the + * changes to be ignored. Otherwise the file is closed, causing the + * changes to be written out all at once, and then unlocked + * afterwards. + */ + if (0 != errors) { + fprintf (stderr, + _("%s: error detected, changes ignored\n"), Prog); + fail_exit (EXIT_FAILURE); + } + + close_files (); + + nscd_flush_cache ("passwd"); + nscd_flush_cache ("group"); + +#ifdef USE_PAM + unsigned int i; + /* Now update the passwords using PAM */ + for (i = 0; i < nusers; i++) { + if (do_pam_passwd_non_interractive ("newusers", usernames[i], passwords[i]) != 0) { + fprintf (stderr, + _("%s: (line %d, user %s) password not changed\n"), + Prog, lines[i], usernames[i]); + errors++; + } + } +#endif /* USE_PAM */ + + return ((0 == errors) ? EXIT_SUCCESS : EXIT_FAILURE); +} + diff --git a/src/nologin.c b/src/nologin.c new file mode 100644 index 0000000..7fe8a6a --- /dev/null +++ b/src/nologin.c @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2004 The FreeBSD Project. + * 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <stdio.h> +#include <stdlib.h> +#include <syslog.h> +#include <unistd.h> + +int main (void) +{ + const char *user, *tty; + + tty = ttyname (0); + if (NULL == tty) { + tty = "UNKNOWN"; + } + user = getlogin (); + if (NULL == user) { + user = "UNKNOWN"; + } + openlog ("nologin", LOG_CONS, LOG_AUTH); + syslog (LOG_CRIT, "Attempted login by %s on %s", user, tty); + closelog (); + + printf ("%s", "This account is currently not available.\n"); + + return EXIT_FAILURE; +} diff --git a/src/passwd.c b/src/passwd.c new file mode 100644 index 0000000..3af3e65 --- /dev/null +++ b/src/passwd.c @@ -0,0 +1,1168 @@ +/* + * Copyright (c) 1989 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2001 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2011, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <sys/types.h> +#ifdef WITH_SELINUX +#include <selinux/selinux.h> +#include <selinux/flask.h> +#include <selinux/av_permissions.h> +#include <selinux/context.h> +#endif /* WITH_SELINUX */ +#include <time.h> +#include "defines.h" +#include "getdef.h" +#include "nscd.h" +#include "prototypes.h" +#include "pwauth.h" +#include "pwio.h" +#include "shadowio.h" + +/* + * exit status values + */ +/*@-exitarg@*/ +#define E_SUCCESS 0 /* success */ +#define E_NOPERM 1 /* permission denied */ +#define E_USAGE 2 /* invalid combination of options */ +#define E_FAILURE 3 /* unexpected failure, nothing done */ +#define E_MISSING 4 /* unexpected failure, passwd file missing */ +#define E_PWDBUSY 5 /* passwd file busy, try again later */ +#define E_BAD_ARG 6 /* invalid argument to option */ +/* + * Global variables + */ +const char *Prog; /* Program name */ + +static char *name; /* The name of user whose password is being changed */ +static char *myname; /* The current user's name */ +static bool amroot; /* The caller's real UID was 0 */ + +static bool + aflg = false, /* -a - show status for all users */ + dflg = false, /* -d - delete password */ + eflg = false, /* -e - force password change */ + iflg = false, /* -i - set inactive days */ + kflg = false, /* -k - change only if expired */ + lflg = false, /* -l - lock the user's password */ + nflg = false, /* -n - set minimum days */ + qflg = false, /* -q - quiet mode */ + Sflg = false, /* -S - show password status */ + uflg = false, /* -u - unlock the user's password */ + wflg = false, /* -w - set warning days */ + xflg = false; /* -x - set maximum days */ + +/* + * set to 1 if there are any flags which require root privileges, + * and require username to be specified + */ +static bool anyflag = false; + +static long age_min = 0; /* Minimum days before change */ +static long age_max = 0; /* Maximum days until change */ +static long warn = 0; /* Warning days before change */ +static long inact = 0; /* Days without change before locked */ + +#ifndef USE_PAM +static bool do_update_age = false; +#endif /* ! USE_PAM */ + +static bool pw_locked = false; +static bool spw_locked = false; + +#ifndef USE_PAM +/* + * Size of the biggest passwd: + * $6$ 3 + * rounds= 7 + * 999999999 9 + * $ 1 + * salt 16 + * $ 1 + * SHA512 123 + * nul 1 + * + * total 161 + */ +static char crypt_passwd[256]; +static bool do_update_pwd = false; +#endif /* !USE_PAM */ + +/* + * External identifiers + */ + +/* local function prototypes */ +static /*@noreturn@*/void usage (int); + +#ifndef USE_PAM +static bool reuse (const char *, const struct passwd *); +static int new_password (const struct passwd *); + +static void check_password (const struct passwd *, const struct spwd *); +#endif /* !USE_PAM */ +static /*@observer@*/const char *date_to_str (time_t); +static /*@observer@*/const char *pw_status (const char *); +static void print_status (const struct passwd *); +static /*@noreturn@*/void fail_exit (int); +static /*@noreturn@*/void oom (void); +static char *update_crypt_pw (char *); +static void update_noshadow (void); + +static void update_shadow (void); +#ifdef WITH_SELINUX +static int check_selinux_access (const char *changed_user, + uid_t changed_uid, + access_vector_t requested_access); +#endif /* WITH_SELINUX */ + +/* + * usage - print command usage and exit + */ +static /*@noreturn@*/void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options] [LOGIN]\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -a, --all report password status on all accounts\n"), usageout); + (void) fputs (_(" -d, --delete delete the password for the named account\n"), usageout); + (void) fputs (_(" -e, --expire force expire the password for the named account\n"), usageout); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -k, --keep-tokens change password only if expired\n"), usageout); + (void) fputs (_(" -i, --inactive INACTIVE set password inactive after expiration\n" + " to INACTIVE\n"), usageout); + (void) fputs (_(" -l, --lock lock the password of the named account\n"), usageout); + (void) fputs (_(" -n, --mindays MIN_DAYS set minimum number of days before password\n" + " change to MIN_DAYS\n"), usageout); + (void) fputs (_(" -q, --quiet quiet mode\n"), usageout); + (void) fputs (_(" -r, --repository REPOSITORY change password in REPOSITORY repository\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs (_(" -S, --status report password status on the named account\n"), usageout); + (void) fputs (_(" -u, --unlock unlock the password of the named account\n"), usageout); + (void) fputs (_(" -w, --warndays WARN_DAYS set expiration warning days to WARN_DAYS\n"), usageout); + (void) fputs (_(" -x, --maxdays MAX_DAYS set maximum number of days before password\n" + " change to MAX_DAYS\n"), usageout); + (void) fputs ("\n", usageout); + exit (status); +} + +#ifndef USE_PAM +static bool reuse (const char *pass, const struct passwd *pw) +{ +#ifdef HAVE_LIBCRACK_HIST + const char *reason; + +#ifdef HAVE_LIBCRACK_PW + const char *FascistHistoryPw (const char *, const struct passwd *); + + reason = FascistHistory (pass, pw); +#else /* !HAVE_LIBCRACK_PW */ + const char *FascistHistory (const char *, int); + + reason = FascistHistory (pass, pw->pw_uid); +#endif /* !HAVE_LIBCRACK_PW */ + if (NULL != reason) { + (void) printf (_("Bad password: %s. "), reason); + return true; + } +#endif /* HAVE_LIBCRACK_HIST */ + return false; +} + +/* + * new_password - validate old password and replace with new (both old and + * new in global "char crypt_passwd[128]") + */ +static int new_password (const struct passwd *pw) +{ + char *clear; /* Pointer to clear text */ + char *cipher; /* Pointer to cipher text */ + const char *salt; /* Pointer to new salt */ + char *cp; /* Pointer to getpass() response */ + char orig[200]; /* Original password */ + char pass[200]; /* New password */ + int i; /* Counter for retries */ + bool warned; + int pass_max_len = -1; + const char *method; + +#ifdef HAVE_LIBCRACK_HIST + int HistUpdate (const char *, const char *); +#endif /* HAVE_LIBCRACK_HIST */ + + /* + * Authenticate the user. The user will be prompted for their own + * password. + */ + + if (!amroot && ('\0' != crypt_passwd[0])) { + clear = getpass (_("Old password: ")); + if (NULL == clear) { + return -1; + } + + cipher = pw_encrypt (clear, crypt_passwd); + + if (NULL == cipher) { + strzero (clear); + fprintf (stderr, + _("%s: failed to crypt password with previous salt: %s\n"), + Prog, strerror (errno)); + SYSLOG ((LOG_INFO, + "Failed to crypt password with previous salt of user '%s'", + pw->pw_name)); + return -1; + } + + if (strcmp (cipher, crypt_passwd) != 0) { + strzero (clear); + strzero (cipher); + SYSLOG ((LOG_WARN, "incorrect password for %s", + pw->pw_name)); + (void) sleep (1); + (void) fprintf (stderr, + _("Incorrect password for %s.\n"), + pw->pw_name); + return -1; + } + STRFCPY (orig, clear); + strzero (clear); + strzero (cipher); + } else { + orig[0] = '\0'; + } + + /* + * Get the new password. The user is prompted for the new password + * and has five tries to get it right. The password will be tested + * for strength, unless it is the root user. This provides an escape + * for initial login passwords. + */ + method = getdef_str ("ENCRYPT_METHOD"); + if (NULL == method) { + if (!getdef_bool ("MD5_CRYPT_ENAB")) { + pass_max_len = getdef_num ("PASS_MAX_LEN", 8); + } + } else { + if ( (strcmp (method, "MD5") == 0) +#ifdef USE_SHA_CRYPT + || (strcmp (method, "SHA256") == 0) + || (strcmp (method, "SHA512") == 0) +#endif /* USE_SHA_CRYPT */ + ) { + pass_max_len = -1; + } else { + pass_max_len = getdef_num ("PASS_MAX_LEN", 8); + } + } + if (!qflg) { + if (pass_max_len == -1) { + (void) printf (_( +"Enter the new password (minimum of %d characters)\n" +"Please use a combination of upper and lower case letters and numbers.\n"), + getdef_num ("PASS_MIN_LEN", 5)); + } else { + (void) printf (_( +"Enter the new password (minimum of %d, maximum of %d characters)\n" +"Please use a combination of upper and lower case letters and numbers.\n"), + getdef_num ("PASS_MIN_LEN", 5), pass_max_len); + } + } + + warned = false; + for (i = getdef_num ("PASS_CHANGE_TRIES", 5); i > 0; i--) { + cp = getpass (_("New password: ")); + if (NULL == cp) { + memzero (orig, sizeof orig); + return -1; + } + if (warned && (strcmp (pass, cp) != 0)) { + warned = false; + } + STRFCPY (pass, cp); + strzero (cp); + + if (!amroot && (!obscure (orig, pass, pw) || reuse (pass, pw))) { + (void) puts (_("Try again.")); + continue; + } + + /* + * If enabled, warn about weak passwords even if you are + * root (enter this password again to use it anyway). + * --marekm + */ + if (amroot && !warned && getdef_bool ("PASS_ALWAYS_WARN") + && (!obscure (orig, pass, pw) || reuse (pass, pw))) { + (void) puts (_("\nWarning: weak password (enter it again to use it anyway).")); + warned = true; + continue; + } + cp = getpass (_("Re-enter new password: ")); + if (NULL == cp) { + memzero (orig, sizeof orig); + return -1; + } + if (strcmp (cp, pass) != 0) { + (void) fputs (_("They don't match; try again.\n"), stderr); + } else { + strzero (cp); + break; + } + } + memzero (orig, sizeof orig); + + if (i == 0) { + memzero (pass, sizeof pass); + return -1; + } + + /* + * Encrypt the password, then wipe the cleartext password. + */ + salt = crypt_make_salt (NULL, NULL); + cp = pw_encrypt (pass, salt); + memzero (pass, sizeof pass); + + if (NULL == cp) { + fprintf (stderr, + _("%s: failed to crypt password with salt '%s': %s\n"), + Prog, salt, strerror (errno)); + return -1; + } + +#ifdef HAVE_LIBCRACK_HIST + HistUpdate (pw->pw_name, crypt_passwd); +#endif /* HAVE_LIBCRACK_HIST */ + STRFCPY (crypt_passwd, cp); + return 0; +} + +/* + * check_password - test a password to see if it can be changed + * + * check_password() sees if the invoker has permission to change the + * password for the given user. + */ +static void check_password (const struct passwd *pw, const struct spwd *sp) +{ + time_t now; + int exp_status; + + exp_status = isexpired (pw, sp); + + /* + * If not expired and the "change only if expired" option (idea from + * PAM) was specified, do nothing. --marekm + */ + if (kflg && (0 == exp_status)) { + exit (E_SUCCESS); + } + + /* + * Root can change any password any time. + */ + if (amroot) { + return; + } + + (void) time (&now); + + /* + * Expired accounts cannot be changed ever. Passwords which are + * locked may not be changed. Passwords where min > max may not be + * changed. Passwords which have been inactive too long cannot be + * changed. + */ + if ( (sp->sp_pwdp[0] == '!') + || (exp_status > 1) + || ( (sp->sp_max >= 0) + && (sp->sp_min > sp->sp_max))) { + (void) fprintf (stderr, + _("The password for %s cannot be changed.\n"), + sp->sp_namp); + SYSLOG ((LOG_WARN, "password locked for '%s'", sp->sp_namp)); + closelog (); + exit (E_NOPERM); + } + + /* + * Passwords may only be changed after sp_min time is up. + */ + if (sp->sp_lstchg > 0) { + time_t ok; + ok = (time_t) sp->sp_lstchg * SCALE; + if (sp->sp_min > 0) { + ok += (time_t) sp->sp_min * SCALE; + } + + if (now < ok) { + (void) fprintf (stderr, + _("The password for %s cannot be changed yet.\n"), + pw->pw_name); + SYSLOG ((LOG_WARN, "now < minimum age for '%s'", pw->pw_name)); + closelog (); + exit (E_NOPERM); + } + } +} +#endif /* !USE_PAM */ + +static /*@observer@*/const char *date_to_str (time_t t) +{ + static char buf[80]; + struct tm *tm; + + tm = gmtime (&t); +#ifdef HAVE_STRFTIME + (void) strftime (buf, sizeof buf, "%m/%d/%Y", tm); +#else /* !HAVE_STRFTIME */ + (void) snprintf (buf, sizeof buf, "%02d/%02d/%04d", + tm->tm_mon + 1, tm->tm_mday, tm->tm_year + 1900); +#endif /* !HAVE_STRFTIME */ + return buf; +} + +static /*@observer@*/const char *pw_status (const char *pass) +{ + if (*pass == '*' || *pass == '!') { + return "L"; + } + if (*pass == '\0') { + return "NP"; + } + return "P"; +} + +/* + * print_status - print current password status + */ +static void print_status (const struct passwd *pw) +{ + struct spwd *sp; + + sp = getspnam (pw->pw_name); /* local, no need for xgetspnam */ + if (NULL != sp) { + (void) printf ("%s %s %s %lld %lld %lld %lld\n", + pw->pw_name, + pw_status (sp->sp_pwdp), + date_to_str (sp->sp_lstchg * SCALE), + ((long long)sp->sp_min * SCALE) / DAY, + ((long long)sp->sp_max * SCALE) / DAY, + ((long long)sp->sp_warn * SCALE) / DAY, + ((long long)sp->sp_inact * SCALE) / DAY); + } else { + (void) printf ("%s %s\n", + pw->pw_name, pw_status (pw->pw_passwd)); + } +} + + +static /*@noreturn@*/void fail_exit (int status) +{ + if (pw_locked) { + if (pw_unlock () == 0) { + (void) fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + } + + if (spw_locked) { + if (spw_unlock () == 0) { + (void) fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", spw_dbname ())); + /* continue */ + } + } + + exit (status); +} + +static /*@noreturn@*/void oom (void) +{ + (void) fprintf (stderr, _("%s: out of memory\n"), Prog); + fail_exit (E_FAILURE); +} + +static char *update_crypt_pw (char *cp) +{ +#ifndef USE_PAM + if (do_update_pwd) { + cp = xstrdup (crypt_passwd); + } +#endif /* !USE_PAM */ + + if (dflg) { + *cp = '\0'; + } + + if (uflg && *cp == '!') { + if (cp[1] == '\0') { + (void) fprintf (stderr, + _("%s: unlocking the password would result in a passwordless account.\n" + "You should set a password with usermod -p to unlock the password of this account.\n"), + Prog); + fail_exit (E_FAILURE); + } else { + cp++; + } + } + + if (lflg && *cp != '!') { + char *newpw = xmalloc (strlen (cp) + 2); + + strcpy (newpw, "!"); + strcat (newpw, cp); + cp = newpw; + } + return cp; +} + + +static void update_noshadow (void) +{ + const struct passwd *pw; + struct passwd *npw; + + if (pw_lock () == 0) { + (void) fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, pw_dbname ()); + exit (E_PWDBUSY); + } + pw_locked = true; + if (pw_open (O_CREAT | O_RDWR) == 0) { + (void) fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, pw_dbname ()); + SYSLOG ((LOG_WARN, "cannot open %s", pw_dbname ())); + fail_exit (E_MISSING); + } + pw = pw_locate (name); + if (NULL == pw) { + (void) fprintf (stderr, + _("%s: user '%s' does not exist in %s\n"), + Prog, name, pw_dbname ()); + fail_exit (E_NOPERM); + } + npw = __pw_dup (pw); + if (NULL == npw) { + oom (); + } + npw->pw_passwd = update_crypt_pw (npw->pw_passwd); + if (pw_update (npw) == 0) { + (void) fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, pw_dbname (), npw->pw_name); + fail_exit (E_FAILURE); + } + if (pw_close () == 0) { + (void) fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", pw_dbname ())); + fail_exit (E_FAILURE); + } + if (pw_unlock () == 0) { + (void) fprintf (stderr, + _("%s: failed to unlock %s\n"), + Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + pw_locked = false; +} + +static void update_shadow (void) +{ + const struct spwd *sp; + struct spwd *nsp; + + if (spw_lock () == 0) { + (void) fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, spw_dbname ()); + exit (E_PWDBUSY); + } + spw_locked = true; + if (spw_open (O_CREAT | O_RDWR) == 0) { + (void) fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, spw_dbname ()); + SYSLOG ((LOG_WARN, "cannot open %s", spw_dbname ())); + fail_exit (E_FAILURE); + } + sp = spw_locate (name); + if (NULL == sp) { + /* Try to update the password in /etc/passwd instead. */ + (void) spw_close (); + update_noshadow (); + if (spw_unlock () == 0) { + (void) fprintf (stderr, + _("%s: failed to unlock %s\n"), + Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", spw_dbname ())); + /* continue */ + } + spw_locked = false; + return; + } + nsp = __spw_dup (sp); + if (NULL == nsp) { + oom (); + } + nsp->sp_pwdp = update_crypt_pw (nsp->sp_pwdp); + if (xflg) { + nsp->sp_max = (age_max * DAY) / SCALE; + } + if (nflg) { + nsp->sp_min = (age_min * DAY) / SCALE; + } + if (wflg) { + nsp->sp_warn = (warn * DAY) / SCALE; + } + if (iflg) { + nsp->sp_inact = (inact * DAY) / SCALE; + } +#ifndef USE_PAM + if (do_update_age) { + nsp->sp_lstchg = (long) gettime () / SCALE; + if (0 == nsp->sp_lstchg) { + /* Better disable aging than requiring a password + * change */ + nsp->sp_lstchg = -1; + } + } +#endif /* !USE_PAM */ + + /* + * Force change on next login, like SunOS 4.x passwd -e or Solaris + * 2.x passwd -f. Solaris 2.x seems to do the same thing (set + * sp_lstchg to 0). + */ + if (eflg) { + nsp->sp_lstchg = 0; + } + + if (spw_update (nsp) == 0) { + (void) fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, spw_dbname (), nsp->sp_namp); + fail_exit (E_FAILURE); + } + if (spw_close () == 0) { + (void) fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", spw_dbname ())); + fail_exit (E_FAILURE); + } + if (spw_unlock () == 0) { + (void) fprintf (stderr, + _("%s: failed to unlock %s\n"), + Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", spw_dbname ())); + /* continue */ + } + spw_locked = false; +} + +#ifdef WITH_SELINUX +static int check_selinux_access (const char *changed_user, + uid_t changed_uid, + access_vector_t requested_access) +{ + int status = -1; + security_context_t user_context; + context_t c; + const char *user; + + /* if in permissive mode then allow the operation */ + if (security_getenforce() == 0) { + return 0; + } + + /* get the context of the process which executed passwd */ + if (getprevcon(&user_context) != 0) { + return -1; + } + + /* get the "user" portion of the context (the part before the first + colon) */ + c = context_new(user_context); + user = context_user_get(c); + + /* if changing a password for an account with UID==0 or for an account + where the identity matches then return success */ + if (changed_uid != 0 && strcmp(changed_user, user) == 0) { + status = 0; + } else { + struct av_decision avd; + int retval; + retval = security_compute_av(user_context, + user_context, + SECCLASS_PASSWD, + requested_access, + &avd); + if ((retval == 0) && + ((requested_access & avd.allowed) == requested_access)) { + status = 0; + } + } + context_free(c); + freecon(user_context); + return status; +} + +#endif /* WITH_SELINUX */ + +/* + * passwd - change a user's password file information + * + * This command controls the password file and commands which are used + * to modify it. + * + * The valid options are + * + * -d delete the password for the named account (*) + * -e expire the password for the named account (*) + * -f execute chfn command to interpret flags + * -g execute gpasswd command to interpret flags + * -i # set sp_inact to # days (*) + * -k change password only if expired + * -l lock the password of the named account (*) + * -n # set sp_min to # days (*) + * -r # change password in # repository + * -s execute chsh command to interpret flags + * -S show password status of named account + * -u unlock the password of the named account (*) + * -w # set sp_warn to # days (*) + * -x # set sp_max to # days (*) + * + * (*) requires root permission to execute. + * + * All of the time fields are entered in days and converted to the + * appropriate internal format. For finer resolute the chage + * command must be used. + */ +int main (int argc, char **argv) +{ + const struct passwd *pw; /* Password file entry for user */ + +#ifndef USE_PAM + char *cp; /* Miscellaneous character pointing */ + + const struct spwd *sp; /* Shadow file entry for user */ +#endif /* !USE_PAM */ + + sanitize_env (); + + /* + * Get the program name. The program name is used as a prefix to + * most error messages. + */ + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + /* + * The program behaves differently when executed by root than when + * executed by a normal user. + */ + amroot = (getuid () == 0); + + OPENLOG ("passwd"); + + { + /* + * Parse the command line options. + */ + int c; + static struct option long_options[] = { + {"all", no_argument, NULL, 'a'}, + {"delete", no_argument, NULL, 'd'}, + {"expire", no_argument, NULL, 'e'}, + {"help", no_argument, NULL, 'h'}, + {"inactive", required_argument, NULL, 'i'}, + {"keep-tokens", no_argument, NULL, 'k'}, + {"lock", no_argument, NULL, 'l'}, + {"mindays", required_argument, NULL, 'n'}, + {"quiet", no_argument, NULL, 'q'}, + {"repository", required_argument, NULL, 'r'}, + {"root", required_argument, NULL, 'R'}, + {"status", no_argument, NULL, 'S'}, + {"unlock", no_argument, NULL, 'u'}, + {"warndays", required_argument, NULL, 'w'}, + {"maxdays", required_argument, NULL, 'x'}, + {NULL, 0, NULL, '\0'} + }; + + while ((c = getopt_long (argc, argv, "adehi:kln:qr:R:Suw:x:", + long_options, NULL)) != -1) { + switch (c) { + case 'a': + aflg = true; + break; + case 'd': + dflg = true; + anyflag = true; + break; + case 'e': + eflg = true; + anyflag = true; + break; + case 'h': + usage (E_SUCCESS); + /*@notreached@*/break; + case 'i': + if ( (getlong (optarg, &inact) == 0) + || (inact < -1)) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + usage (E_BAD_ARG); + } + iflg = true; + anyflag = true; + break; + case 'k': + /* change only if expired, like Linux-PAM passwd -k. */ + kflg = true; /* ok for users */ + break; + case 'l': + lflg = true; + anyflag = true; + break; + case 'n': + if ( (getlong (optarg, &age_min) == 0) + || (age_min < -1)) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + usage (E_BAD_ARG); + } + nflg = true; + anyflag = true; + break; + case 'q': + qflg = true; /* ok for users */ + break; + case 'r': + /* -r repository (files|nis|nisplus) */ + /* only "files" supported for now */ + if (strcmp (optarg, "files") != 0) { + fprintf (stderr, + _("%s: repository %s not supported\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + case 'S': + Sflg = true; /* ok for users */ + break; + case 'u': + uflg = true; + anyflag = true; + break; + case 'w': + if ( (getlong (optarg, &warn) == 0) + || (warn < -1)) { + (void) fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + usage (E_BAD_ARG); + } + wflg = true; + anyflag = true; + break; + case 'x': + if ( (getlong (optarg, &age_max) == 0) + || (age_max < -1)) { + (void) fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + usage (E_BAD_ARG); + } + xflg = true; + anyflag = true; + break; + default: + usage (E_BAD_ARG); + } + } + } + + /* + * Now I have to get the user name. The name will be gotten from the + * command line if possible. Otherwise it is figured out from the + * environment. + */ + pw = get_my_pwent (); + if (NULL == pw) { + (void) fprintf (stderr, + _("%s: Cannot determine your user name.\n"), + Prog); + SYSLOG ((LOG_WARN, "Cannot determine the user name of the caller (UID %lu)", + (unsigned long) getuid ())); + exit (E_NOPERM); + } + myname = xstrdup (pw->pw_name); + if (optind < argc) { + name = argv[optind]; + } else { + name = myname; + } + + /* + * Make sure that at most one username was specified. + */ + if (argc > (optind+1)) { + usage (E_USAGE); + } + + /* + * The -a flag requires -S, no other flags, no username, and + * you must be root. --marekm + */ + if (aflg) { + if (anyflag || !Sflg || (optind < argc)) { + usage (E_USAGE); + } + if (!amroot) { + (void) fprintf (stderr, + _("%s: Permission denied.\n"), + Prog); + exit (E_NOPERM); + } + setpwent (); + while ( (pw = getpwent ()) != NULL ) { + print_status (pw); + } + endpwent (); + exit (E_SUCCESS); + } +#if 0 + /* + * Allow certain users (administrators) to change passwords of + * certain users. Not implemented yet. --marekm + */ + if (may_change_passwd (myname, name)) + amroot = 1; +#endif + + /* + * If any of the flags were given, a user name must be supplied on + * the command line. Only an unadorned command line doesn't require + * the user's name be given. Also, -x, -n, -w, -i, -e, -d, + * -l, -u may appear with each other. -S, -k must appear alone. + */ + + /* + * -S now ok for normal users (check status of my own account), and + * doesn't require username. --marekm + */ + if (anyflag && optind >= argc) { + usage (E_USAGE); + } + + if ( (Sflg && kflg) + || (anyflag && (Sflg || kflg))) { + usage (E_USAGE); + } + + if (anyflag && !amroot) { + (void) fprintf (stderr, _("%s: Permission denied.\n"), Prog); + exit (E_NOPERM); + } + + pw = xgetpwnam (name); + if (NULL == pw) { + (void) fprintf (stderr, + _("%s: user '%s' does not exist\n"), + Prog, name); + exit (E_NOPERM); + } +#ifdef WITH_SELINUX + /* only do this check when getuid()==0 because it's a pre-condition for + changing a password without entering the old one */ + if ((is_selinux_enabled() > 0) && (getuid() == 0) && + (check_selinux_access (name, pw->pw_uid, PASSWD__PASSWD) != 0)) { + security_context_t user_context = NULL; + const char *user = "Unknown user context"; + if (getprevcon (&user_context) == 0) { + user = user_context; /* FIXME: use context_user_get? */ + } + SYSLOG ((LOG_ALERT, + "%s is not authorized to change the password of %s", + user, name)); + (void) fprintf(stderr, + _("%s: %s is not authorized to change the password of %s\n"), + Prog, user, name); + if (NULL != user_context) { + freecon (user_context); + } + exit (E_NOPERM); + } +#endif /* WITH_SELINUX */ + + /* + * If the UID of the user does not match the current real UID, + * check if I'm root. + */ + if (!amroot && (pw->pw_uid != getuid ())) { + (void) fprintf (stderr, + _("%s: You may not view or modify password information for %s.\n"), + Prog, name); + SYSLOG ((LOG_WARN, + "%s: can't view or modify password information for %s", + Prog, name)); + closelog (); + exit (E_NOPERM); + } + + if (Sflg) { + print_status (pw); + exit (E_SUCCESS); + } +#ifndef USE_PAM + /* + * The user name is valid, so let's get the shadow file entry. + */ + sp = getspnam (name); /* !USE_PAM, no need for xgetspnam */ + if (NULL == sp) { + if (errno == EACCES) { + (void) fprintf (stderr, + _("%s: Permission denied.\n"), + Prog); + exit (E_NOPERM); + } + sp = pwd_to_spwd (pw); + } + + cp = sp->sp_pwdp; + + /* + * If there are no other flags, just change the password. + */ + if (!anyflag) { + STRFCPY (crypt_passwd, cp); + + /* + * See if the user is permitted to change the password. + * Otherwise, go ahead and set a new password. + */ + check_password (pw, sp); + + /* + * Let the user know whose password is being changed. + */ + if (!qflg) { + (void) printf (_("Changing password for %s\n"), name); + } + + if (new_password (pw) != 0) { + (void) fprintf (stderr, + _("The password for %s is unchanged.\n"), + name); + closelog (); + exit (E_NOPERM); + } + do_update_pwd = true; + do_update_age = true; + } +#endif /* !USE_PAM */ + /* + * Before going any further, raise the ulimit to prevent colliding + * into a lowered ulimit, and set the real UID to root to protect + * against unexpected signals. Any keyboard signals are set to be + * ignored. + */ + pwd_init (); + +#ifdef USE_PAM + /* + * Don't set the real UID for PAM... + */ + if (!anyflag) { + do_pam_passwd (name, qflg, kflg); + exit (E_SUCCESS); + } +#endif /* USE_PAM */ + if (setuid (0) != 0) { + (void) fputs (_("Cannot change ID to root.\n"), stderr); + SYSLOG ((LOG_ERR, "can't setuid(0)")); + closelog (); + exit (E_NOPERM); + } + if (spw_file_present ()) { + update_shadow (); + } else { + update_noshadow (); + } + + nscd_flush_cache ("passwd"); + nscd_flush_cache ("group"); + + SYSLOG ((LOG_INFO, "password for '%s' changed by '%s'", name, myname)); + closelog (); + if (!qflg) { + if (!anyflag) { +#ifndef USE_PAM + (void) printf (_("%s: password changed.\n"), Prog); +#endif /* USE_PAM */ + } else { + (void) printf (_("%s: password expiry information changed.\n"), Prog); + } + } + + return E_SUCCESS; +} + diff --git a/src/pwck.c b/src/pwck.c new file mode 100644 index 0000000..523135f --- /dev/null +++ b/src/pwck.c @@ -0,0 +1,893 @@ +/* + * Copyright (c) 1992 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2001 , Michał Moskal + * Copyright (c) 2001 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2011, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <fcntl.h> +#include <grp.h> +#include <pwd.h> +#include <stdio.h> +#include <getopt.h> +#include "chkname.h" +#include "commonio.h" +#include "defines.h" +#include "prototypes.h" +#include "pwio.h" +#include "shadowio.h" +#include "getdef.h" +#include "nscd.h" +#ifdef WITH_TCB +#include "tcbfuncs.h" +#endif /* WITH_TCB */ + +/* + * Exit codes + */ +/*@-exitarg@*/ +#define E_OKAY 0 +#define E_SUCCESS 0 +#define E_USAGE 1 +#define E_BADENTRY 2 +#define E_CANTOPEN 3 +#define E_CANTLOCK 4 +#define E_CANTUPDATE 5 +#define E_CANTSORT 6 + +/* + * Global variables + */ +const char *Prog; + +static bool use_system_pw_file = true; +static bool use_system_spw_file = true; + +static bool is_shadow = false; + +static bool spw_opened = false; + +static bool pw_locked = false; +static bool spw_locked = false; + +/* Options */ +static bool read_only = false; +static bool sort_mode = false; +static bool quiet = false; /* don't report warnings, only errors */ + +/* local function prototypes */ +static void fail_exit (int code); +static /*@noreturn@*/void usage (int status); +static void process_flags (int argc, char **argv); +static void open_files (void); +static void close_files (bool changed); +static void check_pw_file (int *errors, bool *changed); +static void check_spw_file (int *errors, bool *changed); + +/* + * fail_exit - do some cleanup and exit with the given error code + */ +static void fail_exit (int code) +{ + if (spw_locked) { + if (spw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, spw_dbname ()); + if (use_system_spw_file) { + SYSLOG ((LOG_ERR, "failed to unlock %s", + spw_dbname ())); + } + /* continue */ + } + } + + if (pw_locked) { + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + if (use_system_pw_file) { + SYSLOG ((LOG_ERR, "failed to unlock %s", + pw_dbname ())); + } + /* continue */ + } + } + + closelog (); + + exit (code); +} +/* + * usage - print syntax message and exit + */ +static /*@noreturn@*/void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; +#ifdef WITH_TCB + if (getdef_bool ("USE_TCB")) { + (void) fprintf (usageout, + _("Usage: %s [options] [passwd]\n" + "\n" + "Options:\n"), + Prog); + } else +#endif /* WITH_TCB */ + { + (void) fprintf (usageout, + _("Usage: %s [options] [passwd [shadow]]\n" + "\n" + "Options:\n"), + Prog); + } + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -q, --quiet report errors only\n"), usageout); + (void) fputs (_(" -r, --read-only display errors and warnings\n" + " but do not change files\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); +#ifdef WITH_TCB + if (!getdef_bool ("USE_TCB")) +#endif /* !WITH_TCB */ + { + (void) fputs (_(" -s, --sort sort entries by UID\n"), usageout); + } + (void) fputs ("\n", usageout); + exit (status); +} + +/* + * process_flags - parse the command line options + * + * It will not return if an error is encountered. + */ +static void process_flags (int argc, char **argv) +{ + int c; + static struct option long_options[] = { + {"help", no_argument, NULL, 'h'}, + {"quiet", no_argument, NULL, 'q'}, + {"read-only", no_argument, NULL, 'r'}, + {"root", required_argument, NULL, 'R'}, + {"sort", no_argument, NULL, 's'}, + {NULL, 0, NULL, '\0'} + }; + + /* + * Parse the command line arguments + */ + while ((c = getopt_long (argc, argv, "ehqrR:s", + long_options, NULL)) != -1) { + switch (c) { + case 'h': + usage (E_SUCCESS); + /*@notreached@*/break; + case 'e': /* added for Debian shadow-961025-2 compatibility */ + case 'q': + quiet = true; + break; + case 'r': + read_only = true; + break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + case 's': + sort_mode = true; + break; + default: + usage (E_USAGE); + } + } + + if (sort_mode && read_only) { + fprintf (stderr, _("%s: -s and -r are incompatible\n"), Prog); + exit (E_USAGE); + } + + /* + * Make certain we have the right number of arguments + */ + if (argc > (optind + 2)) { + usage (E_USAGE); + } + + /* + * If there are two left over filenames, use those as the password + * and shadow password filenames. + */ + if (optind != argc) { + pw_setdbname (argv[optind]); + use_system_pw_file = false; + } + if ((optind + 2) == argc) { +#ifdef WITH_TCB + if (getdef_bool ("USE_TCB")) { + fprintf (stderr, + _("%s: no alternative shadow file allowed when USE_TCB is enabled.\n"), + Prog); + usage (E_USAGE); + } +#endif /* WITH_TCB */ + spw_setdbname (argv[optind + 1]); + is_shadow = true; + use_system_spw_file = false; + } else if (optind == argc) { + is_shadow = spw_file_present (); + } +} + +/* + * open_files - open the shadow database + * + * In read-only mode, the databases are not locked and are opened + * only for reading. + */ +static void open_files (void) +{ + bool use_tcb = false; +#ifdef WITH_TCB + use_tcb = getdef_bool ("USE_TCB"); +#endif /* WITH_TCB */ + + /* + * Lock the files if we aren't in "read-only" mode + */ + if (!read_only) { + if (pw_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, pw_dbname ()); + fail_exit (E_CANTLOCK); + } + pw_locked = true; + if (is_shadow && !use_tcb) { + if (spw_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, spw_dbname ()); + fail_exit (E_CANTLOCK); + } + spw_locked = true; + } + } + + /* + * Open the files. Use O_RDONLY if we are in read_only mode, O_RDWR + * otherwise. + */ + if (pw_open (read_only ? O_RDONLY : O_CREAT | O_RDWR) == 0) { + fprintf (stderr, _("%s: cannot open %s\n"), + Prog, pw_dbname ()); + if (use_system_pw_file) { + SYSLOG ((LOG_WARN, "cannot open %s", pw_dbname ())); + } + fail_exit (E_CANTOPEN); + } + if (is_shadow && !use_tcb) { + if (spw_open (read_only ? O_RDONLY : O_CREAT | O_RDWR) == 0) { + fprintf (stderr, _("%s: cannot open %s\n"), + Prog, spw_dbname ()); + if (use_system_spw_file) { + SYSLOG ((LOG_WARN, "cannot open %s", + spw_dbname ())); + } + fail_exit (E_CANTOPEN); + } + spw_opened = true; + } +} + +/* + * close_files - close and unlock the password/shadow databases + * + * If changed is not set, the databases are not closed, and no + * changes are committed in the databases. The databases are + * unlocked anyway. + */ +static void close_files (bool changed) +{ + /* + * All done. If there were no change we can just abandon any + * changes to the files. + */ + if (changed) { + if (pw_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, pw_dbname ()); + if (use_system_pw_file) { + SYSLOG ((LOG_ERR, + "failure while writing changes to %s", + pw_dbname ())); + } + fail_exit (E_CANTUPDATE); + } + if (spw_opened && (spw_close () == 0)) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, spw_dbname ()); + if (use_system_spw_file) { + SYSLOG ((LOG_ERR, + "failure while writing changes to %s", + spw_dbname ())); + } + fail_exit (E_CANTUPDATE); + } + spw_opened = false; + } + + /* + * Don't be anti-social - unlock the files when you're done. + */ + if (spw_locked) { + if (spw_unlock () == 0) { + fprintf (stderr, + _("%s: failed to unlock %s\n"), + Prog, spw_dbname ()); + if (use_system_spw_file) { + SYSLOG ((LOG_ERR, "failed to unlock %s", + spw_dbname ())); + } + /* continue */ + } + } + spw_locked = false; + if (pw_locked) { + if (pw_unlock () == 0) { + fprintf (stderr, + _("%s: failed to unlock %s\n"), + Prog, pw_dbname ()); + if (use_system_pw_file) { + SYSLOG ((LOG_ERR, "failed to unlock %s", + pw_dbname ())); + } + /* continue */ + } + } + pw_locked = false; +} + +/* + * check_pw_file - check the content of the passwd file + */ +static void check_pw_file (int *errors, bool *changed) +{ + struct commonio_entry *pfe, *tpfe; + struct passwd *pwd; + struct spwd *spw; + + /* + * Loop through the entire password file. + */ + for (pfe = __pw_get_head (); NULL != pfe; pfe = pfe->next) { + /* + * If this is a NIS line, skip it. You can't "know" what NIS + * is going to do without directly asking NIS ... + */ + if (('+' == pfe->line[0]) || ('-' == pfe->line[0])) { + continue; + } + + /* + * Start with the entries that are completely corrupt. They + * have no (struct passwd) entry because they couldn't be + * parsed properly. + */ + if (NULL == pfe->eptr) { + /* + * Tell the user this entire line is bogus and ask + * them to delete it. + */ + puts (_("invalid password file entry")); + printf (_("delete line '%s'? "), pfe->line); + *errors += 1; + + /* + * prompt the user to delete the entry or not + */ + if (!yes_or_no (read_only)) { + continue; + } + + /* + * All password file deletions wind up here. This + * code removes the current entry from the linked + * list. When done, it skips back to the top of the + * loop to try out the next list element. + */ + delete_pw: + if (use_system_pw_file) { + SYSLOG ((LOG_INFO, "delete passwd line '%s'", + pfe->line)); + } + *changed = true; + + __pw_del_entry (pfe); + continue; + } + + /* + * Password structure is good, start using it. + */ + pwd = pfe->eptr; + + /* + * Make sure this entry has a unique name. + */ + for (tpfe = __pw_get_head (); NULL != tpfe; tpfe = tpfe->next) { + const struct passwd *ent = tpfe->eptr; + + /* + * Don't check this entry + */ + if (tpfe == pfe) { + continue; + } + + /* + * Don't check invalid entries. + */ + if (NULL == ent) { + continue; + } + + if (strcmp (pwd->pw_name, ent->pw_name) != 0) { + continue; + } + + /* + * Tell the user this entry is a duplicate of + * another and ask them to delete it. + */ + puts (_("duplicate password entry")); + printf (_("delete line '%s'? "), pfe->line); + *errors += 1; + + /* + * prompt the user to delete the entry or not + */ + if (yes_or_no (read_only)) { + goto delete_pw; + } + } + + /* + * Check for invalid usernames. --marekm + */ + if (!is_valid_user_name (pwd->pw_name)) { + printf (_("invalid user name '%s'\n"), pwd->pw_name); + *errors += 1; + } + + /* + * Check for invalid user ID. + */ + if (pwd->pw_uid == (uid_t)-1) { + printf (_("invalid user ID '%lu'\n"), (long unsigned int)pwd->pw_uid); + *errors += 1; + } + + /* + * Make sure the primary group exists + */ + /* local, no need for xgetgrgid */ + if (!quiet && (NULL == getgrgid (pwd->pw_gid))) { + + /* + * No primary group, just give a warning + */ + + printf (_("user '%s': no group %lu\n"), + pwd->pw_name, (unsigned long) pwd->pw_gid); + *errors += 1; + } + + /* + * Make sure the home directory exists + */ + if (!quiet && (access (pwd->pw_dir, F_OK) != 0)) { + /* + * Home directory doesn't exist, give a warning + */ + printf (_("user '%s': directory '%s' does not exist\n"), + pwd->pw_name, pwd->pw_dir); + *errors += 1; + } + + /* + * Make sure the login shell is executable + */ + if ( !quiet + && ('\0' != pwd->pw_shell[0]) + && (access (pwd->pw_shell, F_OK) != 0)) { + + /* + * Login shell doesn't exist, give a warning + */ + printf (_("user '%s': program '%s' does not exist\n"), + pwd->pw_name, pwd->pw_shell); + *errors += 1; + } + + /* + * Make sure this entry exists in the /etc/shadow file. + */ + + if (is_shadow) { +#ifdef WITH_TCB + if (getdef_bool ("USE_TCB")) { + if (shadowtcb_set_user (pwd->pw_name) == SHADOWTCB_FAILURE) { + printf (_("no tcb directory for %s\n"), + pwd->pw_name); + printf (_("create tcb directory for %s?"), + pwd->pw_name); + *errors += 1; + if (yes_or_no (read_only)) { + if (shadowtcb_create (pwd->pw_name, pwd->pw_uid) == SHADOWTCB_FAILURE) { + *errors += 1; + printf (_("failed to create tcb directory for %s\n"), pwd->pw_name); + continue; + } + } else { + continue; + } + } + if (spw_lock () == 0) { + *errors += 1; + fprintf (stderr, + _("%s: cannot lock %s.\n"), + Prog, spw_dbname ()); + continue; + } + spw_locked = true; + if (spw_open (read_only ? O_RDONLY : O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, spw_dbname ()); + *errors += 1; + if (spw_unlock () == 0) { + fprintf (stderr, + _("%s: failed to unlock %s\n"), + Prog, spw_dbname ()); + if (use_system_spw_file) { + SYSLOG ((LOG_ERR, + "failed to unlock %s", + spw_dbname ())); + } + } + continue; + } + spw_opened = true; + } +#endif /* WITH_TCB */ + spw = (struct spwd *) spw_locate (pwd->pw_name); + if (NULL == spw) { + printf (_("no matching password file entry in %s\n"), + spw_dbname ()); + printf (_("add user '%s' in %s? "), + pwd->pw_name, spw_dbname ()); + *errors += 1; + if (yes_or_no (read_only)) { + struct spwd sp; + struct passwd pw; + + sp.sp_namp = pwd->pw_name; + sp.sp_pwdp = pwd->pw_passwd; + sp.sp_min = + getdef_num ("PASS_MIN_DAYS", -1); + sp.sp_max = + getdef_num ("PASS_MAX_DAYS", -1); + sp.sp_warn = + getdef_num ("PASS_WARN_AGE", -1); + sp.sp_inact = -1; + sp.sp_expire = -1; + sp.sp_flag = SHADOW_SP_FLAG_UNSET; + sp.sp_lstchg = (long) time ((time_t *) 0) / SCALE; + if (0 == sp.sp_lstchg) { + /* Better disable aging than + * requiring a password change + */ + sp.sp_lstchg = -1; + } + *changed = true; + + if (spw_update (&sp) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, spw_dbname (), sp.sp_namp); + fail_exit (E_CANTUPDATE); + } + /* remove password from /etc/passwd */ + pw = *pwd; + pw.pw_passwd = SHADOW_PASSWD_STRING; /* XXX warning: const */ + if (pw_update (&pw) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, pw_dbname (), pw.pw_name); + fail_exit (E_CANTUPDATE); + } + } + } else { + /* The passwd entry has a shadow counterpart. + * Make sure no passwords are in passwd. + */ + if ( !quiet + && (strcmp (pwd->pw_passwd, + SHADOW_PASSWD_STRING) != 0)) { + printf (_("user %s has an entry in %s, but its password field in %s is not set to 'x'\n"), + pwd->pw_name, spw_dbname (), pw_dbname ()); + *errors += 1; + } + } + } +#ifdef WITH_TCB + if (getdef_bool ("USE_TCB") && spw_locked) { + if (spw_opened && (spw_close () == 0)) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, spw_dbname ()); + if (use_system_spw_file) { + SYSLOG ((LOG_ERR, + "failure while writing changes to %s", + spw_dbname ())); + } + } else { + spw_opened = false; + } + if (spw_unlock () == 0) { + fprintf (stderr, + _("%s: failed to unlock %s\n"), + Prog, spw_dbname ()); + if (use_system_spw_file) { + SYSLOG ((LOG_ERR, "failed to unlock %s", + spw_dbname ())); + } + } else { + spw_locked = false; + } + } +#endif /* WITH_TCB */ + } +} + +/* + * check_spw_file - check the content of the shadowed password file (shadow) + */ +static void check_spw_file (int *errors, bool *changed) +{ + struct commonio_entry *spe, *tspe; + struct spwd *spw; + + /* + * Loop through the entire shadow password file. + */ + for (spe = __spw_get_head (); NULL != spe; spe = spe->next) { + /* + * Do not treat lines which were missing in shadow + * and were added earlier. + */ + if (NULL == spe->line) { + continue; + } + + /* + * If this is a NIS line, skip it. You can't "know" what NIS + * is going to do without directly asking NIS ... + */ + if (('+' == spe->line[0]) || ('-' == spe->line[0])) { + continue; + } + + /* + * Start with the entries that are completely corrupt. They + * have no (struct spwd) entry because they couldn't be + * parsed properly. + */ + if (NULL == spe->eptr) { + /* + * Tell the user this entire line is bogus and ask + * them to delete it. + */ + puts (_("invalid shadow password file entry")); + printf (_("delete line '%s'? "), spe->line); + *errors += 1; + + /* + * prompt the user to delete the entry or not + */ + if (!yes_or_no (read_only)) { + continue; + } + + /* + * All shadow file deletions wind up here. This code + * removes the current entry from the linked list. + * When done, it skips back to the top of the loop + * to try out the next list element. + */ + delete_spw: + if (use_system_spw_file) { + SYSLOG ((LOG_INFO, "delete shadow line '%s'", + spe->line)); + } + *changed = true; + + __spw_del_entry (spe); + continue; + } + + /* + * Shadow password structure is good, start using it. + */ + spw = spe->eptr; + + /* + * Make sure this entry has a unique name. + */ + for (tspe = __spw_get_head (); NULL != tspe; tspe = tspe->next) { + const struct spwd *ent = tspe->eptr; + + /* + * Don't check this entry + */ + if (tspe == spe) { + continue; + } + + /* + * Don't check invalid entries. + */ + if (NULL == ent) { + continue; + } + + if (strcmp (spw->sp_namp, ent->sp_namp) != 0) { + continue; + } + + /* + * Tell the user this entry is a duplicate of + * another and ask them to delete it. + */ + puts (_("duplicate shadow password entry")); + printf (_("delete line '%s'? "), spe->line); + *errors += 1; + + /* + * prompt the user to delete the entry or not + */ + if (yes_or_no (read_only)) { + goto delete_spw; + } + } + + /* + * Make sure this entry exists in the /etc/passwd + * file. + */ + if (pw_locate (spw->sp_namp) == NULL) { + /* + * Tell the user this entry has no matching + * /etc/passwd entry and ask them to delete it. + */ + printf (_("no matching password file entry in %s\n"), + pw_dbname ()); + printf (_("delete line '%s'? "), spe->line); + *errors += 1; + + /* + * prompt the user to delete the entry or not + */ + if (yes_or_no (read_only)) { + goto delete_spw; + } + } + + /* + * Warn if last password change in the future. --marekm + */ + if (!quiet) { + time_t t = time ((time_t *) 0); + if ( (t != 0) + && (spw->sp_lstchg > (long) t / SCALE)) { + printf (_("user %s: last password change in the future\n"), + spw->sp_namp); + *errors += 1; + } + } + } +} + +/* + * pwck - verify password file integrity + */ +int main (int argc, char **argv) +{ + int errors = 0; + bool changed = false; + + /* + * Get my name so that I can use it to report errors. + */ + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + OPENLOG ("pwck"); + + /* Parse the command line arguments */ + process_flags (argc, argv); + + open_files (); + + if (sort_mode) { + if (pw_sort () != 0) { + fprintf (stderr, + _("%s: cannot sort entries in %s\n"), + Prog, pw_dbname ()); + fail_exit (E_CANTSORT); + } + if (is_shadow) { + if (spw_sort () != 0) { + fprintf (stderr, + _("%s: cannot sort entries in %s\n"), + Prog, spw_dbname ()); + fail_exit (E_CANTSORT); + } + } + changed = true; + } else { + check_pw_file (&errors, &changed); + + if (is_shadow) { + check_spw_file (&errors, &changed); + } + } + + close_files (changed); + + nscd_flush_cache ("passwd"); + + /* + * Tell the user what we did and exit. + */ + if (0 != errors) { + printf (changed ? + _("%s: the files have been updated\n") : + _("%s: no changes\n"), Prog); + } + + closelog (); + return ((0 != errors) ? E_BADENTRY : E_OKAY); +} + diff --git a/src/pwconv.c b/src/pwconv.c new file mode 100644 index 0000000..e2d61f8 --- /dev/null +++ b/src/pwconv.c @@ -0,0 +1,333 @@ +/* + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2002 - 2006, Tomasz Kłoczko + * Copyright (c) 2009 - 2012, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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. + */ + +/* + * pwconv - create or update /etc/shadow with information from + * /etc/passwd. + * + * It is more like SysV pwconv, slightly different from the original Shadow + * pwconv. Depends on "x" as password in /etc/passwd which means that the + * password has already been moved to /etc/shadow. There is no need to move + * /etc/npasswd to /etc/passwd, password files are updated using library + * routines with proper locking. + * + * Can be used to update /etc/shadow after adding/deleting users by editing + * /etc/passwd. There is no man page yet, but this program should be close + * to pwconv(1M) on Solaris 2.x. + * + * Warning: make sure that all users have "x" as the password in /etc/passwd + * before running this program for the first time on a system which already + * has shadow passwords. Anything else (like "*" from old versions of the + * shadow suite) will replace the user's encrypted password in /etc/shadow. + * + * Doesn't currently support pw_age information in /etc/passwd, and doesn't + * support DBM files. Add it if you need it... + * + */ + +#include <config.h> + +#ident "$Id$" + +#include <errno.h> +#include <fcntl.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <getopt.h> +#include "defines.h" +#include "getdef.h" +#include "prototypes.h" +#include "pwio.h" +#include "shadowio.h" +#include "nscd.h" + +/* + * exit status values + */ +/*@-exitarg@*/ +#define E_SUCCESS 0 /* success */ +#define E_NOPERM 1 /* permission denied */ +#define E_USAGE 2 /* invalid command syntax */ +#define E_FAILURE 3 /* unexpected failure, nothing done */ +#define E_MISSING 4 /* unexpected failure, passwd file missing */ +#define E_PWDBUSY 5 /* passwd file(s) busy */ +#define E_BADENTRY 6 /* bad shadow entry */ +/* + * Global variables + */ +const char *Prog; + +static bool spw_locked = false; +static bool pw_locked = false; + +/* local function prototypes */ +static void fail_exit (int status); +static void usage (int status); +static void process_flags (int argc, char **argv); + +static void fail_exit (int status) +{ + if (pw_locked) { + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + } + + if (spw_locked) { + if (spw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", spw_dbname ())); + /* continue */ + } + } + + exit (status); +} + +static void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options]\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs ("\n", usageout); + exit (status); +} + +/* + * process_flags - parse the command line options + * + * It will not return if an error is encountered. + */ +static void process_flags (int argc, char **argv) +{ + /* + * Parse the command line options. + */ + int c; + static struct option long_options[] = { + {"help", no_argument, NULL, 'h'}, + {"root", required_argument, NULL, 'R'}, + {NULL, 0, NULL, '\0'} + }; + + while ((c = getopt_long (argc, argv, "hR:", + long_options, NULL)) != -1) { + switch (c) { + case 'h': + usage (E_SUCCESS); + /*@notreached@*/break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + default: + usage (E_USAGE); + } + } + + if (optind != argc) { + usage (E_USAGE); + } +} + +int main (int argc, char **argv) +{ + const struct passwd *pw; + struct passwd pwent; + const struct spwd *sp; + struct spwd spent; + + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + OPENLOG ("pwconv"); + + process_flags (argc, argv); + +#ifdef WITH_TCB + if (getdef_bool("USE_TCB")) { + fprintf (stderr, _("%s: can't work with tcb enabled\n"), Prog); + exit (E_FAILURE); + } +#endif /* WITH_TCB */ + + if (pw_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, pw_dbname ()); + fail_exit (E_PWDBUSY); + } + pw_locked = true; + if (pw_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), Prog, pw_dbname ()); + fail_exit (E_MISSING); + } + + if (spw_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, spw_dbname ()); + fail_exit (E_PWDBUSY); + } + spw_locked = true; + if (spw_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), Prog, spw_dbname ()); + fail_exit (E_FAILURE); + } + + /* + * Remove /etc/shadow entries for users not in /etc/passwd. + */ + (void) spw_rewind (); + while ((sp = spw_next ()) != NULL) { + if (pw_locate (sp->sp_namp) != NULL) { + continue; + } + + if (spw_remove (sp->sp_namp) == 0) { + /* + * This shouldn't happen (the entry exists) but... + */ + fprintf (stderr, + _("%s: cannot remove entry '%s' from %s\n"), + Prog, sp->sp_namp, spw_dbname ()); + fail_exit (E_FAILURE); + } + } + + /* + * Update shadow entries which don't have "x" as pw_passwd. Add any + * missing shadow entries. + */ + (void) pw_rewind (); + while ((pw = pw_next ()) != NULL) { + sp = spw_locate (pw->pw_name); + if (NULL != sp) { + /* do we need to update this entry? */ + if (strcmp (pw->pw_passwd, SHADOW_PASSWD_STRING) == 0) { + continue; + } + /* update existing shadow entry */ + spent = *sp; + } else { + /* add new shadow entry */ + memset (&spent, 0, sizeof spent); + spent.sp_namp = pw->pw_name; + spent.sp_min = getdef_num ("PASS_MIN_DAYS", -1); + spent.sp_max = getdef_num ("PASS_MAX_DAYS", -1); + spent.sp_warn = getdef_num ("PASS_WARN_AGE", -1); + spent.sp_inact = -1; + spent.sp_expire = -1; + spent.sp_flag = SHADOW_SP_FLAG_UNSET; + } + spent.sp_pwdp = pw->pw_passwd; + spent.sp_lstchg = (long) time ((time_t *) 0) / SCALE; + if (0 == spent.sp_lstchg) { + /* Better disable aging than requiring a password + * change */ + spent.sp_lstchg = -1; + } + if (spw_update (&spent) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, spw_dbname (), spent.sp_namp); + fail_exit (E_FAILURE); + } + + /* remove password from /etc/passwd */ + pwent = *pw; + pwent.pw_passwd = SHADOW_PASSWD_STRING; /* XXX warning: const */ + if (pw_update (&pwent) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, pw_dbname (), pwent.pw_name); + fail_exit (E_FAILURE); + } + } + + if (spw_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", spw_dbname ())); + fail_exit (E_FAILURE); + } + if (pw_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", pw_dbname ())); + fail_exit (E_FAILURE); + } + + /* /etc/passwd- (backup file) */ + errno = 0; + if ((chmod (PASSWD_FILE "-", 0600) != 0) && (errno != ENOENT)) { + fprintf (stderr, + _("%s: failed to change the mode of %s to 0600\n"), + Prog, PASSWD_FILE "-"); + SYSLOG ((LOG_ERR, "failed to change the mode of %s to 0600", PASSWD_FILE "-")); + /* continue */ + } + + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + + if (spw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", spw_dbname ())); + /* continue */ + } + + nscd_flush_cache ("passwd"); + + return E_SUCCESS; +} + diff --git a/src/pwunconv.c b/src/pwunconv.c new file mode 100644 index 0000000..fabf023 --- /dev/null +++ b/src/pwunconv.c @@ -0,0 +1,256 @@ +/* + * Copyright (c) 1989 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2001 - 2005, Tomasz Kłoczko + * Copyright (c) 2008 - 2012, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <fcntl.h> +#include <pwd.h> +#include <stdio.h> +#include <sys/types.h> +#include <unistd.h> +#include <getopt.h> +#include "defines.h" +#include "nscd.h" +#include "prototypes.h" +#include "pwio.h" +#include "shadowio.h" +/*@-exitarg@*/ +#include "exitcodes.h" + +/* + * Global variables + */ +const char *Prog; + +static bool spw_locked = false; +static bool pw_locked = false; + +/* local function prototypes */ +static void fail_exit (int status); +static void usage (int status); +static void process_flags (int argc, char **argv); + +static void fail_exit (int status) +{ + if (spw_locked) { + if (spw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", spw_dbname ())); + /* continue */ + } + } + if (pw_locked) { + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + } + exit (status); +} + +static void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options]\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs ("\n", usageout); + exit (status); +} + +/* + * process_flags - parse the command line options + * + * It will not return if an error is encountered. + */ +static void process_flags (int argc, char **argv) +{ + /* + * Parse the command line options. + */ + int c; + static struct option long_options[] = { + {"help", no_argument, NULL, 'h'}, + {"root", required_argument, NULL, 'R'}, + {NULL, 0, NULL, '\0'} + }; + + while ((c = getopt_long (argc, argv, "hR:", + long_options, NULL)) != -1) { + switch (c) { + case 'h': + usage (E_SUCCESS); + /*@notreached@*/break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + default: + usage (E_USAGE); + } + } + + if (optind != argc) { + usage (E_USAGE); + } +} + +int main (int argc, char **argv) +{ + const struct passwd *pw; + struct passwd pwent; + const struct spwd *spwd; + + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + OPENLOG ("pwunconv"); + + process_flags (argc, argv); + +#ifdef WITH_TCB + if (getdef_bool("USE_TCB")) { + fprintf (stderr, _("%s: can't work with tcb enabled\n"), Prog); + exit (1); + } +#endif /* WITH_TCB */ + + if (!spw_file_present ()) { + /* shadow not installed, do nothing */ + exit (0); + } + + if (pw_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, pw_dbname ()); + fail_exit (5); + } + pw_locked = true; + if (pw_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, pw_dbname ()); + fail_exit (1); + } + + if (spw_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, spw_dbname ()); + fail_exit (5); + } + spw_locked = true; + if (spw_open (O_RDONLY) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, spw_dbname ()); + fail_exit (1); + } + + (void) pw_rewind (); + while ((pw = pw_next ()) != NULL) { + spwd = spw_locate (pw->pw_name); + if (NULL == spwd) { + continue; + } + + pwent = *pw; + + /* + * Update password if non-shadow is "x". + */ + if (strcmp (pw->pw_passwd, SHADOW_PASSWD_STRING) == 0) { + pwent.pw_passwd = spwd->sp_pwdp; + } + + /* + * Password aging works differently in the two different + * systems. With shadow password files you apparently must + * have some aging information. The maxweeks or minweeks + * may not map exactly. In pwconv we set max == 10000, + * which is about 30 years. Here we have to undo that + * kludge. So, if maxdays == 10000, no aging information is + * put into the new file. Otherwise, the days are converted + * to weeks and so on. + */ + if (pw_update (&pwent) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, pw_dbname (), pwent.pw_name); + fail_exit (3); + } + } + + (void) spw_close (); /* was only open O_RDONLY */ + + if (pw_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", pw_dbname ())); + fail_exit (3); + } + + if (unlink (SHADOW) != 0) { + fprintf (stderr, + _("%s: cannot delete %s\n"), Prog, SHADOW); + SYSLOG ((LOG_ERR, "cannot delete %s", SHADOW)); + fail_exit (3); + } + + if (spw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", spw_dbname ())); + /* continue */ + } + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + + nscd_flush_cache ("passwd"); + + return 0; +} + diff --git a/src/su.c b/src/su.c new file mode 100644 index 0000000..974048e --- /dev/null +++ b/src/su.c @@ -0,0 +1,1222 @@ +/* + * Copyright (c) 1989 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2000 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2013, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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. + */ + +/* Some parts substantially derived from an ancestor of: + su for GNU. Run a shell with substitute user and group IDs. + + Copyright (C) 1992-2003 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. */ + + +#include <config.h> + +#ident "$Id$" + +#include <getopt.h> +#include <grp.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <sys/types.h> +#include <unistd.h> +#ifndef USE_PAM +#include <sys/ioctl.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#endif /* !USE_PAM */ +#include "prototypes.h" +#include "defines.h" +#include "pwauth.h" +#include "getdef.h" +#ifdef USE_PAM +#include "pam_defs.h" +#endif /* USE_PAM */ +/*@-exitarg@*/ +#include "exitcodes.h" + +/* + * Global variables + */ +const char *Prog; +static /*@observer@*/const char *caller_tty = NULL; /* Name of tty SU is run from */ +static bool caller_is_root = false; +static uid_t caller_uid; +#ifndef USE_PAM +static bool caller_on_console = false; +#ifdef SU_ACCESS +static /*@only@*/char *caller_pass; +#endif +#endif /* !USE_PAM */ +static bool doshell = false; +static bool fakelogin = false; +static /*@observer@*/const char *shellstr; +static /*@null@*/char *command = NULL; + + +/* not needed by sulog.c anymore */ +static char name[BUFSIZ]; +static char caller_name[BUFSIZ]; + +/* If nonzero, change some environment vars to indicate the user su'd to. */ +static bool change_environment = true; + +#ifdef USE_PAM +static char kill_msg[256]; +static char wait_msg[256]; +static pam_handle_t *pamh = NULL; +static int caught = 0; +/* PID of the child, in case it needs to be killed */ +static pid_t pid_child = 0; +#endif + +/* + * External identifiers + */ + +extern char **newenvp; /* libmisc/env.c */ +extern size_t newenvc; /* libmisc/env.c */ + +/* local function prototypes */ + +static void execve_shell (const char *shellname, + char *args[], + char *const envp[]); +#ifdef USE_PAM +static RETSIGTYPE kill_child (int unused(s)); +static void prepare_pam_close_session (void); +#else /* !USE_PAM */ +static RETSIGTYPE die (int); +static bool iswheel (const char *); +#endif /* !USE_PAM */ +static bool restricted_shell (const char *shellname); +static /*@noreturn@*/void su_failure (const char *tty, bool su_to_root); +static /*@only@*/struct passwd * check_perms (void); +#ifdef USE_PAM +static void check_perms_pam (const struct passwd *pw); +#else /* !USE_PAM */ +static void check_perms_nopam (const struct passwd *pw); +#endif /* !USE_PAM */ +static void save_caller_context (char **argv); +static void process_flags (int argc, char **argv); +static void set_environment (struct passwd *pw); + +#ifndef USE_PAM +/* + * die - set or reset termio modes. + * + * die() is called before processing begins. signal() is then called + * with die() as the signal handler. If signal later calls die() with a + * signal number, the terminal modes are then reset. + */ +static RETSIGTYPE die (int killed) +{ + static TERMIO sgtty; + + if (killed != 0) { + STTY (0, &sgtty); + } else { + GTTY (0, &sgtty); + } + + if (killed != 0) { + _exit (128+killed); + } +} + +static bool iswheel (const char *username) +{ + struct group *grp; + + grp = getgrnam ("wheel"); /* !USE_PAM, no need for xgetgrnam */ + if ( (NULL ==grp) + || (NULL == grp->gr_mem)) { + return false; + } + return is_on_list (grp->gr_mem, username); +} +#else /* USE_PAM */ +static RETSIGTYPE kill_child (int unused(s)) +{ + if (0 != pid_child) { + (void) kill (-pid_child, SIGKILL); + (void) write (STDERR_FILENO, kill_msg, strlen (kill_msg)); + } else { + (void) write (STDERR_FILENO, wait_msg, strlen (wait_msg)); + } + _exit (255); +} +#endif /* USE_PAM */ + +/* borrowed from GNU sh-utils' "su.c" */ +static bool restricted_shell (const char *shellname) +{ + /*@observer@*/const char *line; + + setusershell (); + while ((line = getusershell ()) != NULL) { + if (('#' != *line) && (strcmp (line, shellname) == 0)) { + endusershell (); + return false; + } + } + endusershell (); + return true; +} + +static /*@noreturn@*/void su_failure (const char *tty, bool su_to_root) +{ + sulog (tty, false, caller_name, name); /* log failed attempt */ +#ifdef USE_SYSLOG + if (getdef_bool ("SYSLOG_SU_ENAB")) { + SYSLOG ((su_to_root ? LOG_NOTICE : LOG_INFO, + "- %s %s:%s", tty, + ('\0' != caller_name[0]) ? caller_name : "???", + ('\0' != name[0]) ? name : "???")); + } + closelog (); +#endif + +#ifdef WITH_AUDIT + audit_fd = audit_open (); + audit_log_acct_message (audit_fd, + AUDIT_USER_ROLE_CHANGE, + NULL, /* Prog. name */ + "su", + ('\0' != caller_name[0]) ? caller_name : "???", + AUDIT_NO_ID, + "localhost", + NULL, /* addr */ + tty, + 0); /* result */ + close (audit_fd); +#endif /* WITH_AUDIT */ + + exit (1); +} + +/* + * execve_shell - Execute a shell with execve, or interpret it with + * /bin/sh + */ +static void execve_shell (const char *shellname, + char *args[], + char *const envp[]) +{ + int err; + (void) execve (shellname, (char **) args, envp); + err = errno; + + if (access (shellname, R_OK|X_OK) == 0) { + /* + * Assume this is a shell script (with no shebang). + * Interpret it with /bin/sh + */ + size_t n_args = 0; + char **targs; + while (NULL != args[n_args]) { + n_args++; + } + targs = (char **) xmalloc ((n_args + 3) * sizeof (args[0])); + targs[0] = "sh"; + targs[1] = "-"; + targs[2] = xstrdup (shellname); + targs[n_args+2] = NULL; + while (1 != n_args) { + targs[n_args+1] = args[n_args - 1]; + n_args--; + } + + (void) execve (SHELL, targs, envp); + } else { + errno = err; + } +} + +#ifdef USE_PAM +/* Signal handler for parent process later */ +static void catch_signals (int sig) +{ + caught = sig; +} + +/* + * prepare_pam_close_session - Fork and wait for the child to close the session + * + * Only the child returns. The parent will wait for the child to + * terminate and exit. + */ +static void prepare_pam_close_session (void) +{ + sigset_t ourset; + int status; + int ret; + + pid_child = fork (); + if (pid_child == 0) { /* child shell */ + return; /* Only the child will return from pam_create_session */ + } else if ((pid_t)-1 == pid_child) { + (void) fprintf (stderr, + _("%s: Cannot fork user shell\n"), + Prog); + SYSLOG ((LOG_WARN, "Cannot execute %s", shellstr)); + closelog (); + exit (1); + /* Only the child returns. See above. */ + } + + /* parent only */ + sigfillset (&ourset); + if (sigprocmask (SIG_BLOCK, &ourset, NULL) != 0) { + (void) fprintf (stderr, + _("%s: signal malfunction\n"), + Prog); + caught = SIGTERM; + } + if (0 == caught) { + struct sigaction action; + + action.sa_handler = catch_signals; + sigemptyset (&action.sa_mask); + action.sa_flags = 0; + sigemptyset (&ourset); + + if ( (sigaddset (&ourset, SIGTERM) != 0) + || (sigaddset (&ourset, SIGALRM) != 0) + || (sigaction (SIGTERM, &action, NULL) != 0) + || ( !doshell /* handle SIGINT (Ctrl-C), SIGQUIT + * (Ctrl-\), and SIGTSTP (Ctrl-Z) + * since the child will not control + * the tty. + */ + && ( (sigaddset (&ourset, SIGINT) != 0) + || (sigaddset (&ourset, SIGQUIT) != 0) + || (sigaddset (&ourset, SIGTSTP) != 0) + || (sigaction (SIGINT, &action, NULL) != 0) + || (sigaction (SIGQUIT, &action, NULL) != 0) + || (sigaction (SIGTSTP, &action, NULL) != 0))) + || (sigprocmask (SIG_UNBLOCK, &ourset, NULL) != 0) + ) { + fprintf (stderr, + _("%s: signal masking malfunction\n"), + Prog); + caught = SIGTERM; + } + } + + if (0 == caught) { + bool stop = true; + + do { + pid_t pid; + stop = true; + + pid = waitpid (-1, &status, WUNTRACED); + + /* When interrupted by signal, the signal will be + * forwarded to the child, and termination will be + * forced later. + */ + if ( ((pid_t)-1 == pid) + && (EINTR == errno) + && (SIGTSTP == caught)) { + caught = 0; + /* Except for SIGTSTP, which request to + * stop the child. + * We will SIGSTOP ourself on the next + * waitpid round. + */ + kill (pid_child, SIGSTOP); + stop = false; + } else if ( ((pid_t)-1 != pid) + && (0 != WIFSTOPPED (status))) { + /* The child (shell) was suspended. + * Suspend su. */ + kill (getpid (), SIGSTOP); + /* wake child when resumed */ + kill (pid, SIGCONT); + stop = false; + } else if ( (pid_t)-1 != pid) { + pid_child = 0; + } + } while (!stop); + } + + if (0 != caught && 0 != pid_child) { + (void) fputs ("\n", stderr); + (void) fputs (_("Session terminated, terminating shell..."), + stderr); + (void) kill (-pid_child, caught); + + snprintf (kill_msg, sizeof kill_msg, _(" ...killed.\n")); + snprintf (wait_msg, sizeof wait_msg, _(" ...waiting for child to terminate.\n")); + + (void) signal (SIGALRM, kill_child); + (void) signal (SIGCHLD, catch_signals); + (void) alarm (2); + + sigemptyset (&ourset); + if ((sigaddset (&ourset, SIGALRM) != 0) + || (sigprocmask (SIG_BLOCK, &ourset, NULL) != 0)) { + fprintf (stderr, _("%s: signal masking malfunction\n"), Prog); + kill_child (0); + } else { + while (0 == waitpid (pid_child, &status, WNOHANG)) { + sigsuspend (&ourset); + } + pid_child = 0; + (void) sigprocmask (SIG_UNBLOCK, &ourset, NULL); + } + + (void) fputs (_(" ...terminated.\n"), stderr); + } + + ret = pam_close_session (pamh, 0); + if (PAM_SUCCESS != ret) { + SYSLOG ((LOG_ERR, "pam_close_session: %s", + pam_strerror (pamh, ret))); + fprintf (stderr, _("%s: %s\n"), Prog, pam_strerror (pamh, ret)); + } + + (void) pam_setcred (pamh, PAM_DELETE_CRED); + (void) pam_end (pamh, PAM_SUCCESS); + + exit ((0 != WIFEXITED (status)) ? WEXITSTATUS (status) + : WTERMSIG (status) + 128); + /* Only the child returns. See above. */ +} +#endif /* USE_PAM */ + +/* + * usage - print command line syntax and exit + */ +static void usage (int status) +{ + (void) + fputs (_("Usage: su [options] [LOGIN]\n" + "\n" + "Options:\n" + " -c, --command COMMAND pass COMMAND to the invoked shell\n" + " -h, --help display this help message and exit\n" + " -, -l, --login make the shell a login shell\n" + " -m, -p,\n" + " --preserve-environment do not reset environment variables, and\n" + " keep the same shell\n" + " -s, --shell SHELL use SHELL instead of the default in passwd\n" + "\n"), (E_SUCCESS != status) ? stderr : stdout); + exit (status); +} + +#ifdef USE_PAM +static void check_perms_pam (const struct passwd *pw) +{ + int ret; + ret = pam_authenticate (pamh, 0); + if (PAM_SUCCESS != ret) { + SYSLOG (((pw->pw_uid != 0)? LOG_NOTICE : LOG_WARN, "pam_authenticate: %s", + pam_strerror (pamh, ret))); + fprintf (stderr, _("%s: %s\n"), Prog, pam_strerror (pamh, ret)); + (void) pam_end (pamh, ret); + su_failure (caller_tty, 0 == pw->pw_uid); + } + + ret = pam_acct_mgmt (pamh, 0); + if (PAM_SUCCESS != ret) { + if (caller_is_root) { + fprintf (stderr, + _("%s: %s\n(Ignored)\n"), + Prog, pam_strerror (pamh, ret)); + } else if (PAM_NEW_AUTHTOK_REQD == ret) { + ret = pam_chauthtok (pamh, PAM_CHANGE_EXPIRED_AUTHTOK); + if (PAM_SUCCESS != ret) { + SYSLOG ((LOG_ERR, "pam_chauthtok: %s", + pam_strerror (pamh, ret))); + fprintf (stderr, + _("%s: %s\n"), + Prog, pam_strerror (pamh, ret)); + (void) pam_end (pamh, ret); + su_failure (caller_tty, 0 == pw->pw_uid); + } + } else { + SYSLOG ((LOG_ERR, "pam_acct_mgmt: %s", + pam_strerror (pamh, ret))); + fprintf (stderr, + _("%s: %s\n"), + Prog, pam_strerror (pamh, ret)); + (void) pam_end (pamh, ret); + su_failure (caller_tty, 0 == pw->pw_uid); + } + } +} +#else /* !USE_PAM */ +static void check_perms_nopam (const struct passwd *pw) +{ + /*@observer@*/const struct spwd *spwd = NULL; + /*@observer@*/const char *password = pw->pw_passwd; + RETSIGTYPE (*oldsig) (int); + + if (caller_is_root) { + return; + } + + /* + * BSD systems only allow "wheel" to SU to root. USG systems don't, + * so we make this a configurable option. + */ + + /* The original Shadow 3.3.2 did this differently. Do it like BSD: + * + * - check for UID 0 instead of name "root" - there are systems with + * several root accounts under different names, + * + * - check the contents of /etc/group instead of the current group + * set (you must be listed as a member, GID 0 is not sufficient). + * + * In addition to this traditional feature, we now have complete su + * access control (allow, deny, no password, own password). Thanks + * to Chris Evans <lady0110@sable.ox.ac.uk>. + */ + + if ( (0 == pw->pw_uid) + && getdef_bool ("SU_WHEEL_ONLY") + && !iswheel (caller_name)) { + fprintf (stderr, + _("You are not authorized to su %s\n"), + name); + exit (1); + } + spwd = getspnam (name); /* !USE_PAM, no need for xgetspnam */ +#ifdef SU_ACCESS + if (strcmp (pw->pw_passwd, SHADOW_PASSWD_STRING) == 0) { + if (NULL != spwd) { + password = spwd->sp_pwdp; + } + } + + switch (check_su_auth (caller_name, name, 0 == pw->pw_uid)) { + case 0: /* normal su, require target user's password */ + break; + case 1: /* require no password */ + password = ""; /* XXX warning: const */ + break; + case 2: /* require own password */ + (void) puts (_("(Enter your own password)")); + password = caller_pass; + break; + default: /* access denied (-1) or unexpected value */ + fprintf (stderr, + _("You are not authorized to su %s\n"), + name); + exit (1); + } +#endif /* SU_ACCESS */ + /* + * Set up a signal handler in case the user types QUIT. + */ + die (0); + oldsig = signal (SIGQUIT, die); + + /* + * See if the system defined authentication method is being used. + * The first character of an administrator defined method is an '@' + * character. + */ + if (pw_auth (password, name, PW_SU, (char *) 0) != 0) { + SYSLOG (((pw->pw_uid != 0)? LOG_NOTICE : LOG_WARN, + "Authentication failed for %s", name)); + fprintf(stderr, _("%s: Authentication failure\n"), Prog); + su_failure (caller_tty, 0 == pw->pw_uid); + } + (void) signal (SIGQUIT, oldsig); + + /* + * Check to see if the account is expired. root gets to ignore any + * expired accounts, but normal users can't become a user with an + * expired password. + */ + if (NULL != spwd) { + (void) expire (pw, spwd); + } + + /* + * Check to see if the account permits "su". root gets to ignore any + * restricted accounts, but normal users can't become a user if + * there is a "SU" entry in the /etc/porttime file denying access to + * the account. + */ + if (!isttytime (name, "SU", time ((time_t *) 0))) { + SYSLOG (((0 != pw->pw_uid) ? LOG_WARN : LOG_CRIT, + "SU by %s to restricted account %s", + caller_name, name)); + fprintf (stderr, + _("%s: You are not authorized to su at that time\n"), + Prog); + su_failure (caller_tty, 0 == pw->pw_uid); + } +} +#endif /* !USE_PAM */ + +/* + * check_perms - check permissions to switch to the user 'name' + * + * In case of subsystem login, the user is first authenticated in the + * caller's root subsystem, and then in the user's target subsystem. + */ +static /*@only@*/struct passwd * check_perms (void) +{ +#ifdef USE_PAM + const char *tmp_name; + int ret; +#endif /* !USE_PAM */ + /* + * The password file entries for the user is gotten and the account + * validated. + */ + struct passwd *pw = xgetpwnam (name); + if (NULL == pw) { + (void) fprintf (stderr, + _("No passwd entry for user '%s'\n"), name); + SYSLOG ((LOG_NOTICE, "No passwd entry for user '%s'", name)); + su_failure (caller_tty, true); + } + + (void) signal (SIGINT, SIG_IGN); + (void) signal (SIGQUIT, SIG_IGN); + +#ifdef USE_PAM + check_perms_pam (pw); + /* PAM authentication can request a change of account */ + ret = pam_get_item(pamh, PAM_USER, (const void **) &tmp_name); + if (ret != PAM_SUCCESS) { + SYSLOG((LOG_ERR, "pam_get_item: internal PAM error\n")); + (void) fprintf (stderr, + "%s: Internal PAM error retrieving username\n", + Prog); + (void) pam_end (pamh, ret); + su_failure (caller_tty, 0 == pw->pw_uid); + } + if (strcmp (name, tmp_name) != 0) { + SYSLOG ((LOG_INFO, + "Change user from '%s' to '%s' as requested by PAM", + name, tmp_name)); + strncpy (name, tmp_name, sizeof(name) - 1); + name[sizeof(name) - 1] = '\0'; + pw = xgetpwnam (name); + if (NULL == pw) { + (void) fprintf (stderr, + _("No passwd entry for user '%s'\n"), + name); + SYSLOG ((LOG_NOTICE, + "No passwd entry for user '%s'", name)); + su_failure (caller_tty, true); + } + } +#else /* !USE_PAM */ + check_perms_nopam (pw); +#endif /* !USE_PAM */ + + (void) signal (SIGINT, SIG_DFL); + (void) signal (SIGQUIT, SIG_DFL); + + /* + * Even if --shell is specified, the subsystem login test is based on + * the shell specified in /etc/passwd (not the one specified with + * --shell, which will be the one executed in the chroot later). + */ + if ('*' == pw->pw_shell[0]) { /* subsystem root required */ + subsystem (pw); /* change to the subsystem root */ + endpwent (); /* close the old password databases */ + endspent (); + pw_free (pw); + return check_perms (); /* authenticate in the subsystem */ + } + + return pw; +} + +/* + * save_caller_context - save information from the call context + * + * Save the program's name (Prog), caller's UID (caller_uid / + * caller_is_root), name (caller_name), and password (caller_pass), + * the TTY (ttyp), and whether su was called from a console + * (is_console) for further processing and before they might change. + */ +static void save_caller_context (char **argv) +{ + struct passwd *pw = NULL; +#ifndef USE_PAM +#ifdef SU_ACCESS + const char *password = NULL; +#endif /* SU_ACCESS */ +#endif /* !USE_PAM */ + /* + * Get the program name. The program name is used as a prefix to + * most error messages. + */ + Prog = Basename (argv[0]); + + caller_uid = getuid (); + caller_is_root = (caller_uid == 0); + + /* + * Get the tty name. Entries will be logged indicating that the user + * tried to change to the named new user from the current terminal. + */ + caller_tty = ttyname (0); + if ((isatty (0) != 0) && (NULL != caller_tty)) { +#ifndef USE_PAM + caller_on_console = console (caller_tty); +#endif /* !USE_PAM */ + } else { + /* + * Be more paranoid, like su from SimplePAMApps. --marekm + */ + if (!caller_is_root) { + fprintf (stderr, + _("%s: must be run from a terminal\n"), + Prog); + exit (1); + } + caller_tty = "???"; + } + + /* + * Get the user's real name. The current UID is used to determine + * who has executed su. That user ID must exist. + */ + pw = get_my_pwent (); + if (NULL == pw) { + fprintf (stderr, + _("%s: Cannot determine your user name.\n"), + Prog); + SYSLOG ((LOG_WARN, "Cannot determine the user name of the caller (UID %lu)", + (unsigned long) caller_uid)); + su_failure (caller_tty, true); /* unknown target UID*/ + } + STRFCPY (caller_name, pw->pw_name); + +#ifndef USE_PAM +#ifdef SU_ACCESS + /* + * Sort out the password of user calling su, in case needed later + * -- chris + */ + password = pw->pw_passwd; + if (strcmp (pw->pw_passwd, SHADOW_PASSWD_STRING) == 0) { + const struct spwd *spwd = getspnam (caller_name); + if (NULL != spwd) { + password = spwd->sp_pwdp; + } + } + free (caller_pass); + caller_pass = xstrdup (password); +#endif /* SU_ACCESS */ +#endif /* !USE_PAM */ + pw_free (pw); +} + +/* + * process_flags - Process the command line arguments + * + * process_flags() interprets the command line arguments and sets + * the values that the user will be created with accordingly. The + * values are checked for sanity. + */ +static void process_flags (int argc, char **argv) +{ + int c; + static struct option long_options[] = { + {"command", required_argument, NULL, 'c'}, + {"help", no_argument, NULL, 'h'}, + {"login", no_argument, NULL, 'l'}, + {"preserve-environment", no_argument, NULL, 'p'}, + {"shell", required_argument, NULL, 's'}, + {NULL, 0, NULL, '\0'} + }; + + while ((c = getopt_long (argc, argv, "c:hlmps:", + long_options, NULL)) != -1) { + switch (c) { + case 'c': + command = optarg; + break; + case 'h': + usage (E_SUCCESS); + break; + case 'l': + fakelogin = true; + break; + case 'm': + case 'p': + /* This will only have an effect if the target + * user do not have a restricted shell, or if + * su is called by root. + */ + change_environment = false; + break; + case 's': + shellstr = optarg; + break; + default: + usage (E_USAGE); /* NOT REACHED */ + } + } + + if ((optind < argc) && (strcmp (argv[optind], "-") == 0)) { + fakelogin = true; + optind++; + if ( (optind < argc) + && (strcmp (argv[optind], "--") == 0)) { + optind++; + } + } + + /* + * The next argument must be either a user ID, or some flag to a + * subshell. Pretty sticky since you can't have an argument which + * doesn't start with a "-" unless you specify the new user name. + * Any remaining arguments will be passed to the user's login shell. + */ + if ((optind < argc) && ('-' != argv[optind][0])) { + STRFCPY (name, argv[optind++]); /* use this login id */ + if ((optind < argc) && (strcmp (argv[optind], "--") == 0)) { + optind++; + } + } + if ('\0' == name[0]) { /* use default user */ + struct passwd *root_pw = getpwnam ("root"); + if ((NULL != root_pw) && (0 == root_pw->pw_uid)) { + (void) strcpy (name, "root"); + } else { + root_pw = getpwuid (0); + if (NULL == root_pw) { + SYSLOG ((LOG_CRIT, "There is no UID 0 user.")); + su_failure (caller_tty, true); + } + (void) strcpy (name, root_pw->pw_name); + } + } + + doshell = (argc == optind); /* any arguments remaining? */ + if (NULL != command) { + doshell = false; + } +} + +static void set_environment (struct passwd *pw) +{ + const char *cp; + /* + * If a new login is being set up, the old environment will be + * ignored and a new one created later on. + */ + if (change_environment && fakelogin) { + /* + * The terminal type will be left alone if it is present in + * the environment already. + */ + cp = getenv ("TERM"); + if (NULL != cp) { + addenv ("TERM", cp); + } + + /* + * For some terminals COLORTERM seems to be the only way + * for checking for that specific terminal. For instance, + * gnome-terminal sets its TERM as "xterm" but its + * COLORTERM as "gnome-terminal". The COLORTERM variable + * is also of use when running GNU screen since it sets + * TERM to "screen" but doesn't touch COLORTERM. + */ + cp = getenv ("COLORTERM"); + if (NULL != cp) { + addenv ("COLORTERM", cp); + } + +#ifndef USE_PAM + cp = getdef_str ("ENV_TZ"); + if (NULL != cp) { + addenv (('/' == *cp) ? tz (cp) : cp, NULL); + } + + /* + * The clock frequency will be reset to the login value if required + */ + cp = getdef_str ("ENV_HZ"); + if (NULL != cp) { + addenv (cp, NULL); /* set the default $HZ, if one */ + } +#endif /* !USE_PAM */ + + /* + * Also leave DISPLAY and XAUTHORITY if present, else + * pam_xauth will not work. + */ + cp = getenv ("DISPLAY"); + if (NULL != cp) { + addenv ("DISPLAY", cp); + } + cp = getenv ("XAUTHORITY"); + if (NULL != cp) { + addenv ("XAUTHORITY", cp); + } + } else { + char **envp = environ; + while (NULL != *envp) { + addenv (*envp, NULL); + envp++; + } + } + + cp = getdef_str ((pw->pw_uid == 0) ? "ENV_SUPATH" : "ENV_PATH"); + if (NULL == cp) { + addenv ((pw->pw_uid == 0) ? "PATH=/sbin:/bin:/usr/sbin:/usr/bin" : "PATH=/bin:/usr/bin", NULL); + } else if (strchr (cp, '=') != NULL) { + addenv (cp, NULL); + } else { + addenv ("PATH", cp); + } + + if (getenv ("IFS") != NULL) { /* don't export user IFS ... */ + addenv ("IFS= \t\n", NULL); /* ... instead, set a safe IFS */ + } + +#ifdef USE_PAM + /* we need to setup the environment *after* pam_open_session(), + * else the UID is changed before stuff like pam_xauth could + * run, and we cannot access /etc/shadow and co + */ + environ = newenvp; /* make new environment active */ + + if (change_environment) { + /* update environment with all pam set variables */ + char **envcp = pam_getenvlist (pamh); + if (NULL != envcp) { + while (NULL != *envcp) { + addenv (*envcp, NULL); + envcp++; + } + } + } + +#else /* !USE_PAM */ + environ = newenvp; /* make new environment active */ +#endif /* !USE_PAM */ + + if (change_environment) { + if (fakelogin) { + if (shellstr != pw->pw_shell) { + free (pw->pw_shell); + pw->pw_shell = xstrdup (shellstr); + } + setup_env (pw); + } else { + addenv ("HOME", pw->pw_dir); + addenv ("USER", pw->pw_name); + addenv ("LOGNAME", pw->pw_name); + addenv ("SHELL", shellstr); + } + } + +} + +/* + * su - switch user id + * + * su changes the user's ids to the values for the specified user. if + * no new user name is specified, "root" or UID 0 is used by default. + * + * Any additional arguments are passed to the user's shell. In + * particular, the argument "-c" will cause the next argument to be + * interpreted as a command by the common shell programs. + */ +int main (int argc, char **argv) +{ + const char *cp; + struct passwd *pw = NULL; + +#ifdef USE_PAM + int ret; +#endif /* USE_PAM */ + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + save_caller_context (argv); + + OPENLOG ("su"); + + process_flags (argc, argv); + + initenv (); + +#ifdef USE_PAM + ret = pam_start ("su", name, &conv, &pamh); + if (PAM_SUCCESS != ret) { + SYSLOG ((LOG_ERR, "pam_start: error %d", ret); + fprintf (stderr, + _("%s: pam_start: error %d\n"), + Prog, ret)); + exit (1); + } + + ret = pam_set_item (pamh, PAM_TTY, (const void *) caller_tty); + if (PAM_SUCCESS == ret) { + ret = pam_set_item (pamh, PAM_RUSER, (const void *) caller_name); + } + if (PAM_SUCCESS != ret) { + SYSLOG ((LOG_ERR, "pam_set_item: %s", + pam_strerror (pamh, ret))); + fprintf (stderr, _("%s: %s\n"), Prog, pam_strerror (pamh, ret)); + pam_end (pamh, ret); + exit (1); + } +#endif /* USE_PAM */ + + pw = check_perms (); + + /* If the user do not want to change the environment, + * use the current SHELL. + * (unless another shell is required by the command line) + */ + if ((NULL == shellstr) && !change_environment) { + shellstr = getenv ("SHELL"); + } + + /* If su is not called by root, and the target user has a + * restricted shell, the environment must be changed and the shell + * must be the one specified in /etc/passwd. + */ + if ( !caller_is_root + && restricted_shell (pw->pw_shell)) { + shellstr = NULL; + change_environment = true; + } + + /* If the shell is not set at this time, use the shell specified + * in /etc/passwd. + */ + if (NULL == shellstr) { + shellstr = pw->pw_shell; + } + + /* + * Set the default shell. + */ + if ((NULL == shellstr) || ('\0' == shellstr[0])) { + shellstr = SHELL; + } + + sulog (caller_tty, true, caller_name, name); /* save SU information */ +#ifdef USE_SYSLOG + if (getdef_bool ("SYSLOG_SU_ENAB")) { + SYSLOG ((LOG_INFO, "+ %s %s:%s", caller_tty, + ('\0' != caller_name[0]) ? caller_name : "???", + ('\0' != name[0]) ? name : "???")); + } +#endif + +#ifdef USE_PAM + /* set primary group id and supplementary groups */ + if (setup_groups (pw) != 0) { + pam_end (pamh, PAM_ABORT); + exit (1); + } + + /* + * pam_setcred() may do things like resource limits, console groups, + * and much more, depending on the configured modules + */ + ret = pam_setcred (pamh, PAM_ESTABLISH_CRED); + if (PAM_SUCCESS != ret) { + SYSLOG ((LOG_ERR, "pam_setcred: %s", pam_strerror (pamh, ret))); + fprintf (stderr, _("%s: %s\n"), Prog, pam_strerror (pamh, ret)); + (void) pam_end (pamh, ret); + exit (1); + } + + ret = pam_open_session (pamh, 0); + if (PAM_SUCCESS != ret) { + SYSLOG ((LOG_ERR, "pam_open_session: %s", + pam_strerror (pamh, ret))); + fprintf (stderr, _("%s: %s\n"), Prog, pam_strerror (pamh, ret)); + pam_setcred (pamh, PAM_DELETE_CRED); + (void) pam_end (pamh, ret); + exit (1); + } + + prepare_pam_close_session (); + + /* become the new user */ + if (change_uid (pw) != 0) { + exit (1); + } +#else /* !USE_PAM */ + /* no limits if su from root (unless su must fake login's behavior) */ + if (!caller_is_root || fakelogin) { + setup_limits (pw); + } + + if (setup_uid_gid (pw, caller_on_console) != 0) { + exit (1); + } +#endif /* !USE_PAM */ + +#ifdef WITH_AUDIT + audit_fd = audit_open (); + audit_log_acct_message (audit_fd, + AUDIT_USER_ROLE_CHANGE, + NULL, /* Prog. name */ + "su", + ('\0' != caller_name[0]) ? caller_name : "???", + AUDIT_NO_ID, + "localhost", + NULL, /* addr */ + caller_tty, + 1); /* result */ + close (audit_fd); +#endif /* WITH_AUDIT */ + + set_environment (pw); + + if (!doshell) { + /* There is no need for a controlling terminal. + * This avoids the callee to inject commands on + * the caller's tty. */ + int err = -1; + +#ifdef USE_PAM + /* When PAM is used, we are on the child */ + err = setsid (); +#else + /* Otherwise, we cannot use setsid */ + int fd = open ("/dev/tty", O_RDWR); + + if (fd >= 0) { + err = ioctl (fd, TIOCNOTTY, (char *) 0); + (void) close (fd); + } else if (ENXIO == errno) { + /* There are no controlling terminal already */ + err = 0; + } +#endif /* USE_PAM */ + + if (-1 == err) { + (void) fprintf (stderr, + _("%s: Cannot drop the controlling terminal\n"), + Prog); + exit (1); + } + } + + /* + * PAM_DATA_SILENT is not supported by some modules, and + * there is no strong need to clean up the process space's + * memory since we will either call exec or exit. + pam_end (pamh, PAM_SUCCESS | PAM_DATA_SILENT); + */ + + endpwent (); + endspent (); + /* + * This is a workaround for Linux libc bug/feature (?) - the + * /dev/log file descriptor is open without the close-on-exec flag + * and used to be passed to the new shell. There is "fcntl(LogFile, + * F_SETFD, 1)" in libc/misc/syslog.c, but it is commented out (at + * least in 5.4.33). Why? --marekm + */ + closelog (); + + /* + * See if the user has extra arguments on the command line. In that + * case they will be provided to the new user's shell as arguments. + */ + if (fakelogin) { + char *arg0; + + cp = getdef_str ("SU_NAME"); + if (NULL == cp) { + cp = Basename (shellstr); + } + + arg0 = xmalloc (strlen (cp) + 2); + arg0[0] = '-'; + strcpy (arg0 + 1, cp); + cp = arg0; + } else { + cp = Basename (shellstr); + } + + if (!doshell) { + int err; + /* Position argv to the remaining arguments */ + argv += optind; + if (NULL != command) { + argv -= 2; + argv[0] = "-c"; + argv[1] = command; + } + /* + * Use the shell and create an argv + * with the rest of the command line included. + */ + argv[-1] = cp; + execve_shell (shellstr, &argv[-1], environ); + err = errno; + (void) fprintf (stderr, + _("Cannot execute %s\n"), shellstr); + errno = err; + } else { + (void) shell (shellstr, cp, environ); + } + + pw_free (pw); + + return (errno == ENOENT ? E_CMD_NOTFOUND : E_CMD_NOEXEC); +} + diff --git a/src/suauth.c b/src/suauth.c new file mode 100644 index 0000000..a5bbe4c --- /dev/null +++ b/src/suauth.c @@ -0,0 +1,239 @@ +/* + * Copyright (c) 1990 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2002 - 2005, Tomasz Kłoczko + * Copyright (c) 2007 - 2008, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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 <errno.h> +#include <grp.h> +#include <pwd.h> +#include <stdio.h> +#include <sys/types.h> +#include "defines.h" +#include "prototypes.h" + +#ifndef SUAUTHFILE +#define SUAUTHFILE "/etc/suauth" +#endif + +#define NOACTION 0 +#define NOPWORD 1 +#define DENY -1 +#define OWNPWORD 2 + +#ifdef SU_ACCESS + +/* Really, I could do with a few const char's here defining all the + * strings output to the user or the syslog. -- chris + */ +static int applies (const char *, char *); + +static int isgrp (const char *, const char *); + +static int lines = 0; + + +int check_su_auth (const char *actual_id, + const char *wanted_id, + bool su_to_root) +{ + int posn, endline; + const char field[] = ":"; + FILE *authfile_fd; + char temp[1024]; + char *to_users; + char *from_users; + char *action; + + if (!(authfile_fd = fopen (SUAUTHFILE, "r"))) { + int err = errno; + /* + * If the file doesn't exist - default to the standard su + * behaviour (no access control). If open fails for some + * other reason - maybe someone is trying to fool us with + * file descriptors limit etc., so deny access. --marekm + */ + if (ENOENT == err) { + return NOACTION; + } + SYSLOG ((LOG_ERR, + "could not open/read config file '%s': %s\n", + SUAUTHFILE, strerror (err))); + return DENY; + } + + while (fgets (temp, sizeof (temp), authfile_fd) != NULL) { + lines++; + + if (temp[endline = strlen (temp) - 1] != '\n') { + SYSLOG ((LOG_ERR, + "%s, line %d: line too long or missing newline", + SUAUTHFILE, lines)); + continue; + } + + while (endline > 0 && (temp[endline - 1] == ' ' + || temp[endline - 1] == '\t' + || temp[endline - 1] == '\n')) + endline--; + temp[endline] = '\0'; + + posn = 0; + while (temp[posn] == ' ' || temp[posn] == '\t') + posn++; + + if (temp[posn] == '\n' || temp[posn] == '#' + || temp[posn] == '\0') { + continue; + } + if (!(to_users = strtok (temp + posn, field)) + || !(from_users = strtok ((char *) NULL, field)) + || !(action = strtok ((char *) NULL, field)) + || strtok ((char *) NULL, field)) { + SYSLOG ((LOG_ERR, + "%s, line %d. Bad number of fields.\n", + SUAUTHFILE, lines)); + continue; + } + + if (!applies (wanted_id, to_users)) + continue; + if (!applies (actual_id, from_users)) + continue; + if (!strcmp (action, "DENY")) { + SYSLOG ((su_to_root ? LOG_WARN : LOG_NOTICE, + "DENIED su from '%s' to '%s' (%s)\n", + actual_id, wanted_id, SUAUTHFILE)); + fputs (_("Access to su to that account DENIED.\n"), + stderr); + fclose (authfile_fd); + return DENY; + } else if (!strcmp (action, "NOPASS")) { + SYSLOG ((su_to_root ? LOG_NOTICE : LOG_INFO, + "NO password asked for su from '%s' to '%s' (%s)\n", + actual_id, wanted_id, SUAUTHFILE)); + fputs (_("Password authentication bypassed.\n"),stderr); + fclose (authfile_fd); + return NOPWORD; + } else if (!strcmp (action, "OWNPASS")) { + SYSLOG ((su_to_root ? LOG_NOTICE : LOG_INFO, + "su from '%s' to '%s': asking for user's own password (%s)\n", + actual_id, wanted_id, SUAUTHFILE)); + fputs (_("Please enter your OWN password as authentication.\n"), + stderr); + fclose (authfile_fd); + return OWNPWORD; + } else { + SYSLOG ((LOG_ERR, + "%s, line %d: unrecognised action!\n", + SUAUTHFILE, lines)); + } + } + fclose (authfile_fd); + return NOACTION; +} + +static int applies (const char *single, char *list) +{ + const char split[] = ", "; + char *tok; + + int state = 0; + + for (tok = strtok (list, split); tok != NULL; + tok = strtok (NULL, split)) { + + if (!strcmp (tok, "ALL")) { + if (state != 0) { + SYSLOG ((LOG_ERR, + "%s, line %d: ALL in bad place\n", + SUAUTHFILE, lines)); + return 0; + } + state = 1; + } else if (!strcmp (tok, "EXCEPT")) { + if (state != 1) { + SYSLOG ((LOG_ERR, + "%s, line %d: EXCEPT in bas place\n", + SUAUTHFILE, lines)); + return 0; + } + state = 2; + } else if (!strcmp (tok, "GROUP")) { + if ((state != 0) && (state != 2)) { + SYSLOG ((LOG_ERR, + "%s, line %d: GROUP in bad place\n", + SUAUTHFILE, lines)); + return 0; + } + state = (state == 0) ? 3 : 4; + } else { + switch (state) { + case 0: /* No control words yet */ + if (!strcmp (tok, single)) + return 1; + break; + case 1: /* An all */ + SYSLOG ((LOG_ERR, + "%s, line %d: expect another token after ALL\n", + SUAUTHFILE, lines)); + return 0; + case 2: /* All except */ + if (!strcmp (tok, single)) + return 0; + break; + case 3: /* Group */ + if (isgrp (single, tok)) + return 1; + break; + case 4: /* All except group */ + if (isgrp (single, tok)) + return 0; + /* FALL THRU */ + } + } + } + if ((state != 0) && (state != 3)) + return 1; + return 0; +} + +static int isgrp (const char *name, const char *group) +{ + struct group *grp; + + grp = getgrnam (group); /* local, no need for xgetgrnam */ + + if (!grp || !grp->gr_mem) + return 0; + + return is_on_list (grp->gr_mem, name); +} +#endif /* SU_ACCESS */ diff --git a/src/sulogin.c b/src/sulogin.c new file mode 100644 index 0000000..4264099 --- /dev/null +++ b/src/sulogin.c @@ -0,0 +1,257 @@ +/* + * Copyright (c) 1989 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2002 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2010, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <fcntl.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <sys/ioctl.h> +#include "defines.h" +#include "getdef.h" +#include "prototypes.h" +#include "pwauth.h" +/*@-exitarg@*/ +#include "exitcodes.h" + +/* + * Global variables + */ +const char *Prog; + +static char name[BUFSIZ]; +static char pass[BUFSIZ]; + +static struct passwd pwent; + +extern char **newenvp; +extern size_t newenvc; + +extern char **environ; + +#ifndef ALARM +#define ALARM 60 +#endif + +/* local function prototypes */ +static RETSIGTYPE catch_signals (int); + +static RETSIGTYPE catch_signals (unused int sig) +{ + _exit (1); +} + +/* + * syslogd is usually not running at the time when sulogin is typically + * called, cluttering the screen with unnecessary messages. Suggested by + * Ivan Nejgebauer <ian@unsux.ns.ac.yu>. --marekm + */ +#undef USE_SYSLOG + + /*ARGSUSED*/ int main (int argc, char **argv) +{ +#ifndef USE_PAM + const char *env; +#endif /* !USE_PAM */ + char **envp = environ; + TERMIO termio; + int err = 0; + +#ifdef USE_TERMIO + ioctl (0, TCGETA, &termio); + termio.c_iflag |= (ICRNL | IXON); + termio.c_oflag |= (OPOST | ONLCR); + termio.c_cflag |= (CREAD); + termio.c_lflag |= (ISIG | ICANON | ECHO | ECHOE | ECHOK); + ioctl (0, TCSETAF, &termio); +#endif +#ifdef USE_TERMIOS + tcgetattr (0, &termio); + termio.c_iflag |= (ICRNL | IXON); + termio.c_oflag |= (CREAD); + termio.c_lflag |= (ECHO | ECHOE | ECHOK | ICANON | ISIG); + tcsetattr (0, TCSANOW, &termio); +#endif + + Prog = Basename (argv[0]); + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + +#ifdef USE_SYSLOG + OPENLOG ("sulogin"); +#endif + initenv (); + if (argc > 1) { + close (0); + close (1); + close (2); + + if (open (argv[1], O_RDWR) >= 0) { + dup (0); + dup (0); + } else { +#ifdef USE_SYSLOG + SYSLOG (LOG_WARN, "cannot open %s\n", argv[1]); + closelog (); +#endif + exit (1); + } + } + if (access (PASSWD_FILE, F_OK) == -1) { /* must be a password file! */ + (void) puts (_("No password file")); +#ifdef USE_SYSLOG + SYSLOG (LOG_WARN, "No password file\n"); + closelog (); +#endif + exit (1); + } +#if !defined(DEBUG) && defined(SULOGIN_ONLY_INIT) + if (getppid () != 1) { /* parent must be INIT */ +#ifdef USE_SYSLOG + SYSLOG (LOG_WARN, "Pid == %d, not 1\n", getppid ()); + closelog (); +#endif + exit (1); + } +#endif + if ((isatty (0) == 0) || (isatty (1) == 0) || (isatty (2) == 0)) { +#ifdef USE_SYSLOG + closelog (); +#endif + exit (1); /* must be a terminal */ + } + /* If we were init, we need to start a new session */ + if (getppid() == 1) { + setsid(); + if (ioctl(0, TIOCSCTTY, 1) != 0) { + (void) fputs (_("TIOCSCTTY failed"), stderr); + } + } + while (NULL != *envp) { /* add inherited environment, */ + addenv (*envp, NULL); /* some variables change later */ + envp++; + } + +#ifndef USE_PAM + env = getdef_str ("ENV_TZ"); + if (NULL != env) { + addenv (('/' == *env) ? tz (env) : env, NULL); + } + env = getdef_str ("ENV_HZ"); + if (NULL != env) { + addenv (env, NULL); /* set the default $HZ, if one */ + } +#endif /* !USE_PAM */ + + (void) strcpy (name, "root"); /* KLUDGE!!! */ + + (void) signal (SIGALRM, catch_signals); /* exit if the timer expires */ + (void) alarm (ALARM); /* only wait so long ... */ + + while (true) { /* repeatedly get login/password pairs */ + char *cp; + pw_entry (name, &pwent); /* get entry from password file */ + if (pwent.pw_name == (char *) 0) { + /* + * Fail secure + */ + (void) puts (_("No password entry for 'root'")); +#ifdef USE_SYSLOG + SYSLOG (LOG_WARN, "No password entry for 'root'\n"); + closelog (); +#endif + exit (1); + } + + /* + * Here we prompt for the root password, or if no password + * is given we just exit. + */ + + /* get a password for root */ + cp = getpass (_( +"\n" +"Type control-d to proceed with normal startup,\n" +"(or give root password for system maintenance):")); + /* + * XXX - can't enter single user mode if root password is + * empty. I think this doesn't happen very often :-). But + * it will work with standard getpass() (no NULL on EOF). + * --marekm + */ + if ((NULL == cp) || ('\0' == *cp)) { +#ifdef USE_SYSLOG + SYSLOG (LOG_INFO, "Normal startup\n"); + closelog (); +#endif + (void) puts (""); +#ifdef TELINIT + execl (PATH_TELINIT, "telinit", RUNLEVEL, (char *) 0); +#endif + exit (0); + } else { + STRFCPY (pass, cp); + strzero (cp); + } + if (valid (pass, &pwent)) { /* check encrypted passwords ... */ + break; /* ... encrypted passwords matched */ + } + +#ifdef USE_SYSLOG + SYSLOG (LOG_WARN, "Incorrect root password\n"); +#endif + sleep (2); + (void) puts (_("Login incorrect")); + } + strzero (pass); + (void) alarm (0); + (void) signal (SIGALRM, SIG_DFL); + environ = newenvp; /* make new environment active */ + + (void) puts (_("Entering System Maintenance Mode")); +#ifdef USE_SYSLOG + SYSLOG (LOG_INFO, "System Maintenance Mode\n"); +#endif + +#ifdef USE_SYSLOG + closelog (); +#endif + /* exec the shell finally. */ + err = shell (pwent.pw_shell, (char *) 0, environ); + + return ((err == ENOENT) ? E_CMD_NOTFOUND : E_CMD_NOEXEC); +} + diff --git a/src/useradd.c b/src/useradd.c new file mode 100644 index 0000000..0e0fa1f --- /dev/null +++ b/src/useradd.c @@ -0,0 +1,2315 @@ +/* + * Copyright (c) 1991 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2000 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2012, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <assert.h> +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <grp.h> +#include <lastlog.h> +#include <pwd.h> +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM +#include "pam_defs.h" +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <time.h> +#include <unistd.h> +#include "chkname.h" +#include "defines.h" +#include "faillog.h" +#include "getdef.h" +#include "groupio.h" +#include "nscd.h" +#include "prototypes.h" +#include "pwauth.h" +#include "pwio.h" +#ifdef SHADOWGRP +#include "sgroupio.h" +#endif +#include "shadowio.h" +#ifdef ENABLE_SUBIDS +#include "subordinateio.h" +#endif /* ENABLE_SUBIDS */ +#ifdef WITH_TCB +#include "tcbfuncs.h" +#endif + +#ifndef SKEL_DIR +#define SKEL_DIR "/etc/skel" +#endif +#ifndef USER_DEFAULTS_FILE +#define USER_DEFAULTS_FILE "/etc/default/useradd" +#define NEW_USER_FILE "/etc/default/nuaddXXXXXX" +#endif +/* + * Needed for MkLinux DR1/2/2.1 - J. + */ +#ifndef LASTLOG_FILE +#define LASTLOG_FILE "/var/log/lastlog" +#endif +/* + * Global variables + */ +const char *Prog; + +/* + * These defaults are used if there is no defaults file. + */ +static gid_t def_group = 100; +static const char *def_gname = "other"; +static const char *def_home = "/home"; +static const char *def_shell = ""; +static const char *def_template = SKEL_DIR; +static const char *def_create_mail_spool = "no"; + +static long def_inactive = -1; +static const char *def_expire = ""; + +#define VALID(s) (strcspn (s, ":\n") == strlen (s)) + +static const char *user_name = ""; +static const char *user_pass = "!"; +static uid_t user_id; +static gid_t user_gid; +static const char *user_comment = ""; +static const char *user_home = ""; +static const char *user_shell = ""; +static const char *create_mail_spool = ""; +#ifdef WITH_SELINUX +static /*@notnull@*/const char *user_selinux = ""; +#endif /* WITH_SELINUX */ + +static long user_expire = -1; +static bool is_shadow_pwd; + +#ifdef SHADOWGRP +static bool is_shadow_grp; +static bool sgr_locked = false; +#endif +#ifdef ENABLE_SUBIDS +static bool is_sub_uid = false; +static bool is_sub_gid = false; +static bool sub_uid_locked = false; +static bool sub_gid_locked = false; +static uid_t sub_uid_start; /* New subordinate uid range */ +static unsigned long sub_uid_count; +static gid_t sub_gid_start; /* New subordinate gid range */ +static unsigned long sub_gid_count; +#endif /* ENABLE_SUBIDS */ +static bool pw_locked = false; +static bool gr_locked = false; +static bool spw_locked = false; +static char **user_groups; /* NULL-terminated list */ +static long sys_ngroups; +static bool do_grp_update = false; /* group files need to be updated */ + +static bool + bflg = false, /* new default root of home directory */ + cflg = false, /* comment (GECOS) field for new account */ + dflg = false, /* home directory for new account */ + Dflg = false, /* set/show new user default values */ + eflg = false, /* days since 1970-01-01 when account is locked */ + fflg = false, /* days until account with expired password is locked */ + gflg = false, /* primary group ID for new account */ + Gflg = false, /* secondary group set for new account */ + kflg = false, /* specify a directory to fill new user directory */ + lflg = false, /* do not add user to lastlog/faillog databases */ + mflg = false, /* create user's home directory if it doesn't exist */ + Mflg = false, /* do not create user's home directory even if CREATE_HOME is set */ + Nflg = false, /* do not create a group having the same name as the user, but add the user to def_group (or the group specified with -g) */ + oflg = false, /* permit non-unique user ID to be specified with -u */ + rflg = false, /* create a system account */ + sflg = false, /* shell program for new account */ + uflg = false, /* specify user ID for new account */ + Uflg = false; /* create a group having the same name as the user */ + +#ifdef WITH_SELINUX +#define Zflg ('\0' != *user_selinux) +#endif /* WITH_SELINUX */ + +static bool home_added = false; + +/* + * exit status values + */ +/*@-exitarg@*/ +#define E_SUCCESS 0 /* success */ +#define E_PW_UPDATE 1 /* can't update password file */ +#define E_USAGE 2 /* invalid command syntax */ +#define E_BAD_ARG 3 /* invalid argument to option */ +#define E_UID_IN_USE 4 /* UID already in use (and no -o) */ +#define E_NOTFOUND 6 /* specified group doesn't exist */ +#define E_NAME_IN_USE 9 /* username already in use */ +#define E_GRP_UPDATE 10 /* can't update group file */ +#define E_HOMEDIR 12 /* can't create home directory */ +#define E_SE_UPDATE 14 /* can't update SELinux user mapping */ +#ifdef ENABLE_SUBIDS +#define E_SUB_UID_UPDATE 16 /* can't update the subordinate uid file */ +#define E_SUB_GID_UPDATE 18 /* can't update the subordinate gid file */ +#endif /* ENABLE_SUBIDS */ + +#define DGROUP "GROUP=" +#define DHOME "HOME=" +#define DSHELL "SHELL=" +#define DINACT "INACTIVE=" +#define DEXPIRE "EXPIRE=" +#define DSKEL "SKEL=" +#define DCREATE_MAIL_SPOOL "CREATE_MAIL_SPOOL=" + +/* local function prototypes */ +static void fail_exit (int); +static void get_defaults (void); +static void show_defaults (void); +static int set_defaults (void); +static int get_groups (char *); +static void usage (int status); +static void new_pwent (struct passwd *); + +static long scale_age (long); +static void new_spent (struct spwd *); +static void grp_update (void); + +static void process_flags (int argc, char **argv); +static void close_files (void); +static void open_files (void); +static void open_shadow (void); +static void faillog_reset (uid_t); +static void lastlog_reset (uid_t); +static void tallylog_reset (char *); +static void usr_update (void); +static void create_home (void); +static void create_mail (void); + +/* + * fail_exit - undo as much as possible + */ +static void fail_exit (int code) +{ + if (home_added) { + if (rmdir (user_home) != 0) { + fprintf (stderr, + _("%s: %s was created, but could not be removed\n"), + Prog, user_home); + SYSLOG ((LOG_ERR, "failed to remove %s", user_home)); + } + } + + if (spw_locked) { + if (spw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", spw_dbname ())); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "unlocking shadow file", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + /* continue */ + } + } + if (pw_locked) { + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "unlocking passwd file", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + /* continue */ + } + } + if (gr_locked) { + if (gr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", gr_dbname ())); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "unlocking group file", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + /* continue */ + } + } +#ifdef SHADOWGRP + if (sgr_locked) { + if (sgr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sgr_dbname ())); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "unlocking gshadow file", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + /* continue */ + } + } +#endif +#ifdef ENABLE_SUBIDS + if (sub_uid_locked) { + if (sub_uid_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sub_uid_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sub_uid_dbname ())); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "unlocking subordinate user file", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + /* continue */ + } + } + if (sub_gid_locked) { + if (sub_gid_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sub_gid_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sub_gid_dbname ())); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "unlocking subordinate group file", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + /* continue */ + } + } +#endif /* ENABLE_SUBIDS */ + +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "adding user", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + SYSLOG ((LOG_INFO, "failed adding user '%s', data deleted", user_name)); + exit (code); +} + +#define MATCH(x,y) (strncmp((x),(y),strlen(y)) == 0) + +/* + * get_defaults - read the defaults file + * + * get_defaults() reads the defaults file for this command. It sets the + * various values from the file, or uses built-in default values if the + * file does not exist. + */ +static void get_defaults (void) +{ + FILE *fp; + char buf[1024]; + char *cp; + + /* + * Open the defaults file for reading. + */ + + fp = fopen (USER_DEFAULTS_FILE, "r"); + if (NULL == fp) { + return; + } + + /* + * Read the file a line at a time. Only the lines that have relevant + * values are used, everything else can be ignored. + */ + while (fgets (buf, (int) sizeof buf, fp) == buf) { + cp = strrchr (buf, '\n'); + if (NULL != cp) { + *cp = '\0'; + } + + cp = strchr (buf, '='); + if (NULL == cp) { + continue; + } + + cp++; + + /* + * Primary GROUP identifier + */ + if (MATCH (buf, DGROUP)) { + const struct group *grp = getgr_nam_gid (cp); + if (NULL == grp) { + fprintf (stderr, + _("%s: group '%s' does not exist\n"), + Prog, cp); + fprintf (stderr, + _("%s: the %s configuration in %s will be ignored\n"), + Prog, DGROUP, USER_DEFAULTS_FILE); + } else { + def_group = grp->gr_gid; + def_gname = xstrdup (grp->gr_name); + } + } + + /* + * Default HOME filesystem + */ + else if (MATCH (buf, DHOME)) { + def_home = xstrdup (cp); + } + + /* + * Default Login Shell command + */ + else if (MATCH (buf, DSHELL)) { + def_shell = xstrdup (cp); + } + + /* + * Default Password Inactive value + */ + else if (MATCH (buf, DINACT)) { + if ( (getlong (cp, &def_inactive) == 0) + || (def_inactive < -1)) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, cp); + fprintf (stderr, + _("%s: the %s configuration in %s will be ignored\n"), + Prog, DINACT, USER_DEFAULTS_FILE); + def_inactive = -1; + } + } + + /* + * Default account expiration date + */ + else if (MATCH (buf, DEXPIRE)) { + def_expire = xstrdup (cp); + } + + /* + * Default Skeleton information + */ + else if (MATCH (buf, DSKEL)) { + if ('\0' == *cp) { + cp = SKEL_DIR; /* XXX warning: const */ + } + + def_template = xstrdup (cp); + } + + /* + * Create by default user mail spool or not ? + */ + else if (MATCH (buf, DCREATE_MAIL_SPOOL)) { + if (*cp == '\0') { + cp = "no"; /* XXX warning: const */ + } + + def_create_mail_spool = xstrdup (cp); + } + } + (void) fclose (fp); +} + +/* + * show_defaults - show the contents of the defaults file + * + * show_defaults() displays the values that are used from the default + * file and the built-in values. + */ +static void show_defaults (void) +{ + printf ("GROUP=%u\n", (unsigned int) def_group); + printf ("HOME=%s\n", def_home); + printf ("INACTIVE=%ld\n", def_inactive); + printf ("EXPIRE=%s\n", def_expire); + printf ("SHELL=%s\n", def_shell); + printf ("SKEL=%s\n", def_template); + printf ("CREATE_MAIL_SPOOL=%s\n", def_create_mail_spool); +} + +/* + * set_defaults - write new defaults file + * + * set_defaults() re-writes the defaults file using the values that + * are currently set. Duplicated lines are pruned, missing lines are + * added, and unrecognized lines are copied as is. + */ +static int set_defaults (void) +{ + FILE *ifp; + FILE *ofp; + char buf[1024]; + static char new_file[] = NEW_USER_FILE; + char *cp; + int ofd; + int wlen; + bool out_group = false; + bool out_home = false; + bool out_inactive = false; + bool out_expire = false; + bool out_shell = false; + bool out_skel = false; + bool out_create_mail_spool = false; + + /* + * Create a temporary file to copy the new output to. + */ + ofd = mkstemp (new_file); + if (-1 == ofd) { + fprintf (stderr, + _("%s: cannot create new defaults file\n"), + Prog); + return -1; + } + + ofp = fdopen (ofd, "w"); + if (NULL == ofp) { + fprintf (stderr, + _("%s: cannot open new defaults file\n"), + Prog); + return -1; + } + + /* + * Open the existing defaults file and copy the lines to the + * temporary file, using any new values. Each line is checked + * to insure that it is not output more than once. + */ + ifp = fopen (USER_DEFAULTS_FILE, "r"); + if (NULL == ifp) { + fprintf (ofp, "# useradd defaults file\n"); + goto skip; + } + + while (fgets (buf, (int) sizeof buf, ifp) == buf) { + cp = strrchr (buf, '\n'); + if (NULL != cp) { + *cp = '\0'; + } else { + /* A line which does not end with \n is only valid + * at the end of the file. + */ + if (feof (ifp) == 0) { + fprintf (stderr, + _("%s: line too long in %s: %s..."), + Prog, USER_DEFAULTS_FILE, buf); + (void) fclose (ifp); + return -1; + } + } + + if (!out_group && MATCH (buf, DGROUP)) { + fprintf (ofp, DGROUP "%u\n", (unsigned int) def_group); + out_group = true; + } else if (!out_home && MATCH (buf, DHOME)) { + fprintf (ofp, DHOME "%s\n", def_home); + out_home = true; + } else if (!out_inactive && MATCH (buf, DINACT)) { + fprintf (ofp, DINACT "%ld\n", def_inactive); + out_inactive = true; + } else if (!out_expire && MATCH (buf, DEXPIRE)) { + fprintf (ofp, DEXPIRE "%s\n", def_expire); + out_expire = true; + } else if (!out_shell && MATCH (buf, DSHELL)) { + fprintf (ofp, DSHELL "%s\n", def_shell); + out_shell = true; + } else if (!out_skel && MATCH (buf, DSKEL)) { + fprintf (ofp, DSKEL "%s\n", def_template); + out_skel = true; + } else if (!out_create_mail_spool + && MATCH (buf, DCREATE_MAIL_SPOOL)) { + fprintf (ofp, + DCREATE_MAIL_SPOOL "%s\n", + def_create_mail_spool); + out_create_mail_spool = true; + } else + fprintf (ofp, "%s\n", buf); + } + (void) fclose (ifp); + + skip: + /* + * Check each line to insure that every line was output. This + * causes new values to be added to a file which did not previously + * have an entry for that value. + */ + if (!out_group) + fprintf (ofp, DGROUP "%u\n", (unsigned int) def_group); + if (!out_home) + fprintf (ofp, DHOME "%s\n", def_home); + if (!out_inactive) + fprintf (ofp, DINACT "%ld\n", def_inactive); + if (!out_expire) + fprintf (ofp, DEXPIRE "%s\n", def_expire); + if (!out_shell) + fprintf (ofp, DSHELL "%s\n", def_shell); + if (!out_skel) + fprintf (ofp, DSKEL "%s\n", def_template); + + if (!out_create_mail_spool) + fprintf (ofp, DCREATE_MAIL_SPOOL "%s\n", def_create_mail_spool); + + /* + * Flush and close the file. Check for errors to make certain + * the new file is intact. + */ + (void) fflush (ofp); + if ( (ferror (ofp) != 0) + || (fsync (fileno (ofp)) != 0) + || (fclose (ofp) != 0)) { + unlink (new_file); + return -1; + } + + /* + * Rename the current default file to its backup name. + */ + wlen = snprintf (buf, sizeof buf, "%s-", USER_DEFAULTS_FILE); + assert (wlen < (int) sizeof buf); + unlink (buf); + if ((link (USER_DEFAULTS_FILE, buf) != 0) && (ENOENT != errno)) { + int err = errno; + fprintf (stderr, + _("%s: Cannot create backup file (%s): %s\n"), + Prog, buf, strerror (err)); + unlink (new_file); + return -1; + } + + /* + * Rename the new default file to its correct name. + */ + if (rename (new_file, USER_DEFAULTS_FILE) != 0) { + int err = errno; + fprintf (stderr, + _("%s: rename: %s: %s\n"), + Prog, new_file, strerror (err)); + return -1; + } +#ifdef WITH_AUDIT + audit_logger (AUDIT_USYS_CONFIG, Prog, + "changing useradd defaults", + NULL, AUDIT_NO_ID, + SHADOW_AUDIT_SUCCESS); +#endif + SYSLOG ((LOG_INFO, + "useradd defaults: GROUP=%u, HOME=%s, SHELL=%s, INACTIVE=%ld, " + "EXPIRE=%s, SKEL=%s, CREATE_MAIL_SPOOL=%s", + (unsigned int) def_group, def_home, def_shell, + def_inactive, def_expire, def_template, + def_create_mail_spool)); + return 0; +} + +/* + * get_groups - convert a list of group names to an array of group IDs + * + * get_groups() takes a comma-separated list of group names and + * converts it to a NULL-terminated array. Any unknown group + * names are reported as errors. + */ +static int get_groups (char *list) +{ + char *cp; + const struct group *grp; + int errors = 0; + int ngroups = 0; + + if ('\0' == *list) { + return 0; + } + + /* + * So long as there is some data to be converted, strip off + * each name and look it up. A mix of numerical and string + * values for group identifiers is permitted. + */ + do { + /* + * Strip off a single name from the list + */ + cp = strchr (list, ','); + if (NULL != cp) { + *cp++ = '\0'; + } + + /* + * Names starting with digits are treated as numerical + * GID values, otherwise the string is looked up as is. + */ + grp = getgr_nam_gid (list); + + /* + * There must be a match, either by GID value or by + * string name. + * FIXME: It should exist according to gr_locate, + * otherwise, we can't change its members + */ + if (NULL == grp) { + fprintf (stderr, + _("%s: group '%s' does not exist\n"), + Prog, list); + errors++; + } + list = cp; + + /* + * If the group doesn't exist, don't dump core... + * Instead, try the next one. --marekm + */ + if (NULL == grp) { + continue; + } + +#ifdef USE_NIS + /* + * Don't add this group if they are an NIS group. Tell + * the user to go to the server for this group. + */ + if (__isgrNIS ()) { + fprintf (stderr, + _("%s: group '%s' is a NIS group.\n"), + Prog, grp->gr_name); + continue; + } +#endif + + if (ngroups == sys_ngroups) { + fprintf (stderr, + _("%s: too many groups specified (max %d).\n"), + Prog, ngroups); + break; + } + + /* + * Add the group name to the user's list of groups. + */ + user_groups[ngroups++] = xstrdup (grp->gr_name); + } while (NULL != list); + + user_groups[ngroups] = (char *) 0; + + /* + * Any errors in finding group names are fatal + */ + if (0 != errors) { + return -1; + } + + return 0; +} + +/* + * usage - display usage message and exit + */ +static void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options] LOGIN\n" + " %s -D\n" + " %s -D [options]\n" + "\n" + "Options:\n"), + Prog, Prog, Prog); + (void) fputs (_(" -b, --base-dir BASE_DIR base directory for the home directory of the\n" + " new account\n"), usageout); + (void) fputs (_(" -c, --comment COMMENT GECOS field of the new account\n"), usageout); + (void) fputs (_(" -d, --home-dir HOME_DIR home directory of the new account\n"), usageout); + (void) fputs (_(" -D, --defaults print or change default useradd configuration\n"), usageout); + (void) fputs (_(" -e, --expiredate EXPIRE_DATE expiration date of the new account\n"), usageout); + (void) fputs (_(" -f, --inactive INACTIVE password inactivity period of the new account\n"), usageout); + (void) fputs (_(" -g, --gid GROUP name or ID of the primary group of the new\n" + " account\n"), usageout); + (void) fputs (_(" -G, --groups GROUPS list of supplementary groups of the new\n" + " account\n"), usageout); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -k, --skel SKEL_DIR use this alternative skeleton directory\n"), usageout); + (void) fputs (_(" -K, --key KEY=VALUE override /etc/login.defs defaults\n"), usageout); + (void) fputs (_(" -l, --no-log-init do not add the user to the lastlog and\n" + " faillog databases\n"), usageout); + (void) fputs (_(" -m, --create-home create the user's home directory\n"), usageout); + (void) fputs (_(" -M, --no-create-home do not create the user's home directory\n"), usageout); + (void) fputs (_(" -N, --no-user-group do not create a group with the same name as\n" + " the user\n"), usageout); + (void) fputs (_(" -o, --non-unique allow to create users with duplicate\n" + " (non-unique) UID\n"), usageout); + (void) fputs (_(" -p, --password PASSWORD encrypted password of the new account\n"), usageout); + (void) fputs (_(" -r, --system create a system account\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs (_(" -s, --shell SHELL login shell of the new account\n"), usageout); + (void) fputs (_(" -u, --uid UID user ID of the new account\n"), usageout); + (void) fputs (_(" -U, --user-group create a group with the same name as the user\n"), usageout); +#ifdef WITH_SELINUX + (void) fputs (_(" -Z, --selinux-user SEUSER use a specific SEUSER for the SELinux user mapping\n"), usageout); +#endif /* WITH_SELINUX */ + (void) fputs ("\n", usageout); + exit (status); +} + +/* + * new_pwent - initialize the values in a password file entry + * + * new_pwent() takes all of the values that have been entered and + * fills in a (struct passwd) with them. + */ +static void new_pwent (struct passwd *pwent) +{ + memzero (pwent, sizeof *pwent); + pwent->pw_name = (char *) user_name; + if (is_shadow_pwd) { + pwent->pw_passwd = (char *) SHADOW_PASSWD_STRING; + } else { + pwent->pw_passwd = (char *) user_pass; + } + + pwent->pw_uid = user_id; + pwent->pw_gid = user_gid; + pwent->pw_gecos = (char *) user_comment; + pwent->pw_dir = (char *) user_home; + pwent->pw_shell = (char *) user_shell; +} + +static long scale_age (long x) +{ + if (x <= 0) { + return x; + } + + return x * (DAY / SCALE); +} + +/* + * new_spent - initialize the values in a shadow password file entry + * + * new_spent() takes all of the values that have been entered and + * fills in a (struct spwd) with them. + */ +static void new_spent (struct spwd *spent) +{ + memzero (spent, sizeof *spent); + spent->sp_namp = (char *) user_name; + spent->sp_pwdp = (char *) user_pass; + spent->sp_lstchg = (long) gettime () / SCALE; + if (0 == spent->sp_lstchg) { + /* Better disable aging than requiring a password change */ + spent->sp_lstchg = -1; + } + if (!rflg) { + spent->sp_min = scale_age (getdef_num ("PASS_MIN_DAYS", -1)); + spent->sp_max = scale_age (getdef_num ("PASS_MAX_DAYS", -1)); + spent->sp_warn = scale_age (getdef_num ("PASS_WARN_AGE", -1)); + spent->sp_inact = scale_age (def_inactive); + spent->sp_expire = scale_age (user_expire); + } else { + spent->sp_min = -1; + spent->sp_max = -1; + spent->sp_warn = -1; + spent->sp_inact = -1; + spent->sp_expire = -1; + } + spent->sp_flag = SHADOW_SP_FLAG_UNSET; +} + +/* + * grp_update - add user to secondary group set + * + * grp_update() takes the secondary group set given in user_groups + * and adds the user to each group given by that set. + * + * The group files are opened and locked in open_files(). + * + * close_files() should be called afterwards to commit the changes + * and unlocking the group files. + */ +static void grp_update (void) +{ + const struct group *grp; + struct group *ngrp; + +#ifdef SHADOWGRP + const struct sgrp *sgrp; + struct sgrp *nsgrp; +#endif + + /* + * Scan through the entire group file looking for the groups that + * the user is a member of. + * FIXME: we currently do not check that all groups of user_groups + * were completed with the new user. + */ + for (gr_rewind (), grp = gr_next (); NULL != grp; grp = gr_next ()) { + + /* + * See if the user specified this group as one of their + * concurrent groups. + */ + if (!is_on_list (user_groups, grp->gr_name)) { + continue; + } + + /* + * Make a copy - gr_update() will free() everything + * from the old entry, and we need it later. + */ + ngrp = __gr_dup (grp); + if (NULL == ngrp) { + fprintf (stderr, + _("%s: Out of memory. Cannot update %s.\n"), + Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failed to prepare the new %s entry '%s'", gr_dbname (), user_name)); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "adding user to group", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + fail_exit (E_GRP_UPDATE); /* XXX */ + } + + /* + * Add the username to the list of group members and + * update the group entry to reflect the change. + */ + ngrp->gr_mem = add_list (ngrp->gr_mem, user_name); + if (gr_update (ngrp) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, gr_dbname (), ngrp->gr_name); + SYSLOG ((LOG_ERR, "failed to prepare the new %s entry '%s'", gr_dbname (), user_name)); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "adding user to group", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + fail_exit (E_GRP_UPDATE); + } +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "adding user to group", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_SUCCESS); +#endif + SYSLOG ((LOG_INFO, + "add '%s' to group '%s'", + user_name, ngrp->gr_name)); + } + +#ifdef SHADOWGRP + if (!is_shadow_grp) + return; + + /* + * Scan through the entire shadow group file looking for the groups + * that the user is a member of. The administrative list isn't + * modified. + */ + for (sgr_rewind (), sgrp = sgr_next (); NULL != sgrp; sgrp = sgr_next ()) { + + /* + * See if the user specified this group as one of their + * concurrent groups. + * FIXME: is it really needed? + * This would be important only if the group is in + * user_groups. All these groups should be checked + * for existence with gr_locate already. + */ + if (gr_locate (sgrp->sg_name) == NULL) { + continue; + } + + if (!is_on_list (user_groups, sgrp->sg_name)) { + continue; + } + + /* + * Make a copy - sgr_update() will free() everything + * from the old entry, and we need it later. + */ + nsgrp = __sgr_dup (sgrp); + if (NULL == nsgrp) { + fprintf (stderr, + _("%s: Out of memory. Cannot update %s.\n"), + Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failed to prepare the new %s entry '%s'", sgr_dbname (), user_name)); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "adding user to shadow group", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + fail_exit (E_GRP_UPDATE); /* XXX */ + } + + /* + * Add the username to the list of group members and + * update the group entry to reflect the change. + */ + nsgrp->sg_mem = add_list (nsgrp->sg_mem, user_name); + if (sgr_update (nsgrp) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, sgr_dbname (), nsgrp->sg_name); + SYSLOG ((LOG_ERR, "failed to prepare the new %s entry '%s'", sgr_dbname (), user_name)); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "adding user to shadow group", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + fail_exit (E_GRP_UPDATE); + } +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "adding user to shadow group", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_SUCCESS); +#endif + SYSLOG ((LOG_INFO, + "add '%s' to shadow group '%s'", + user_name, nsgrp->sg_name)); + } +#endif /* SHADOWGRP */ +} + +/* + * process_flags - perform command line argument setting + * + * process_flags() interprets the command line arguments and sets + * the values that the user will be created with accordingly. The + * values are checked for sanity. + */ +static void process_flags (int argc, char **argv) +{ + const struct group *grp; + bool anyflag = false; + char *cp; + + { + /* + * Parse the command line options. + */ + int c; + static struct option long_options[] = { + {"base-dir", required_argument, NULL, 'b'}, + {"comment", required_argument, NULL, 'c'}, + {"home-dir", required_argument, NULL, 'd'}, + {"defaults", no_argument, NULL, 'D'}, + {"expiredate", required_argument, NULL, 'e'}, + {"inactive", required_argument, NULL, 'f'}, + {"gid", required_argument, NULL, 'g'}, + {"groups", required_argument, NULL, 'G'}, + {"help", no_argument, NULL, 'h'}, + {"skel", required_argument, NULL, 'k'}, + {"key", required_argument, NULL, 'K'}, + {"no-log-init", no_argument, NULL, 'l'}, + {"create-home", no_argument, NULL, 'm'}, + {"no-create-home", no_argument, NULL, 'M'}, + {"no-user-group", no_argument, NULL, 'N'}, + {"non-unique", no_argument, NULL, 'o'}, + {"password", required_argument, NULL, 'p'}, + {"system", no_argument, NULL, 'r'}, + {"root", required_argument, NULL, 'R'}, + {"shell", required_argument, NULL, 's'}, + {"uid", required_argument, NULL, 'u'}, + {"user-group", no_argument, NULL, 'U'}, +#ifdef WITH_SELINUX + {"selinux-user", required_argument, NULL, 'Z'}, +#endif /* WITH_SELINUX */ + {NULL, 0, NULL, '\0'} + }; + while ((c = getopt_long (argc, argv, +#ifdef WITH_SELINUX + "b:c:d:De:f:g:G:hk:K:lmMNop:rR:s:u:UZ:", +#else /* !WITH_SELINUX */ + "b:c:d:De:f:g:G:hk:K:lmMNop:rR:s:u:U", +#endif /* !WITH_SELINUX */ + long_options, NULL)) != -1) { + switch (c) { + case 'b': + if ( ( !VALID (optarg) ) + || ( optarg[0] != '/' )) { + fprintf (stderr, + _("%s: invalid base directory '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + def_home = optarg; + bflg = true; + break; + case 'c': + if (!VALID (optarg)) { + fprintf (stderr, + _("%s: invalid comment '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + user_comment = optarg; + cflg = true; + break; + case 'd': + if ( ( !VALID (optarg) ) + || ( optarg[0] != '/' )) { + fprintf (stderr, + _("%s: invalid home directory '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + user_home = optarg; + dflg = true; + break; + case 'D': + if (anyflag) { + usage (E_USAGE); + } + Dflg = true; + break; + case 'e': + if ('\0' != *optarg) { + user_expire = strtoday (optarg); + if (user_expire < -1) { + fprintf (stderr, + _("%s: invalid date '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + } else { + user_expire = -1; + } + + /* + * -e "" is allowed without /etc/shadow + * (it's a no-op in such case) + */ + if ((-1 != user_expire) && !is_shadow_pwd) { + fprintf (stderr, + _("%s: shadow passwords required for -e\n"), + Prog); + exit (E_USAGE); + } + if (Dflg) { + def_expire = optarg; + } + eflg = true; + break; + case 'f': + if ( (getlong (optarg, &def_inactive) == 0) + || (def_inactive < -1)) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + /* + * -f -1 is allowed + * it's a no-op without /etc/shadow + */ + if ((-1 != def_inactive) && !is_shadow_pwd) { + fprintf (stderr, + _("%s: shadow passwords required for -f\n"), + Prog); + exit (E_USAGE); + } + fflg = true; + break; + case 'g': + grp = getgr_nam_gid (optarg); + if (NULL == grp) { + fprintf (stderr, + _("%s: group '%s' does not exist\n"), + Prog, optarg); + exit (E_NOTFOUND); + } + if (Dflg) { + def_group = grp->gr_gid; + def_gname = optarg; + } else { + user_gid = grp->gr_gid; + } + gflg = true; + break; + case 'G': + if (get_groups (optarg) != 0) { + exit (E_NOTFOUND); + } + if (NULL != user_groups[0]) { + do_grp_update = true; + } + Gflg = true; + break; + case 'h': + usage (E_SUCCESS); + break; + case 'k': + def_template = optarg; + kflg = true; + break; + case 'K': + /* + * override login.defs defaults (-K name=value) + * example: -K UID_MIN=100 -K UID_MAX=499 + * note: -K UID_MIN=10,UID_MAX=499 doesn't work yet + */ + cp = strchr (optarg, '='); + if (NULL == cp) { + fprintf (stderr, + _("%s: -K requires KEY=VALUE\n"), + Prog); + exit (E_BAD_ARG); + } + /* terminate name, point to value */ + *cp = '\0'; + cp++; + if (putdef_str (optarg, cp) < 0) { + exit (E_BAD_ARG); + } + break; + case 'l': + lflg = true; + break; + case 'm': + mflg = true; + break; + case 'M': + Mflg = true; + break; + case 'N': + Nflg = true; + break; + case 'o': + oflg = true; + break; + case 'p': /* set encrypted password */ + if (!VALID (optarg)) { + fprintf (stderr, + _("%s: invalid field '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + user_pass = optarg; + break; + case 'r': + rflg = true; + break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + case 's': + if ( ( !VALID (optarg) ) + || ( ('\0' != optarg[0]) + && ('/' != optarg[0]) + && ('*' != optarg[0]) )) { + fprintf (stderr, + _("%s: invalid shell '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + user_shell = optarg; + def_shell = optarg; + sflg = true; + break; + case 'u': + if ( (get_uid (optarg, &user_id) == 0) + || (user_id == (gid_t)-1)) { + fprintf (stderr, + _("%s: invalid user ID '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + uflg = true; + break; + case 'U': + Uflg = true; + break; +#ifdef WITH_SELINUX + case 'Z': + if (is_selinux_enabled () > 0) { + user_selinux = optarg; + } else { + fprintf (stderr, + _("%s: -Z requires SELinux enabled kernel\n"), + Prog); + + exit (E_BAD_ARG); + } + break; +#endif /* WITH_SELINUX */ + default: + usage (E_USAGE); + } + anyflag = true; + } + } + + if (!gflg && !Nflg && !Uflg) { + /* Get the settings from login.defs */ + Uflg = getdef_bool ("USERGROUPS_ENAB"); + } + + /* + * Certain options are only valid in combination with others. + * Check it here so that they can be specified in any order. + */ + if (oflg && !uflg) { + fprintf (stderr, + _("%s: %s flag is only allowed with the %s flag\n"), + Prog, "-o", "-u"); + usage (E_USAGE); + } + if (kflg && !mflg) { + fprintf (stderr, + _("%s: %s flag is only allowed with the %s flag\n"), + Prog, "-k", "-m"); + usage (E_USAGE); + } + if (Uflg && gflg) { + fprintf (stderr, + _("%s: options %s and %s conflict\n"), + Prog, "-U", "-g"); + usage (E_USAGE); + } + if (Uflg && Nflg) { + fprintf (stderr, + _("%s: options %s and %s conflict\n"), + Prog, "-U", "-N"); + usage (E_USAGE); + } + if (mflg && Mflg) { + fprintf (stderr, + _("%s: options %s and %s conflict\n"), + Prog, "-m", "-M"); + usage (E_USAGE); + } + + /* + * Either -D or username is required. Defaults can be set with -D + * for the -b, -e, -f, -g, -s options only. + */ + if (Dflg) { + if (optind != argc) { + usage (E_USAGE); + } + + if (uflg || Gflg || dflg || cflg || mflg) { + usage (E_USAGE); + } + } else { + if (optind != argc - 1) { + usage (E_USAGE); + } + + user_name = argv[optind]; + if (!is_valid_user_name (user_name)) { + fprintf (stderr, + _("%s: invalid user name '%s'\n"), + Prog, user_name); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "adding user", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + exit (E_BAD_ARG); + } + if (!dflg) { + char *uh; + size_t len = strlen (def_home) + strlen (user_name) + 2; + int wlen; + + uh = xmalloc (len); + wlen = snprintf (uh, len, "%s/%s", def_home, user_name); + assert (wlen == (int) len -1); + + user_home = uh; + } + } + + if (!eflg) { + user_expire = strtoday (def_expire); + } + + if (!gflg) { + user_gid = def_group; + } + + if (!sflg) { + user_shell = def_shell; + } + + create_mail_spool = def_create_mail_spool; + + if (!rflg) { + /* for system accounts defaults are ignored and we + * do not create a home dir */ + if (getdef_bool ("CREATE_HOME")) { + mflg = true; + } + } + + if (Mflg) { + /* absolutely sure that we do not create home dirs */ + mflg = false; + } +} + +/* + * close_files - close all of the files that were opened + * + * close_files() closes all of the files that were opened for this + * new user. This causes any modified entries to be written out. + */ +static void close_files (void) +{ + if (pw_close () == 0) { + fprintf (stderr, _("%s: failure while writing changes to %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", pw_dbname ())); + fail_exit (E_PW_UPDATE); + } + if (is_shadow_pwd && (spw_close () == 0)) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", spw_dbname ())); + fail_exit (E_PW_UPDATE); + } + if (do_grp_update) { + if (gr_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", gr_dbname ())); + fail_exit (E_GRP_UPDATE); + } +#ifdef SHADOWGRP + if (is_shadow_grp && (sgr_close () == 0)) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", sgr_dbname ())); + fail_exit (E_GRP_UPDATE); + } +#endif + } +#ifdef ENABLE_SUBIDS + if (is_sub_uid && (sub_uid_close () == 0)) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), Prog, sub_uid_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", sub_uid_dbname ())); + fail_exit (E_SUB_UID_UPDATE); + } + if (is_sub_gid && (sub_gid_close () == 0)) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), Prog, sub_gid_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", sub_gid_dbname ())); + fail_exit (E_SUB_GID_UPDATE); + } +#endif /* ENABLE_SUBIDS */ + if (is_shadow_pwd) { + if (spw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", spw_dbname ())); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "unlocking shadow file", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + /* continue */ + } + spw_locked = false; + } + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "unlocking passwd file", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + /* continue */ + } + pw_locked = false; + if (gr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", gr_dbname ())); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "unlocking group file", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + /* continue */ + } + gr_locked = false; +#ifdef SHADOWGRP + if (is_shadow_grp) { + if (sgr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sgr_dbname ())); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "unlocking gshadow file", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + /* continue */ + } + sgr_locked = false; + } +#endif +#ifdef ENABLE_SUBIDS + if (is_sub_uid) { + if (sub_uid_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sub_uid_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sub_uid_dbname ())); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "unlocking subordinate user file", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + /* continue */ + } + sub_uid_locked = false; + } + if (is_sub_gid) { + if (sub_gid_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sub_gid_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sub_gid_dbname ())); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "unlocking subordinate group file", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + /* continue */ + } + sub_gid_locked = false; + } +#endif /* ENABLE_SUBIDS */ +} + +/* + * open_files - lock and open the password files + * + * open_files() opens the two password files. + */ +static void open_files (void) +{ + if (pw_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, pw_dbname ()); + exit (E_PW_UPDATE); + } + pw_locked = true; + if (pw_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, _("%s: cannot open %s\n"), Prog, pw_dbname ()); + fail_exit (E_PW_UPDATE); + } + + /* shadow file will be opened by open_shadow(); */ + + /* + * Lock and open the group file. + */ + if (gr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, gr_dbname ()); + fail_exit (E_GRP_UPDATE); + } + gr_locked = true; + if (gr_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, _("%s: cannot open %s\n"), Prog, gr_dbname ()); + fail_exit (E_GRP_UPDATE); + } +#ifdef SHADOWGRP + if (is_shadow_grp) { + if (sgr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sgr_dbname ()); + fail_exit (E_GRP_UPDATE); + } + sgr_locked = true; + if (sgr_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, sgr_dbname ()); + fail_exit (E_GRP_UPDATE); + } + } +#endif +#ifdef ENABLE_SUBIDS + if (is_sub_uid) { + if (sub_uid_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sub_uid_dbname ()); + fail_exit (E_SUB_UID_UPDATE); + } + sub_uid_locked = true; + if (sub_uid_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, sub_uid_dbname ()); + fail_exit (E_SUB_UID_UPDATE); + } + } + if (is_sub_gid) { + if (sub_gid_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sub_gid_dbname ()); + fail_exit (E_SUB_GID_UPDATE); + } + sub_gid_locked = true; + if (sub_gid_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, sub_gid_dbname ()); + fail_exit (E_SUB_GID_UPDATE); + } + } +#endif /* ENABLE_SUBIDS */ +} + +static void open_shadow (void) +{ + if (!is_shadow_pwd) { + return; + } + if (spw_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, spw_dbname ()); + fail_exit (E_PW_UPDATE); + } + spw_locked = true; + if (spw_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, spw_dbname ()); + fail_exit (E_PW_UPDATE); + } +} + +static char *empty_list = NULL; + +/* + * new_grent - initialize the values in a group file entry + * + * new_grent() takes all of the values that have been entered and fills + * in a (struct group) with them. + */ + +static void new_grent (struct group *grent) +{ + memzero (grent, sizeof *grent); + grent->gr_name = (char *) user_name; +#ifdef SHADOWGRP + if (is_shadow_grp) { + grent->gr_passwd = SHADOW_PASSWD_STRING; /* XXX warning: const */ + } else +#endif /* SHADOWGRP */ + { + grent->gr_passwd = "!"; /* XXX warning: const */ + } + grent->gr_gid = user_gid; + grent->gr_mem = &empty_list; +} + +#ifdef SHADOWGRP +/* + * new_sgent - initialize the values in a shadow group file entry + * + * new_sgent() takes all of the values that have been entered and fills + * in a (struct sgrp) with them. + */ + +static void new_sgent (struct sgrp *sgent) +{ + memzero (sgent, sizeof *sgent); + sgent->sg_name = (char *) user_name; + sgent->sg_passwd = "!"; /* XXX warning: const */ + sgent->sg_adm = &empty_list; + sgent->sg_mem = &empty_list; +} +#endif /* SHADOWGRP */ + + +/* + * grp_add - add new group file entries + * + * grp_add() writes the new records to the group files. + */ + +static void grp_add (void) +{ + struct group grp; + +#ifdef SHADOWGRP + struct sgrp sgrp; +#endif /* SHADOWGRP */ + + /* + * Create the initial entries for this new group. + */ + new_grent (&grp); +#ifdef SHADOWGRP + new_sgent (&sgrp); +#endif /* SHADOWGRP */ + + /* + * Write out the new group file entry. + */ + if (gr_update (&grp) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, gr_dbname (), grp.gr_name); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_GROUP, Prog, + "adding group", + grp.gr_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + fail_exit (E_GRP_UPDATE); + } +#ifdef SHADOWGRP + /* + * Write out the new shadow group entries as well. + */ + if (is_shadow_grp && (sgr_update (&sgrp) == 0)) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, sgr_dbname (), sgrp.sg_name); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_GROUP, Prog, + "adding group", + grp.gr_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + fail_exit (E_GRP_UPDATE); + } +#endif /* SHADOWGRP */ + SYSLOG ((LOG_INFO, "new group: name=%s, GID=%u", user_name, user_gid)); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_GROUP, Prog, + "adding group", + grp.gr_name, AUDIT_NO_ID, + SHADOW_AUDIT_SUCCESS); +#endif + do_grp_update = true; +} + +static void faillog_reset (uid_t uid) +{ + struct faillog fl; + int fd; + off_t offset_uid = (off_t) (sizeof fl) * uid; + + if (access (FAILLOG_FILE, F_OK) != 0) { + return; + } + + memzero (&fl, sizeof (fl)); + + fd = open (FAILLOG_FILE, O_RDWR); + if ( (-1 == fd) + || (lseek (fd, offset_uid, SEEK_SET) != offset_uid) + || (write (fd, &fl, sizeof (fl)) != (ssize_t) sizeof (fl)) + || (fsync (fd) != 0) + || (close (fd) != 0)) { + fprintf (stderr, + _("%s: failed to reset the faillog entry of UID %lu: %s\n"), + Prog, (unsigned long) uid, strerror (errno)); + SYSLOG ((LOG_WARN, "failed to reset the faillog entry of UID %lu", (unsigned long) uid)); + /* continue */ + } +} + +static void lastlog_reset (uid_t uid) +{ + struct lastlog ll; + int fd; + off_t offset_uid = (off_t) (sizeof ll) * uid; + + if (access (LASTLOG_FILE, F_OK) != 0) { + return; + } + + memzero (&ll, sizeof (ll)); + + fd = open (LASTLOG_FILE, O_RDWR); + if ( (-1 == fd) + || (lseek (fd, offset_uid, SEEK_SET) != offset_uid) + || (write (fd, &ll, sizeof (ll)) != (ssize_t) sizeof (ll)) + || (fsync (fd) != 0) + || (close (fd) != 0)) { + fprintf (stderr, + _("%s: failed to reset the lastlog entry of UID %lu: %s\n"), + Prog, (unsigned long) uid, strerror (errno)); + SYSLOG ((LOG_WARN, "failed to reset the lastlog entry of UID %lu", (unsigned long) uid)); + /* continue */ + } +} + +static void tallylog_reset (char *user_name) +{ + const char pam_tally2[] = "/sbin/pam_tally2"; + const char *pname; + pid_t childpid; + int failed; + int status; + + if (access(pam_tally2, X_OK) == -1) + return; + + failed = 0; + switch (childpid = fork()) + { + case -1: /* error */ + failed = 1; + break; + case 0: /* child */ + pname = strrchr(pam_tally2, '/'); + if (pname == NULL) + pname = pam_tally2; + else + pname++; /* Skip the '/' */ + execl(pam_tally2, pname, "--user", user_name, "--reset", "--quiet", NULL); + /* If we come here, something has gone terribly wrong */ + perror(pam_tally2); + exit(42); /* don't continue, we now have 2 processes running! */ + /* NOTREACHED */ + break; + default: /* parent */ + if (waitpid(childpid, &status, 0) == -1 || !WIFEXITED(status) || WEXITSTATUS(status) != 0) + failed = 1; + break; + } + + if (failed) + { + fprintf (stderr, + _("%s: failed to reset the tallylog entry of user \"%s\"\n"), + Prog, user_name); + SYSLOG ((LOG_WARN, "failed to reset the tallylog entry of user \"%s\"", user_name)); + } + + return; +} + +/* + * usr_update - create the user entries + * + * usr_update() creates the password file entries for this user + * and will update the group entries if required. + */ +static void usr_update (void) +{ + struct passwd pwent; + struct spwd spent; + + /* + * Fill in the password structure with any new fields, making + * copies of strings. + */ + new_pwent (&pwent); + new_spent (&spent); + + /* + * Create a syslog entry. We need to do this now in case anything + * happens so we know what we were trying to accomplish. + */ + SYSLOG ((LOG_INFO, + "new user: name=%s, UID=%u, GID=%u, home=%s, shell=%s", + user_name, (unsigned int) user_id, + (unsigned int) user_gid, user_home, user_shell)); + + /* + * Initialize faillog and lastlog entries for this UID in case + * it belongs to a previously deleted user. We do it only if + * no user with this UID exists yet (entries for shared UIDs + * are left unchanged). --marekm + */ + /* local, no need for xgetpwuid */ + if ((!lflg) && (getpwuid (user_id) == NULL)) { + faillog_reset (user_id); + lastlog_reset (user_id); + } + + /* + * Put the new (struct passwd) in the table. + */ + if (pw_update (&pwent) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, pw_dbname (), pwent.pw_name); + fail_exit (E_PW_UPDATE); + } + + /* + * Put the new (struct spwd) in the table. + */ + if (is_shadow_pwd && (spw_update (&spent) == 0)) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, spw_dbname (), spent.sp_namp); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "adding shadow password", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif + fail_exit (E_PW_UPDATE); + } +#ifdef ENABLE_SUBIDS + if (is_sub_uid && + (sub_uid_add(user_name, sub_uid_start, sub_uid_count) == 0)) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry\n"), + Prog, sub_uid_dbname ()); + fail_exit (E_SUB_UID_UPDATE); + } + if (is_sub_gid && + (sub_gid_add(user_name, sub_gid_start, sub_gid_count) == 0)) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry\n"), + Prog, sub_uid_dbname ()); + fail_exit (E_SUB_GID_UPDATE); + } +#endif /* ENABLE_SUBIDS */ + +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "adding user", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_SUCCESS); +#endif + /* + * Do any group file updates for this user. + */ + if (do_grp_update) { + grp_update (); + } +} + +/* + * create_home - create the user's home directory + * + * create_home() creates the user's home directory if it does not + * already exist. It will be created mode 755 owned by the user + * with the user's default group. + */ +static void create_home (void) +{ + if (access (user_home, F_OK) != 0) { +#ifdef WITH_SELINUX + if (set_selinux_file_context (user_home) != 0) { + fprintf (stderr, + _("%s: cannot set SELinux context for home directory %s\n"), + Prog, user_home); + fail_exit (E_HOMEDIR); + } +#endif + /* XXX - create missing parent directories. --marekm */ + if (mkdir (user_home, 0) != 0) { + fprintf (stderr, + _("%s: cannot create directory %s\n"), + Prog, user_home); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "adding home directory", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif + fail_exit (E_HOMEDIR); + } + chown (user_home, user_id, user_gid); + chmod (user_home, + 0777 & ~getdef_num ("UMASK", GETDEF_DEFAULT_UMASK)); + home_added = true; +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "adding home directory", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_SUCCESS); +#endif +#ifdef WITH_SELINUX + /* Reset SELinux to create files with default contexts */ + if (reset_selinux_file_context () != 0) { + fprintf (stderr, + _("%s: cannot reset SELinux file creation context\n"), + Prog); + fail_exit (E_HOMEDIR); + } +#endif + } +} + +/* + * create_mail - create the user's mail spool + * + * create_mail() creates the user's mail spool if it does not already + * exist. It will be created mode 660 owned by the user and group + * 'mail' + */ +static void create_mail (void) +{ + if (strcasecmp (create_mail_spool, "yes") == 0) { + const char *spool; + char *file; + int fd; + struct group *gr; + gid_t gid; + mode_t mode; + + spool = getdef_str ("MAIL_DIR"); + if (NULL == spool) { + spool = "/var/mail"; + } + file = alloca (strlen (spool) + strlen (user_name) + 2); + sprintf (file, "%s/%s", spool, user_name); + fd = open (file, O_CREAT | O_WRONLY | O_TRUNC | O_EXCL, 0); + if (fd < 0) { + perror (_("Creating mailbox file")); + return; + } + + gr = getgrnam ("mail"); /* local, no need for xgetgrnam */ + if (NULL == gr) { + fputs (_("Group 'mail' not found. Creating the user mailbox file with 0600 mode.\n"), + stderr); + gid = user_gid; + mode = 0600; + } else { + gid = gr->gr_gid; + mode = 0660; + } + + if ( (fchown (fd, user_id, gid) != 0) + || (fchmod (fd, mode) != 0)) { + perror (_("Setting mailbox file permissions")); + } + + fsync (fd); + close (fd); + } +} + +/* + * main - useradd command + */ +int main (int argc, char **argv) +{ +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM + pam_handle_t *pamh = NULL; + int retval; +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ + +#ifdef ENABLE_SUBIDS + uid_t uid_min; + uid_t uid_max; +#endif + + /* + * Get my name so that I can use it to report errors. + */ + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + OPENLOG ("useradd"); +#ifdef WITH_AUDIT + audit_help_open (); +#endif + + sys_ngroups = sysconf (_SC_NGROUPS_MAX); + user_groups = (char **) xmalloc ((1 + sys_ngroups) * sizeof (char *)); + /* + * Initialize the list to be empty + */ + user_groups[0] = (char *) 0; + + + is_shadow_pwd = spw_file_present (); +#ifdef SHADOWGRP + is_shadow_grp = sgr_file_present (); +#endif + + get_defaults (); + + process_flags (argc, argv); + +#ifdef ENABLE_SUBIDS + uid_min = (uid_t) getdef_ulong ("UID_MIN", 1000UL); + uid_max = (uid_t) getdef_ulong ("UID_MAX", 60000UL); + is_sub_uid = sub_uid_file_present () && !rflg && + (!user_id || (user_id <= uid_max && user_id >= uid_min)); + is_sub_gid = sub_gid_file_present () && !rflg && + (!user_id || (user_id <= uid_max && user_id >= uid_min)); +#endif /* ENABLE_SUBIDS */ + +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM + { + struct passwd *pampw; + pampw = getpwuid (getuid ()); /* local, no need for xgetpwuid */ + if (pampw == NULL) { + fprintf (stderr, + _("%s: Cannot determine your user name.\n"), + Prog); + fail_exit (1); + } + + retval = pam_start ("useradd", pampw->pw_name, &conv, &pamh); + } + + if (PAM_SUCCESS == retval) { + retval = pam_authenticate (pamh, 0); + } + + if (PAM_SUCCESS == retval) { + retval = pam_acct_mgmt (pamh, 0); + } + + if (PAM_SUCCESS != retval) { + fprintf (stderr, _("%s: PAM: %s\n"), + Prog, pam_strerror (pamh, retval)); + SYSLOG((LOG_ERR, "%s", pam_strerror (pamh, retval))); + if (NULL != pamh) { + (void) pam_end (pamh, retval); + } + fail_exit (1); + } + (void) pam_end (pamh, retval); +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ + + /* + * See if we are messing with the defaults file, or creating + * a new user. + */ + if (Dflg) { + if (gflg || bflg || fflg || eflg || sflg) { + exit ((set_defaults () != 0) ? 1 : 0); + } + + show_defaults (); + exit (E_SUCCESS); + } + + /* + * Start with a quick check to see if the user exists. + */ + if (getpwnam (user_name) != NULL) { /* local, no need for xgetpwnam */ + fprintf (stderr, _("%s: user '%s' already exists\n"), Prog, user_name); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "adding user", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + fail_exit (E_NAME_IN_USE); + } + + /* + * Don't blindly overwrite a group when a user is added... + * If you already have a group username, and want to add the user + * to that group, use useradd -g username username. + * --bero + */ + if (Uflg) { + /* local, no need for xgetgrnam */ + if (getgrnam (user_name) != NULL) { + fprintf (stderr, + _("%s: group %s exists - if you want to add this user to that group, use -g.\n"), + Prog, user_name); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "adding group", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif + fail_exit (E_NAME_IN_USE); + } + } + + /* + * Do the hard stuff: + * - open the files, + * - create the user entries, + * - create the home directory, + * - create user mail spool, + * - flush nscd caches for passwd and group services, + * - then close and update the files. + */ + open_files (); + + if (!oflg) { + /* first, seek for a valid uid to use for this user. + * We do this because later we can use the uid we found as + * gid too ... --gafton */ + if (!uflg) { + if (find_new_uid (rflg, &user_id, NULL) < 0) { + fprintf (stderr, _("%s: can't create user\n"), Prog); + fail_exit (E_UID_IN_USE); + } + } else { + if (getpwuid (user_id) != NULL) { + fprintf (stderr, + _("%s: UID %lu is not unique\n"), + Prog, (unsigned long) user_id); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "adding user", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif + fail_exit (E_UID_IN_USE); + } + } + } + +#ifdef WITH_TCB + if (getdef_bool ("USE_TCB")) { + if (shadowtcb_create (user_name, user_id) == SHADOWTCB_FAILURE) { + fprintf (stderr, + _("%s: Failed to create tcb directory for %s\n"), + Prog, user_name); + fail_exit (E_UID_IN_USE); + } + } +#endif + open_shadow (); + + /* do we have to add a group for that user? This is why we need to + * open the group files in the open_files() function --gafton */ + if (Uflg) { + if (find_new_gid (rflg, &user_gid, &user_id) < 0) { + fprintf (stderr, + _("%s: can't create group\n"), + Prog); + fail_exit (4); + } + grp_add (); + } + +#ifdef ENABLE_SUBIDS + if (is_sub_uid) { + if (find_new_sub_uids(user_name, &sub_uid_start, &sub_uid_count) < 0) { + fprintf (stderr, + _("%s: can't create subordinate user IDs\n"), + Prog); + fail_exit(E_SUB_UID_UPDATE); + } + } + if (is_sub_gid) { + if (find_new_sub_gids(user_name, &sub_gid_start, &sub_gid_count) < 0) { + fprintf (stderr, + _("%s: can't create subordinate group IDs\n"), + Prog); + fail_exit(E_SUB_GID_UPDATE); + } + } +#endif /* ENABLE_SUBIDS */ + + usr_update (); + + if (mflg) { + create_home (); + if (home_added) { + copy_tree (def_template, user_home, false, false, + (uid_t)-1, user_id, (gid_t)-1, user_gid); + } else { + fprintf (stderr, + _("%s: warning: the home directory already exists.\n" + "Not copying any file from skel directory into it.\n"), + Prog); + } + + } + + /* Do not create mail directory for system accounts */ + if (!rflg) { + create_mail (); + } + + close_files (); + + /* + * tallylog_reset needs to be able to lookup + * a valid existing user name, + * so we canot call it before close_files() + */ + if (!lflg && getpwuid (user_id) != NULL) { + tallylog_reset (user_name); + } + +#ifdef WITH_SELINUX + if (Zflg) { + if (set_seuser (user_name, user_selinux) != 0) { + fprintf (stderr, + _("%s: warning: the user name %s to %s SELinux user mapping failed.\n"), + Prog, user_name, user_selinux); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "adding SELinux user mapping", + user_name, (unsigned int) user_id, 0); +#endif /* WITH_AUDIT */ + fail_exit (E_SE_UPDATE); + } + } +#endif /* WITH_SELINUX */ + + nscd_flush_cache ("passwd"); + nscd_flush_cache ("group"); + + return E_SUCCESS; +} + diff --git a/src/userdel.c b/src/userdel.c new file mode 100644 index 0000000..9092b5c --- /dev/null +++ b/src/userdel.c @@ -0,0 +1,1283 @@ +/* + * Copyright (c) 1991 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2000 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2012, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <grp.h> +#include <pwd.h> +#include <stdio.h> +#include <stdio.h> +#include <sys/stat.h> +#include <sys/stat.h> +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM +#include "pam_defs.h" +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ +#include "defines.h" +#include "getdef.h" +#include "groupio.h" +#include "nscd.h" +#include "prototypes.h" +#include "pwauth.h" +#include "pwio.h" +#include "shadowio.h" +#ifdef SHADOWGRP +#include "sgroupio.h" +#endif /* SHADOWGRP */ +#ifdef WITH_TCB +#include <tcb.h> +#include "tcbfuncs.h" +#endif /* WITH_TCB */ +/*@-exitarg@*/ +#include "exitcodes.h" +#ifdef ENABLE_SUBIDS +#include "subordinateio.h" +#endif /* ENABLE_SUBIDS */ + +/* + * exit status values + */ +#define E_PW_UPDATE 1 /* can't update password file */ +#define E_NOTFOUND 6 /* specified user doesn't exist */ +#define E_USER_BUSY 8 /* user currently logged in */ +#define E_GRP_UPDATE 10 /* can't update group file */ +#define E_HOMEDIR 12 /* can't remove home directory */ +#define E_SE_UPDATE 14 /* can't update SELinux user mapping */ +#ifdef ENABLE_SUBIDS +#define E_SUB_UID_UPDATE 16 /* can't update the subordinate uid file */ +#define E_SUB_GID_UPDATE 18 /* can't update the subordinate gid file */ +#endif /* ENABLE_SUBIDS */ + +/* + * Global variables + */ +const char *Prog; + +static char *user_name; +static uid_t user_id; +static gid_t user_gid; +static char *user_home; + +static bool fflg = false; +static bool rflg = false; +static bool Zflg = false; + +static bool is_shadow_pwd; + +#ifdef SHADOWGRP +static bool is_shadow_grp; +static bool sgr_locked = false; +#endif /* SHADOWGRP */ +static bool pw_locked = false; +static bool gr_locked = false; +static bool spw_locked = false; +#ifdef ENABLE_SUBIDS +static bool is_sub_uid; +static bool is_sub_gid; +static bool sub_uid_locked = false; +static bool sub_gid_locked = false; +#endif /* ENABLE_SUBIDS */ + +/* local function prototypes */ +static void usage (int status); +static void update_groups (void); +static void remove_usergroup (void); +static void close_files (void); +static void fail_exit (int); +static void open_files (void); +static void update_user (void); +static void user_cancel (const char *); + +#ifdef EXTRA_CHECK_HOME_DIR +static bool path_prefix (const char *, const char *); +#endif /* EXTRA_CHECK_HOME_DIR */ +static int is_owner (uid_t, const char *); +static int remove_mailbox (void); +#ifdef WITH_TCB +static int remove_tcbdir (const char *user_name, uid_t user_id); +#endif /* WITH_TCB */ + +/* + * usage - display usage message and exit + */ +static void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options] LOGIN\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -f, --force force removal of files,\n" + " even if not owned by user\n"), + usageout); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -r, --remove remove home directory and mail spool\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); +#ifdef WITH_SELINUX + (void) fputs (_(" -Z, --selinux-user remove any SELinux user mapping for the user\n"), usageout); +#endif /* WITH_SELINUX */ + (void) fputs ("\n", usageout); + exit (status); +} + +/* + * update_groups - delete user from secondary group set + * + * update_groups() takes the user name that was given and searches + * the group files for membership in any group. + * + * we also check to see if they have any groups they own (the same + * name is their user name) and delete them too (only if USERGROUPS_ENAB + * is enabled). + */ +static void update_groups (void) +{ + const struct group *grp; + struct group *ngrp; + +#ifdef SHADOWGRP + const struct sgrp *sgrp; + struct sgrp *nsgrp; +#endif /* SHADOWGRP */ + + /* + * Scan through the entire group file looking for the groups that + * the user is a member of. + */ + for (gr_rewind (), grp = gr_next (); NULL != grp; grp = gr_next ()) { + + /* + * See if the user specified this group as one of their + * concurrent groups. + */ + if (!is_on_list (grp->gr_mem, user_name)) { + continue; + } + + /* + * Delete the username from the list of group members and + * update the group entry to reflect the change. + */ + ngrp = __gr_dup (grp); + if (NULL == ngrp) { + fprintf (stderr, + _("%s: Out of memory. Cannot update %s.\n"), + Prog, gr_dbname ()); + exit (13); /* XXX */ + } + ngrp->gr_mem = del_list (ngrp->gr_mem, user_name); + if (gr_update (ngrp) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, gr_dbname (), ngrp->gr_name); + exit (E_GRP_UPDATE); + } + + /* + * Update the DBM group file with the new entry as well. + */ +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "deleting user from group", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_SUCCESS); +#endif /* WITH_AUDIT */ + SYSLOG ((LOG_INFO, "delete '%s' from group '%s'\n", + user_name, ngrp->gr_name)); + } + + if (getdef_bool ("USERGROUPS_ENAB")) { + remove_usergroup (); + } + +#ifdef SHADOWGRP + if (!is_shadow_grp) { + return; + } + + /* + * Scan through the entire shadow group file looking for the groups + * that the user is a member of. Both the administrative list and + * the ordinary membership list is checked. + */ + for (sgr_rewind (), sgrp = sgr_next (); + NULL != sgrp; + sgrp = sgr_next ()) { + bool was_member, was_admin; + + /* + * See if the user specified this group as one of their + * concurrent groups. + */ + was_member = is_on_list (sgrp->sg_mem, user_name); + was_admin = is_on_list (sgrp->sg_adm, user_name); + + if (!was_member && !was_admin) { + continue; + } + + nsgrp = __sgr_dup (sgrp); + if (NULL == nsgrp) { + fprintf (stderr, + _("%s: Out of memory. Cannot update %s.\n"), + Prog, sgr_dbname ()); + exit (13); /* XXX */ + } + + if (was_member) { + nsgrp->sg_mem = del_list (nsgrp->sg_mem, user_name); + } + + if (was_admin) { + nsgrp->sg_adm = del_list (nsgrp->sg_adm, user_name); + } + + if (sgr_update (nsgrp) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, sgr_dbname (), nsgrp->sg_name); + exit (E_GRP_UPDATE); + } +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "deleting user from shadow group", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_SUCCESS); +#endif /* WITH_AUDIT */ + SYSLOG ((LOG_INFO, "delete '%s' from shadow group '%s'\n", + user_name, nsgrp->sg_name)); + } +#endif /* SHADOWGRP */ +} + +/* + * remove_usergroup - delete the user's group if it is a usergroup + * + * An usergroup is removed if + * + it has the same name as the user + * + it is the primary group of the user + * + it has no other members + * + it is not the primary group of any other user + */ +static void remove_usergroup (void) +{ + const struct group *grp; + const struct passwd *pwd = NULL; + + grp = gr_locate (user_name); + if (NULL == grp) { + /* This user has no usergroup. */ + return; + } + + if (grp->gr_gid != user_gid) { + fprintf (stderr, + _("%s: group %s not removed because it is not the primary group of user %s.\n"), + Prog, grp->gr_name, user_name); + return; + } + + if (NULL != grp->gr_mem[0]) { + /* The usergroup has other members. */ + fprintf (stderr, + _("%s: group %s not removed because it has other members.\n"), + Prog, grp->gr_name); + return; + } + + if (!fflg) { + /* + * Scan the passwd file to check if this group is still + * used as a primary group. + */ + setpwent (); + while ((pwd = getpwent ()) != NULL) { + if (strcmp (pwd->pw_name, user_name) == 0) { + continue; + } + if (pwd->pw_gid == grp->gr_gid) { + fprintf (stderr, + _("%s: group %s is the primary group of another user and is not removed.\n"), + Prog, grp->gr_name); + break; + } + } + endpwent (); + } + + if (NULL == pwd) { + /* + * We can remove this group, it is not the primary + * group of any remaining user. + */ + if (gr_remove (user_name) == 0) { + fprintf (stderr, + _("%s: cannot remove entry '%s' from %s\n"), + Prog, user_name, gr_dbname ()); + fail_exit (E_GRP_UPDATE); + } + +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_GROUP, Prog, + "deleting group", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_SUCCESS); +#endif /* WITH_AUDIT */ + SYSLOG ((LOG_INFO, + "removed group '%s' owned by '%s'\n", + user_name, user_name)); + +#ifdef SHADOWGRP + if (sgr_locate (user_name) != NULL) { + if (sgr_remove (user_name) == 0) { + fprintf (stderr, + _("%s: cannot remove entry '%s' from %s\n"), + Prog, user_name, sgr_dbname ()); + fail_exit (E_GRP_UPDATE); + } +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_GROUP, Prog, + "deleting shadow group", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_SUCCESS); +#endif /* WITH_AUDIT */ + SYSLOG ((LOG_INFO, + "removed shadow group '%s' owned by '%s'\n", + user_name, user_name)); + + } +#endif /* SHADOWGRP */ + } +} + +/* + * close_files - close all of the files that were opened + * + * close_files() closes all of the files that were opened for this + * new user. This causes any modified entries to be written out. + */ +static void close_files (void) +{ + if (pw_close () == 0) { + fprintf (stderr, _("%s: failure while writing changes to %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", pw_dbname ())); + fail_exit (E_PW_UPDATE); + } + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + pw_locked = false; + + if (is_shadow_pwd) { + if (spw_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", spw_dbname ())); + fail_exit (E_PW_UPDATE); + } + if (spw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", spw_dbname ())); + /* continue */ + } + spw_locked = false; + } + + if (gr_close () == 0) { + fprintf (stderr, _("%s: failure while writing changes to %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", gr_dbname ())); + fail_exit (E_GRP_UPDATE); + } + if (gr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", gr_dbname ())); + /* continue */ + } + gr_locked = false; + +#ifdef SHADOWGRP + if (is_shadow_grp) { + if (sgr_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", sgr_dbname ())); + fail_exit (E_GRP_UPDATE); + } + + if (sgr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sgr_dbname ())); + /* continue */ + } + sgr_locked = false; + } +#endif /* SHADOWGRP */ + +#ifdef ENABLE_SUBIDS + if (is_sub_uid) { + if (sub_uid_close () == 0) { + fprintf (stderr, _("%s: failure while writing changes to %s\n"), Prog, sub_uid_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", sub_uid_dbname ())); + fail_exit (E_SUB_UID_UPDATE); + } + if (sub_uid_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sub_uid_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sub_uid_dbname ())); + /* continue */ + } + sub_uid_locked = false; + } + + if (is_sub_gid) { + if (sub_gid_close () == 0) { + fprintf (stderr, _("%s: failure while writing changes to %s\n"), Prog, sub_gid_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", sub_gid_dbname ())); + fail_exit (E_SUB_GID_UPDATE); + } + if (sub_gid_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sub_gid_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sub_gid_dbname ())); + /* continue */ + } + sub_gid_locked = false; + } +#endif /* ENABLE_SUBIDS */ +} + +/* + * fail_exit - exit with a failure code after unlocking the files + */ +static void fail_exit (int code) +{ + if (pw_locked) { + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + } + if (gr_locked) { + if (gr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", gr_dbname ())); + /* continue */ + } + } + if (spw_locked) { + if (spw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", spw_dbname ())); + /* continue */ + } + } +#ifdef SHADOWGRP + if (sgr_locked) { + if (sgr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sgr_dbname ())); + /* continue */ + } + } +#endif /* SHADOWGRP */ +#ifdef ENABLE_SUBIDS + if (sub_uid_locked) { + if (sub_uid_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sub_uid_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sub_uid_dbname ())); + /* continue */ + } + } + if (sub_gid_locked) { + if (sub_gid_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sub_gid_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sub_gid_dbname ())); + /* continue */ + } + } +#endif /* ENABLE_SUBIDS */ + +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "deleting user", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + + exit (code); +} + +/* + * open_files - lock and open the password files + * + * open_files() opens the two password files. + */ + +static void open_files (void) +{ + if (pw_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, pw_dbname ()); +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "locking password file", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + fail_exit (E_PW_UPDATE); + } + pw_locked = true; + if (pw_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), Prog, pw_dbname ()); +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "opening password file", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + fail_exit (E_PW_UPDATE); + } + if (is_shadow_pwd) { + if (spw_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, spw_dbname ()); +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "locking shadow password file", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + fail_exit (E_PW_UPDATE); + } + spw_locked = true; + if (spw_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, spw_dbname ()); +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "opening shadow password file", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + fail_exit (E_PW_UPDATE); + } + } + if (gr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, gr_dbname ()); +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "locking group file", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + fail_exit (E_GRP_UPDATE); + } + gr_locked = true; + if (gr_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, _("%s: cannot open %s\n"), Prog, gr_dbname ()); +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "opening group file", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + fail_exit (E_GRP_UPDATE); + } +#ifdef SHADOWGRP + if (is_shadow_grp) { + if (sgr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sgr_dbname ()); +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "locking shadow group file", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + fail_exit (E_GRP_UPDATE); + } + sgr_locked= true; + if (sgr_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, _("%s: cannot open %s\n"), + Prog, sgr_dbname ()); +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "opening shadow group file", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + fail_exit (E_GRP_UPDATE); + } + } +#endif /* SHADOWGRP */ +#ifdef ENABLE_SUBIDS + if (is_sub_uid) { + if (sub_uid_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sub_uid_dbname ()); +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "locking subordinate user file", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + fail_exit (E_SUB_UID_UPDATE); + } + sub_uid_locked = true; + if (sub_uid_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), Prog, sub_uid_dbname ()); +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "opening subordinate user file", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + fail_exit (E_SUB_UID_UPDATE); + } + } + if (is_sub_gid) { + if (sub_gid_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sub_gid_dbname ()); +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "locking subordinate group file", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + fail_exit (E_SUB_GID_UPDATE); + } + sub_gid_locked = true; + if (sub_gid_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), Prog, sub_gid_dbname ()); +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "opening subordinate group file", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + fail_exit (E_SUB_GID_UPDATE); + } + } +#endif /* ENABLE_SUBIDS */ +} + +/* + * update_user - delete the user entries + * + * update_user() deletes the password file entries for this user + * and will update the group entries as required. + */ +static void update_user (void) +{ + if (pw_remove (user_name) == 0) { + fprintf (stderr, + _("%s: cannot remove entry '%s' from %s\n"), + Prog, user_name, pw_dbname ()); + fail_exit (E_PW_UPDATE); + } + if ( is_shadow_pwd + && (spw_locate (user_name) != NULL) + && (spw_remove (user_name) == 0)) { + fprintf (stderr, + _("%s: cannot remove entry '%s' from %s\n"), + Prog, user_name, spw_dbname ()); + fail_exit (E_PW_UPDATE); + } +#ifdef ENABLE_SUBIDS + if (is_sub_uid && sub_uid_remove(user_name, 0, ULONG_MAX) == 0) { + fprintf (stderr, + _("%s: cannot remove entry %lu from %s\n"), + Prog, (unsigned long)user_id, sub_uid_dbname ()); + fail_exit (E_SUB_UID_UPDATE); + } + if (is_sub_gid && sub_gid_remove(user_name, 0, ULONG_MAX) == 0) { + fprintf (stderr, + _("%s: cannot remove entry %lu from %s\n"), + Prog, (unsigned long)user_id, sub_gid_dbname ()); + fail_exit (E_SUB_GID_UPDATE); + } +#endif /* ENABLE_SUBIDS */ +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "deleting user entries", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_SUCCESS); +#endif /* WITH_AUDIT */ + SYSLOG ((LOG_INFO, "delete user '%s'\n", user_name)); +} + +/* + * user_cancel - cancel cron and at jobs + * + * user_cancel calls a script for additional cleanups like removal of + * cron, at, or print jobs. + */ + +static void user_cancel (const char *user) +{ + const char *cmd; + const char *argv[3]; + int status; + + cmd = getdef_str ("USERDEL_CMD"); + if (NULL == cmd) { + return; + } + argv[0] = cmd; + argv[1] = user; + argv[2] = (char *)0; + (void) run_command (cmd, argv, NULL, &status); +} + +#ifdef EXTRA_CHECK_HOME_DIR +static bool path_prefix (const char *s1, const char *s2) +{ + return ( (strncmp (s2, s1, strlen (s1)) == 0) + && ( ('\0' == s2[strlen (s1)]) + || ('/' == s2[strlen (s1)]))); +} +#endif /* EXTRA_CHECK_HOME_DIR */ + +/* + * is_owner - Check if path is owned by uid + * + * Return + * 1: path exists and is owned by uid + * 0: path is not owned by uid, or a failure occurred + * -1: path does not exist + */ +static int is_owner (uid_t uid, const char *path) +{ + struct stat st; + + errno = 0; + if (stat (path, &st) != 0) { + if ((ENOENT == errno) || (ENOTDIR == errno)) { + /* The file or directory does not exist */ + return -1; + } else { + return 0; + } + } + return (st.st_uid == uid) ? 1 : 0; +} + +static int remove_mailbox (void) +{ + const char *maildir; + char mailfile[1024]; + int i; + int errors = 0; + + maildir = getdef_str ("MAIL_DIR"); +#ifdef MAIL_SPOOL_DIR + if ((NULL == maildir) && (getdef_str ("MAIL_FILE") == NULL)) { + maildir = MAIL_SPOOL_DIR; + } +#endif /* MAIL_SPOOL_DIR */ + if (NULL == maildir) { + return 0; + } + snprintf (mailfile, sizeof mailfile, "%s/%s", maildir, user_name); + + if (access (mailfile, F_OK) != 0) { + if (ENOENT == errno) { + fprintf (stderr, + _("%s: %s mail spool (%s) not found\n"), + Prog, user_name, mailfile); + return 0; + } else { + fprintf (stderr, + _("%s: warning: can't remove %s: %s\n"), + Prog, mailfile, strerror (errno)); + SYSLOG ((LOG_ERR, "Cannot remove %s: %s", mailfile, strerror (errno))); +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "deleting mail file", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + return -1; + } + } + + if (fflg) { + if (unlink (mailfile) != 0) { + fprintf (stderr, + _("%s: warning: can't remove %s: %s\n"), + Prog, mailfile, strerror (errno)); + SYSLOG ((LOG_ERR, "Cannot remove %s: %s", mailfile, strerror (errno))); +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "deleting mail file", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + errors = 1; + /* continue */ + } +#ifdef WITH_AUDIT + else + { + audit_logger (AUDIT_DEL_USER, Prog, + "deleting mail file", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_SUCCESS); + } +#endif /* WITH_AUDIT */ + return errors; + } + i = is_owner (user_id, mailfile); + if (i == 0) { + fprintf (stderr, + _("%s: %s not owned by %s, not removing\n"), + Prog, mailfile, user_name); + SYSLOG ((LOG_ERR, + "%s not owned by %s, not removed", + mailfile, strerror (errno))); +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "deleting mail file", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + return 1; + } else if (i == -1) { + return 0; /* mailbox doesn't exist */ + } + if (unlink (mailfile) != 0) { + fprintf (stderr, + _("%s: warning: can't remove %s: %s\n"), + Prog, mailfile, strerror (errno)); + SYSLOG ((LOG_ERR, "Cannot remove %s: %s", mailfile, strerror (errno))); +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "deleting mail file", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + errors = 1; + /* continue */ + } +#ifdef WITH_AUDIT + else + { + audit_logger (AUDIT_DEL_USER, Prog, + "deleting mail file", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_SUCCESS); + } +#endif /* WITH_AUDIT */ + return errors; +} + +#ifdef WITH_TCB +static int remove_tcbdir (const char *user_name, uid_t user_id) +{ + char *buf; + int ret = 0; + size_t bufsize = (sizeof TCB_DIR) + strlen (user_name) + 2; + + if (!getdef_bool ("USE_TCB")) { + return 0; + } + + buf = malloc (buflen); + if (NULL == buf) { + fprintf (stderr, _("%s: Can't allocate memory, " + "tcb entry for %s not removed.\n"), + Prog, user_name); + return 1; + } + snprintf (buf, buflen, TCB_DIR "/%s", user_name); + if (shadowtcb_drop_priv () == SHADOWTCB_FAILURE) { + fprintf (stderr, _("%s: Cannot drop privileges: %s\n"), + Prog, strerror (errno)); + shadowtcb_gain_priv (); + free (buf); + return 1; + } + /* Only remove directory contents with dropped privileges. + * We will regain them and remove the user's tcb directory afterwards. + */ + if (remove_tree (buf, false) != 0) { + fprintf (stderr, _("%s: Cannot remove the content of %s: %s\n"), + Prog, buf, strerror (errno)); + shadowtcb_gain_priv (); + free (buf); + return 1; + } + shadowtcb_gain_priv (); + free (buf); + if (shadowtcb_remove (user_name) == SHADOWTCB_FAILURE) { + fprintf (stderr, _("%s: Cannot remove tcb files for %s: %s\n"), + Prog, user_name, strerror (errno)); + ret = 1; + } + return ret; +} +#endif /* WITH_TCB */ + +/* + * main - userdel command + */ +int main (int argc, char **argv) +{ + int errors = 0; /* Error in the removal of the home directory */ + +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM + pam_handle_t *pamh = NULL; + int retval; +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ + + /* + * Get my name so that I can use it to report errors. + */ + Prog = Basename (argv[0]); + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + OPENLOG ("userdel"); +#ifdef WITH_AUDIT + audit_help_open (); +#endif /* WITH_AUDIT */ + + { + /* + * Parse the command line options. + */ + int c; + static struct option long_options[] = { + {"force", no_argument, NULL, 'f'}, + {"help", no_argument, NULL, 'h'}, + {"remove", no_argument, NULL, 'r'}, + {"root", required_argument, NULL, 'R'}, +#ifdef WITH_SELINUX + {"selinux-user", no_argument, NULL, 'Z'}, +#endif /* WITH_SELINUX */ + {NULL, 0, NULL, '\0'} + }; + while ((c = getopt_long (argc, argv, +#ifdef WITH_SELINUX + "fhrR:Z", +#else /* !WITH_SELINUX */ + "fhrR:", +#endif /* !WITH_SELINUX */ + long_options, NULL)) != -1) { + switch (c) { + case 'f': /* force remove even if not owned by user */ + fflg = true; + break; + case 'h': + usage (E_SUCCESS); + break; + case 'r': /* remove home dir and mailbox */ + rflg = true; + break; + case 'R': /* no-op, handled in process_root_flag () */ + break; +#ifdef WITH_SELINUX + case 'Z': + if (is_selinux_enabled () > 0) { + Zflg = true; + } else { + fprintf (stderr, + _("%s: -Z requires SELinux enabled kernel\n"), + Prog); + + exit (E_BAD_ARG); + } + break; +#endif /* WITH_SELINUX */ + default: + usage (E_USAGE); + } + } + } + + if ((optind + 1) != argc) { + usage (E_USAGE); + } + +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM + { + struct passwd *pampw; + pampw = getpwuid (getuid ()); /* local, no need for xgetpwuid */ + if (pampw == NULL) { + fprintf (stderr, + _("%s: Cannot determine your user name.\n"), + Prog); + exit (E_PW_UPDATE); + } + + retval = pam_start ("userdel", pampw->pw_name, &conv, &pamh); + } + + if (PAM_SUCCESS == retval) { + retval = pam_authenticate (pamh, 0); + } + + if (PAM_SUCCESS == retval) { + retval = pam_acct_mgmt (pamh, 0); + } + + if (PAM_SUCCESS != retval) { + fprintf (stderr, _("%s: PAM: %s\n"), + Prog, pam_strerror (pamh, retval)); + SYSLOG((LOG_ERR, "%s", pam_strerror (pamh, retval))); + if (NULL != pamh) { + (void) pam_end (pamh, retval); + } + exit (E_PW_UPDATE); + } + (void) pam_end (pamh, retval); +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ + + is_shadow_pwd = spw_file_present (); +#ifdef SHADOWGRP + is_shadow_grp = sgr_file_present (); +#endif /* SHADOWGRP */ +#ifdef ENABLE_SUBIDS + is_sub_uid = sub_uid_file_present (); + is_sub_gid = sub_gid_file_present (); +#endif /* ENABLE_SUBIDS */ + + /* + * Start with a quick check to see if the user exists. + */ + user_name = argv[argc - 1]; + { + struct passwd *pwd; + pwd = getpwnam (user_name); /* local, no need for xgetpwnam */ + if (NULL == pwd) { + fprintf (stderr, _("%s: user '%s' does not exist\n"), + Prog, user_name); +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "deleting user not found", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + exit (E_NOTFOUND); + } + user_id = pwd->pw_uid; + user_gid = pwd->pw_gid; + user_home = xstrdup (pwd->pw_dir); + } +#ifdef WITH_TCB + if (shadowtcb_set_user (user_name) == SHADOWTCB_FAILURE) { + exit (E_NOTFOUND); + } +#endif /* WITH_TCB */ +#ifdef USE_NIS + + /* + * Now make sure it isn't an NIS user. + */ + if (__ispwNIS ()) { + char *nis_domain; + char *nis_master; + + fprintf (stderr, + _("%s: user %s is a NIS user\n"), Prog, user_name); + if ( !yp_get_default_domain (&nis_domain) + && !yp_master (nis_domain, "passwd.byname", &nis_master)) { + fprintf (stderr, + _("%s: %s is the NIS master\n"), + Prog, nis_master); + } + exit (E_NOTFOUND); + } +#endif /* USE_NIS */ + /* + * Check to make certain the user isn't logged in. + * Note: This is a best effort basis. The user may log in between, + * a cron job may be started on her behalf, etc. + */ + if (user_busy (user_name, user_id) != 0) { + if (!fflg) { +#ifdef WITH_AUDIT + audit_logger (AUDIT_DEL_USER, Prog, + "deleting user logged in", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + exit (E_USER_BUSY); + } + } + + /* + * Do the hard stuff - open the files, create the user entries, + * create the home directory, then close and update the files. + */ + open_files (); + update_user (); + update_groups (); + + if (rflg) { + errors += remove_mailbox (); + } + if (rflg) { + int home_owned = is_owner (user_id, user_home); + if (-1 == home_owned) { + fprintf (stderr, + _("%s: %s home directory (%s) not found\n"), + Prog, user_name, user_home); + rflg = 0; + } else if ((0 == home_owned) && !fflg) { + fprintf (stderr, + _("%s: %s not owned by %s, not removing\n"), + Prog, user_home, user_name); + rflg = 0; + errors++; + /* continue */ + } + } + +#ifdef EXTRA_CHECK_HOME_DIR + /* This may be slow, the above should be good enough. */ + if (rflg && !fflg) { + struct passwd *pwd; + /* + * For safety, refuse to remove the home directory if it + * would result in removing some other user's home + * directory. Still not perfect so be careful, but should + * prevent accidents if someone has /home or / as home + * directory... --marekm + */ + setpwent (); + while ((pwd = getpwent ())) { + if (strcmp (pwd->pw_name, user_name) == 0) { + continue; + } + if (path_prefix (user_home, pwd->pw_dir)) { + fprintf (stderr, + _("%s: not removing directory %s (would remove home of user %s)\n"), + Prog, user_home, pwd->pw_name); + rflg = false; + errors++; + /* continue */ + break; + } + } + endpwent (); + } +#endif /* EXTRA_CHECK_HOME_DIR */ + + if (rflg) { + if (remove_tree (user_home, true) != 0) { + fprintf (stderr, + _("%s: error removing directory %s\n"), + Prog, user_home); + errors++; + /* continue */ + } +#ifdef WITH_AUDIT + else + { + audit_logger (AUDIT_DEL_USER, Prog, + "deleting home directory", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_SUCCESS); + } +#endif /* WITH_AUDIT */ + } +#ifdef WITH_AUDIT + if (0 != errors) { + audit_logger (AUDIT_DEL_USER, Prog, + "deleting home directory", + user_name, AUDIT_NO_ID, + SHADOW_AUDIT_FAILURE); + } +#endif /* WITH_AUDIT */ + +#ifdef WITH_SELINUX + if (Zflg) { + if (del_seuser (user_name) != 0) { + fprintf (stderr, + _("%s: warning: the user name %s to SELinux user mapping removal failed.\n"), + Prog, user_name); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "removing SELinux user mapping", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + fail_exit (E_SE_UPDATE); + } + } +#endif /* WITH_SELINUX */ + + /* + * Cancel any crontabs or at jobs. Have to do this before we remove + * the entry from /etc/passwd. + */ + user_cancel (user_name); + close_files (); + +#ifdef WITH_TCB + errors += remove_tcbdir (user_name, user_id); +#endif /* WITH_TCB */ + + nscd_flush_cache ("passwd"); + nscd_flush_cache ("group"); + + return ((0 != errors) ? E_HOMEDIR : E_SUCCESS); +} + diff --git a/src/usermod.c b/src/usermod.c new file mode 100644 index 0000000..9c5e479 --- /dev/null +++ b/src/usermod.c @@ -0,0 +1,2285 @@ +/* + * Copyright (c) 1991 - 1994, Julianne Frances Haugh + * Copyright (c) 1996 - 2000, Marek Michałkiewicz + * Copyright (c) 2000 - 2006, Tomasz Kłoczko + * Copyright (c) 2007 - 2011, Nicolas François + * 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 copyright holders or contributors may not be used to + * endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``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 COPYRIGHT + * HOLDERS OR CONTRIBUTORS 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> + +#ident "$Id$" + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <grp.h> +#include <lastlog.h> +#include <pwd.h> +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM +#include "pam_defs.h" +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ +#include <stdio.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <time.h> +#include "chkname.h" +#include "defines.h" +#include "faillog.h" +#include "getdef.h" +#include "groupio.h" +#include "nscd.h" +#include "prototypes.h" +#include "pwauth.h" +#include "pwio.h" +#ifdef SHADOWGRP +#include "sgroupio.h" +#endif +#include "shadowio.h" +#ifdef ENABLE_SUBIDS +#include "subordinateio.h" +#endif /* ENABLE_SUBIDS */ +#ifdef WITH_TCB +#include "tcbfuncs.h" +#endif + +/* + * exit status values + * for E_GRP_UPDATE and E_NOSPACE (not used yet), other update requests + * will be implemented (as documented in the Solaris 2.x man page). + */ +/*@-exitarg@*/ +#define E_SUCCESS 0 /* success */ +#define E_PW_UPDATE 1 /* can't update password file */ +#define E_USAGE 2 /* invalid command syntax */ +#define E_BAD_ARG 3 /* invalid argument to option */ +#define E_UID_IN_USE 4 /* UID already in use (and no -o) */ +/* #define E_BAD_PWFILE 5 passwd file contains errors */ +#define E_NOTFOUND 6 /* specified user/group doesn't exist */ +#define E_USER_BUSY 8 /* user to modify is logged in */ +#define E_NAME_IN_USE 9 /* username already in use */ +#define E_GRP_UPDATE 10 /* can't update group file */ +/* #define E_NOSPACE 11 insufficient space to move home dir */ +#define E_HOMEDIR 12 /* unable to complete home dir move */ +#define E_SE_UPDATE 13 /* can't update SELinux user mapping */ +#ifdef ENABLE_SUBIDS +#define E_SUB_UID_UPDATE 16 /* can't update the subordinate uid file */ +#define E_SUB_GID_UPDATE 18 /* can't update the subordinate gid file */ +#endif /* ENABLE_SUBIDS */ + +#define VALID(s) (strcspn (s, ":\n") == strlen (s)) + +/* + * Global variables + */ +const char *Prog; + +static char *user_name; +static char *user_newname; +static char *user_pass; +static uid_t user_id; +static uid_t user_newid; +static gid_t user_gid; +static gid_t user_newgid; +static char *user_comment; +static char *user_newcomment; +static char *user_home; +static char *user_newhome; +static char *user_shell; +#ifdef WITH_SELINUX +static const char *user_selinux = ""; +#endif /* WITH_SELINUX */ +static char *user_newshell; +static long user_expire; +static long user_newexpire; +static long user_inactive; +static long user_newinactive; +static long sys_ngroups; +static char **user_groups; /* NULL-terminated list */ + +static bool + aflg = false, /* append to existing secondary group set */ + cflg = false, /* new comment (GECOS) field */ + dflg = false, /* new home directory */ + eflg = false, /* days since 1970-01-01 when account becomes expired */ + fflg = false, /* days until account with expired password is locked */ + gflg = false, /* new primary group ID */ + Gflg = false, /* new secondary group set */ + Lflg = false, /* lock the password */ + lflg = false, /* new user name */ + mflg = false, /* create user's home directory if it doesn't exist */ + oflg = false, /* permit non-unique user ID to be specified with -u */ + pflg = false, /* new encrypted password */ + sflg = false, /* new shell program */ +#ifdef WITH_SELINUX + Zflg = false, /* new selinux user */ +#endif +#ifdef ENABLE_SUBIDS + vflg = false, /* add subordinate uids */ + Vflg = false, /* delete subordinate uids */ + wflg = false, /* add subordinate gids */ + Wflg = false, /* delete subordinate gids */ +#endif /* ENABLE_SUBIDS */ + uflg = false, /* specify new user ID */ + Uflg = false; /* unlock the password */ + +static bool is_shadow_pwd; + +#ifdef SHADOWGRP +static bool is_shadow_grp; +#endif + +#ifdef ENABLE_SUBIDS +static bool is_sub_uid = false; +static bool is_sub_gid = false; +#endif /* ENABLE_SUBIDS */ + +static bool pw_locked = false; +static bool spw_locked = false; +static bool gr_locked = false; +#ifdef SHADOWGRP +static bool sgr_locked = false; +#endif +#ifdef ENABLE_SUBIDS +static bool sub_uid_locked = false; +static bool sub_gid_locked = false; +#endif /* ENABLE_SUBIDS */ + + +/* local function prototypes */ +static void date_to_str (/*@unique@*//*@out@*/char *buf, size_t maxsize, + long int date); +static int get_groups (char *); +static /*@noreturn@*/void usage (int status); +static void new_pwent (struct passwd *); +static void new_spent (struct spwd *); +static /*@noreturn@*/void fail_exit (int); +static void update_group (void); + +#ifdef SHADOWGRP +static void update_gshadow (void); +#endif +static void grp_update (void); + +static void process_flags (int, char **); +static void close_files (void); +static void open_files (void); +static void usr_update (void); +static void move_home (void); +static void update_lastlog (void); +static void update_faillog (void); + +#ifndef NO_MOVE_MAILBOX +static void move_mailbox (void); +#endif + +static void date_to_str (/*@unique@*//*@out@*/char *buf, size_t maxsize, + long int date) +{ + struct tm *tp; + + if (date < 0) { + strncpy (buf, "never", maxsize); + } else { + time_t t = (time_t) date; + tp = gmtime (&t); +#ifdef HAVE_STRFTIME + strftime (buf, maxsize, "%Y-%m-%d", tp); +#else + (void) snprintf (buf, maxsize, "%04d-%02d-%02d", + tp->tm_year + 1900, + tp->tm_mon + 1, + tp->tm_mday); +#endif /* HAVE_STRFTIME */ + } + buf[maxsize - 1] = '\0'; +} + +/* + * get_groups - convert a list of group names to an array of group IDs + * + * get_groups() takes a comma-separated list of group names and + * converts it to a NULL-terminated array. Any unknown group names are + * reported as errors. + */ +static int get_groups (char *list) +{ + char *cp; + const struct group *grp; + int errors = 0; + int ngroups = 0; + + /* + * Initialize the list to be empty + */ + user_groups[0] = (char *) 0; + + if ('\0' == *list) { + return 0; + } + + /* + * So long as there is some data to be converted, strip off each + * name and look it up. A mix of numerical and string values for + * group identifiers is permitted. + */ + do { + /* + * Strip off a single name from the list + */ + cp = strchr (list, ','); + if (NULL != cp) { + *cp = '\0'; + cp++; + } + + /* + * Names starting with digits are treated as numerical GID + * values, otherwise the string is looked up as is. + */ + grp = getgr_nam_gid (list); + + /* + * There must be a match, either by GID value or by + * string name. + */ + if (NULL == grp) { + fprintf (stderr, _("%s: group '%s' does not exist\n"), + Prog, list); + errors++; + } + list = cp; + + /* + * If the group doesn't exist, don't dump core. Instead, + * try the next one. --marekm + */ + if (NULL == grp) { + continue; + } + +#ifdef USE_NIS + /* + * Don't add this group if they are an NIS group. Tell the + * user to go to the server for this group. + */ + if (__isgrNIS ()) { + fprintf (stderr, + _("%s: group '%s' is a NIS group.\n"), + Prog, grp->gr_name); + gr_free ((struct group *)grp); + continue; + } +#endif + + if (ngroups == sys_ngroups) { + fprintf (stderr, + _("%s: too many groups specified (max %d).\n"), + Prog, ngroups); + gr_free ((struct group *)grp); + break; + } + + /* + * Add the group name to the user's list of groups. + */ + user_groups[ngroups++] = xstrdup (grp->gr_name); + gr_free ((struct group *)grp); + } while (NULL != list); + + user_groups[ngroups] = (char *) 0; + + /* + * Any errors in finding group names are fatal + */ + if (0 != errors) { + return -1; + } + + return 0; +} + +#ifdef ENABLE_SUBIDS +struct ulong_range +{ + unsigned long first; + unsigned long last; +}; + +static struct ulong_range getulong_range(const char *str) +{ + struct ulong_range result = { .first = ULONG_MAX, .last = 0 }; + long long first, last; + char *pos; + + errno = 0; + first = strtoll(str, &pos, 10); + if (('\0' == *str) || ('-' != *pos ) || (ERANGE == errno) || + (first != (unsigned long int)first)) + goto out; + + errno = 0; + last = strtoll(pos + 1, &pos, 10); + if (('\0' != *pos ) || (ERANGE == errno) || + (last != (unsigned long int)last)) + goto out; + + if (first > last) + goto out; + + result.first = (unsigned long int)first; + result.last = (unsigned long int)last; +out: + return result; + +} + +struct ulong_range_list_entry { + struct ulong_range_list_entry *next; + struct ulong_range range; +}; + +static struct ulong_range_list_entry *add_sub_uids = NULL, *del_sub_uids = NULL; +static struct ulong_range_list_entry *add_sub_gids = NULL, *del_sub_gids = NULL; + +static int prepend_range(const char *str, struct ulong_range_list_entry **head) +{ + struct ulong_range range; + struct ulong_range_list_entry *entry; + range = getulong_range(str); + if (range.first > range.last) + return 0; + + entry = malloc(sizeof(*entry)); + if (!entry) { + fprintf (stderr, + _("%s: failed to allocate memory: %s\n"), + Prog, strerror (errno)); + return 0; + } + entry->next = *head; + entry->range = range; + *head = entry; + return 1; +} +#endif /* ENABLE_SUBIDS */ + +/* + * usage - display usage message and exit + */ +static /*@noreturn@*/void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (usageout, + _("Usage: %s [options] LOGIN\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -c, --comment COMMENT new value of the GECOS field\n"), usageout); + (void) fputs (_(" -d, --home HOME_DIR new home directory for the user account\n"), usageout); + (void) fputs (_(" -e, --expiredate EXPIRE_DATE set account expiration date to EXPIRE_DATE\n"), usageout); + (void) fputs (_(" -f, --inactive INACTIVE set password inactive after expiration\n" + " to INACTIVE\n"), usageout); + (void) fputs (_(" -g, --gid GROUP force use GROUP as new primary group\n"), usageout); + (void) fputs (_(" -G, --groups GROUPS new list of supplementary GROUPS\n"), usageout); + (void) fputs (_(" -a, --append append the user to the supplemental GROUPS\n" + " mentioned by the -G option without removing\n" + " him/her from other groups\n"), usageout); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -l, --login NEW_LOGIN new value of the login name\n"), usageout); + (void) fputs (_(" -L, --lock lock the user account\n"), usageout); + (void) fputs (_(" -m, --move-home move contents of the home directory to the\n" + " new location (use only with -d)\n"), usageout); + (void) fputs (_(" -o, --non-unique allow using duplicate (non-unique) UID\n"), usageout); + (void) fputs (_(" -p, --password PASSWORD use encrypted password for the new password\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs (_(" -s, --shell SHELL new login shell for the user account\n"), usageout); + (void) fputs (_(" -u, --uid UID new UID for the user account\n"), usageout); + (void) fputs (_(" -U, --unlock unlock the user account\n"), usageout); +#ifdef ENABLE_SUBIDS + (void) fputs (_(" -v, --add-subuids FIRST-LAST add range of subordinate uids\n"), usageout); + (void) fputs (_(" -V, --del-subuids FIRST-LAST remove range of subordinate uids\n"), usageout); + (void) fputs (_(" -w, --add-subgids FIRST-LAST add range of subordinate gids\n"), usageout); + (void) fputs (_(" -W, --del-subgids FIRST-LAST remove range of subordinate gids\n"), usageout); +#endif /* ENABLE_SUBIDS */ +#ifdef WITH_SELINUX + (void) fputs (_(" -Z, --selinux-user SEUSER new SELinux user mapping for the user account\n"), usageout); +#endif /* WITH_SELINUX */ + (void) fputs ("\n", usageout); + exit (status); +} + +/* + * update encrypted password string (for both shadow and non-shadow + * passwords) + */ +static char *new_pw_passwd (char *pw_pass) +{ + if (Lflg && ('!' != pw_pass[0])) { + char *buf = xmalloc (strlen (pw_pass) + 2); + +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "updating passwd", + user_newname, (unsigned int) user_newid, 0); +#endif + SYSLOG ((LOG_INFO, "lock user '%s' password", user_newname)); + strcpy (buf, "!"); + strcat (buf, pw_pass); + pw_pass = buf; + } else if (Uflg && pw_pass[0] == '!') { + char *s; + + if (pw_pass[1] == '\0') { + fprintf (stderr, + _("%s: unlocking the user's password would result in a passwordless account.\n" + "You should set a password with usermod -p to unlock this user's password.\n"), + Prog); + return pw_pass; + } + +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "updating password", + user_newname, (unsigned int) user_newid, 0); +#endif + SYSLOG ((LOG_INFO, "unlock user '%s' password", user_newname)); + s = pw_pass; + while ('\0' != *s) { + *s = *(s + 1); + s++; + } + } else if (pflg) { +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "changing password", + user_newname, (unsigned int) user_newid, 1); +#endif + SYSLOG ((LOG_INFO, "change user '%s' password", user_newname)); + pw_pass = xstrdup (user_pass); + } + return pw_pass; +} + +/* + * new_pwent - initialize the values in a password file entry + * + * new_pwent() takes all of the values that have been entered and fills + * in a (struct passwd) with them. + */ +static void new_pwent (struct passwd *pwent) +{ + if (lflg) { + if (pw_locate (user_newname) != NULL) { + /* This should never happen. + * It was already checked that the user doesn't + * exist on the system. + */ + fprintf (stderr, + _("%s: user '%s' already exists in %s\n"), + Prog, user_newname, pw_dbname ()); + fail_exit (E_NAME_IN_USE); + } +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "changing name", + user_newname, (unsigned int) user_newid, 1); +#endif + SYSLOG ((LOG_INFO, + "change user name '%s' to '%s'", + pwent->pw_name, user_newname)); + pwent->pw_name = xstrdup (user_newname); + } + /* Update the password in passwd if there is no shadow file or if + * the password is currently in passwd (pw_passwd != "x"). + * We do not force the usage of shadow passwords if they are not + * used for this account. + */ + if ( (!is_shadow_pwd) + || (strcmp (pwent->pw_passwd, SHADOW_PASSWD_STRING) != 0)) { + pwent->pw_passwd = new_pw_passwd (pwent->pw_passwd); + } + + if (uflg) { +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "changing uid", + user_newname, (unsigned int) user_newid, 1); +#endif + SYSLOG ((LOG_INFO, + "change user '%s' UID from '%d' to '%d'", + pwent->pw_name, pwent->pw_uid, user_newid)); + pwent->pw_uid = user_newid; + } + if (gflg) { +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "changing primary group", + user_newname, (unsigned int) user_newid, 1); +#endif + SYSLOG ((LOG_INFO, + "change user '%s' GID from '%d' to '%d'", + pwent->pw_name, pwent->pw_gid, user_newgid)); + pwent->pw_gid = user_newgid; + } + if (cflg) { +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "changing comment", + user_newname, (unsigned int) user_newid, 1); +#endif + pwent->pw_gecos = user_newcomment; + } + + if (dflg) { +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "changing home directory", + user_newname, (unsigned int) user_newid, 1); +#endif + SYSLOG ((LOG_INFO, + "change user '%s' home from '%s' to '%s'", + pwent->pw_name, pwent->pw_dir, user_newhome)); + pwent->pw_dir = user_newhome; + } + if (sflg) { +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "changing user shell", + user_newname, (unsigned int) user_newid, 1); +#endif + SYSLOG ((LOG_INFO, + "change user '%s' shell from '%s' to '%s'", + pwent->pw_name, pwent->pw_shell, user_newshell)); + pwent->pw_shell = user_newshell; + } +} + +/* + * new_spent - initialize the values in a shadow password file entry + * + * new_spent() takes all of the values that have been entered and fills + * in a (struct spwd) with them. + */ +static void new_spent (struct spwd *spent) +{ + if (lflg) { + if (spw_locate (user_newname) != NULL) { + fprintf (stderr, + _("%s: user '%s' already exists in %s\n"), + Prog, user_newname, spw_dbname ()); + fail_exit (E_NAME_IN_USE); + } + spent->sp_namp = xstrdup (user_newname); + } + + if (fflg) { +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "changing inactive days", + user_newname, (unsigned int) user_newid, 1); +#endif + SYSLOG ((LOG_INFO, + "change user '%s' inactive from '%ld' to '%ld'", + spent->sp_namp, spent->sp_inact, user_newinactive)); + spent->sp_inact = user_newinactive; + } + if (eflg) { + /* log dates rather than numbers of days. */ + char new_exp[16], old_exp[16]; + date_to_str (new_exp, sizeof(new_exp), + user_newexpire * DAY); + date_to_str (old_exp, sizeof(old_exp), + user_expire * DAY); +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "changing expiration date", + user_newname, (unsigned int) user_newid, 1); +#endif + SYSLOG ((LOG_INFO, + "change user '%s' expiration from '%s' to '%s'", + spent->sp_namp, old_exp, new_exp)); + spent->sp_expire = user_newexpire; + } + + /* Always update the shadowed password if there is a shadow entry + * (even if shadowed passwords might not be enabled for this + * account (pw_passwd != "x")). + * It seems better to update the password in both places in case a + * shadow and a non shadow entry exist. + * This might occur if: + * + there were already both entries + * + aging has been requested + */ + spent->sp_pwdp = new_pw_passwd (spent->sp_pwdp); + + if (pflg) { + spent->sp_lstchg = (long) gettime () / SCALE; + if (0 == spent->sp_lstchg) { + /* Better disable aging than requiring a password + * change. */ + spent->sp_lstchg = -1; + } + } +} + +/* + * fail_exit - exit with an error code after unlocking files + */ +static /*@noreturn@*/void fail_exit (int code) +{ + if (gr_locked) { + if (gr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", gr_dbname ())); + /* continue */ + } + } +#ifdef SHADOWGRP + if (sgr_locked) { + if (sgr_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sgr_dbname ())); + /* continue */ + } + } +#endif + if (spw_locked) { + if (spw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", spw_dbname ())); + /* continue */ + } + } + if (pw_locked) { + if (pw_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + } +#ifdef ENABLE_SUBIDS + if (sub_uid_locked) { + if (sub_uid_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sub_uid_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sub_uid_dbname ())); + /* continue */ + } + } + if (sub_gid_locked) { + if (sub_gid_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sub_gid_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sub_gid_dbname ())); + /* continue */ + } + } +#endif /* ENABLE_SUBIDS */ + +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "modifying account", + user_name, AUDIT_NO_ID, 0); +#endif + exit (code); +} + + +static void update_group (void) +{ + bool is_member; + bool was_member; + bool changed; + const struct group *grp; + struct group *ngrp; + + changed = false; + + /* + * Scan through the entire group file looking for the groups that + * the user is a member of. + */ + while ((grp = gr_next ()) != NULL) { + /* + * See if the user specified this group as one of their + * concurrent groups. + */ + was_member = is_on_list (grp->gr_mem, user_name); + is_member = Gflg && ( (was_member && aflg) + || is_on_list (user_groups, grp->gr_name)); + + if (!was_member && !is_member) { + continue; + } + + ngrp = __gr_dup (grp); + if (NULL == ngrp) { + fprintf (stderr, + _("%s: Out of memory. Cannot update %s.\n"), + Prog, gr_dbname ()); + fail_exit (E_GRP_UPDATE); + } + + if (was_member) { + if ((!Gflg) || is_member) { + /* User was a member and is still a member + * of this group. + * But the user might have been renamed. + */ + if (lflg) { + ngrp->gr_mem = del_list (ngrp->gr_mem, + user_name); + ngrp->gr_mem = add_list (ngrp->gr_mem, + user_newname); + changed = true; +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "changing group member", + user_newname, AUDIT_NO_ID, 1); +#endif + SYSLOG ((LOG_INFO, + "change '%s' to '%s' in group '%s'", + user_name, user_newname, + ngrp->gr_name)); + } + } else { + /* User was a member but is no more a + * member of this group. + */ + ngrp->gr_mem = del_list (ngrp->gr_mem, user_name); + changed = true; +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "removing group member", + user_name, AUDIT_NO_ID, 1); +#endif + SYSLOG ((LOG_INFO, + "delete '%s' from group '%s'", + user_name, ngrp->gr_name)); + } + } else { + /* User was not a member but is now a member this + * group. + */ + ngrp->gr_mem = add_list (ngrp->gr_mem, user_newname); + changed = true; +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "adding user to group", + user_name, AUDIT_NO_ID, 1); +#endif + SYSLOG ((LOG_INFO, "add '%s' to group '%s'", + user_newname, ngrp->gr_name)); + } + if (!changed) { + continue; + } + + changed = false; + if (gr_update (ngrp) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, gr_dbname (), ngrp->gr_name); + SYSLOG ((LOG_WARN, "failed to prepare the new %s entry '%s'", gr_dbname (), ngrp->gr_name)); + fail_exit (E_GRP_UPDATE); + } + } +} + +#ifdef SHADOWGRP +static void update_gshadow (void) +{ + bool is_member; + bool was_member; + bool was_admin; + bool changed; + const struct sgrp *sgrp; + struct sgrp *nsgrp; + + changed = false; + + /* + * Scan through the entire shadow group file looking for the groups + * that the user is a member of. + */ + while ((sgrp = sgr_next ()) != NULL) { + + /* + * See if the user was a member of this group + */ + was_member = is_on_list (sgrp->sg_mem, user_name); + + /* + * See if the user was an administrator of this group + */ + was_admin = is_on_list (sgrp->sg_adm, user_name); + + /* + * See if the user specified this group as one of their + * concurrent groups. + */ + is_member = Gflg && ( (was_member && aflg) + || is_on_list (user_groups, sgrp->sg_name)); + + if (!was_member && !was_admin && !is_member) { + continue; + } + + nsgrp = __sgr_dup (sgrp); + if (NULL == nsgrp) { + fprintf (stderr, + _("%s: Out of memory. Cannot update %s.\n"), + Prog, sgr_dbname ()); + fail_exit (E_GRP_UPDATE); + } + + if (was_admin && lflg) { + /* User was an admin of this group but the user + * has been renamed. + */ + nsgrp->sg_adm = del_list (nsgrp->sg_adm, user_name); + nsgrp->sg_adm = add_list (nsgrp->sg_adm, user_newname); + changed = true; +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "changing admin name in shadow group", + user_name, AUDIT_NO_ID, 1); +#endif + SYSLOG ((LOG_INFO, + "change admin '%s' to '%s' in shadow group '%s'", + user_name, user_newname, nsgrp->sg_name)); + } + + if (was_member) { + if ((!Gflg) || is_member) { + /* User was a member and is still a member + * of this group. + * But the user might have been renamed. + */ + if (lflg) { + nsgrp->sg_mem = del_list (nsgrp->sg_mem, + user_name); + nsgrp->sg_mem = add_list (nsgrp->sg_mem, + user_newname); + changed = true; +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "changing member in shadow group", + user_name, AUDIT_NO_ID, 1); +#endif + SYSLOG ((LOG_INFO, + "change '%s' to '%s' in shadow group '%s'", + user_name, user_newname, + nsgrp->sg_name)); + } + } else { + /* User was a member but is no more a + * member of this group. + */ + nsgrp->sg_mem = del_list (nsgrp->sg_mem, user_name); + changed = true; +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "removing user from shadow group", + user_name, AUDIT_NO_ID, 1); +#endif + SYSLOG ((LOG_INFO, + "delete '%s' from shadow group '%s'", + user_name, nsgrp->sg_name)); + } + } else if (is_member) { + /* User was not a member but is now a member this + * group. + */ + nsgrp->sg_mem = add_list (nsgrp->sg_mem, user_newname); + changed = true; +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "adding user to shadow group", + user_newname, AUDIT_NO_ID, 1); +#endif + SYSLOG ((LOG_INFO, "add '%s' to shadow group '%s'", + user_newname, nsgrp->sg_name)); + } + if (!changed) { + continue; + } + + changed = false; + + /* + * Update the group entry to reflect the changes. + */ + if (sgr_update (nsgrp) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, sgr_dbname (), nsgrp->sg_name); + SYSLOG ((LOG_WARN, "failed to prepare the new %s entry '%s'", + sgr_dbname (), nsgrp->sg_name)); + fail_exit (E_GRP_UPDATE); + } + } +} +#endif /* SHADOWGRP */ + +/* + * grp_update - add user to secondary group set + * + * grp_update() takes the secondary group set given in user_groups and + * adds the user to each group given by that set. + */ +static void grp_update (void) +{ + update_group (); +#ifdef SHADOWGRP + if (is_shadow_grp) { + update_gshadow (); + } +#endif +} + +/* + * process_flags - perform command line argument setting + * + * process_flags() interprets the command line arguments and sets the + * values that the user will be created with accordingly. The values + * are checked for sanity. + */ +static void process_flags (int argc, char **argv) +{ + const struct group *grp; + + bool anyflag = false; + + { + /* + * Parse the command line options. + */ + int c; + static struct option long_options[] = { + {"append", no_argument, NULL, 'a'}, + {"comment", required_argument, NULL, 'c'}, + {"home", required_argument, NULL, 'd'}, + {"expiredate", required_argument, NULL, 'e'}, + {"inactive", required_argument, NULL, 'f'}, + {"gid", required_argument, NULL, 'g'}, + {"groups", required_argument, NULL, 'G'}, + {"help", no_argument, NULL, 'h'}, + {"login", required_argument, NULL, 'l'}, + {"lock", no_argument, NULL, 'L'}, + {"move-home", no_argument, NULL, 'm'}, + {"non-unique", no_argument, NULL, 'o'}, + {"password", required_argument, NULL, 'p'}, + {"root", required_argument, NULL, 'R'}, + {"shell", required_argument, NULL, 's'}, + {"uid", required_argument, NULL, 'u'}, + {"unlock", no_argument, NULL, 'U'}, +#ifdef ENABLE_SUBIDS + {"add-subuids", required_argument, NULL, 'v'}, + {"del-subuids", required_argument, NULL, 'V'}, + {"add-subgids", required_argument, NULL, 'w'}, + {"del-subgids", required_argument, NULL, 'W'}, +#endif /* ENABLE_SUBIDS */ +#ifdef WITH_SELINUX + {"selinux-user", required_argument, NULL, 'Z'}, +#endif /* WITH_SELINUX */ + {NULL, 0, NULL, '\0'} + }; + while ((c = getopt_long (argc, argv, + "ac:d:e:f:g:G:hl:Lmop:R:s:u:U" +#ifdef ENABLE_SUBIDS + "v:w:V:W:" +#endif /* ENABLE_SUBIDS */ +#ifdef WITH_SELINUX + "Z:" +#endif /* WITH_SELINUX */ + , long_options, NULL)) != -1) { + switch (c) { + case 'a': + aflg = true; + break; + case 'c': + if (!VALID (optarg)) { + fprintf (stderr, + _("%s: invalid field '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + user_newcomment = optarg; + cflg = true; + break; + case 'd': + if (!VALID (optarg)) { + fprintf (stderr, + _("%s: invalid field '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + dflg = true; + user_newhome = optarg; + break; + case 'e': + if ('\0' != *optarg) { + user_newexpire = strtoday (optarg); + if (user_newexpire < -1) { + fprintf (stderr, + _("%s: invalid date '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + user_newexpire *= DAY / SCALE; + } else { + user_newexpire = -1; + } + eflg = true; + break; + case 'f': + if ( (getlong (optarg, &user_newinactive) == 0) + || (user_newinactive < -1)) { + fprintf (stderr, + _("%s: invalid numeric argument '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + fflg = true; + break; + case 'g': + grp = getgr_nam_gid (optarg); + if (NULL == grp) { + fprintf (stderr, + _("%s: group '%s' does not exist\n"), + Prog, optarg); + exit (E_NOTFOUND); + } + user_newgid = grp->gr_gid; + gflg = true; + break; + case 'G': + if (get_groups (optarg) != 0) { + exit (E_NOTFOUND); + } + Gflg = true; + break; + case 'h': + usage (E_SUCCESS); + /*@notreached@*/break; + case 'l': + if (!is_valid_user_name (optarg)) { + fprintf (stderr, + _("%s: invalid user name '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + lflg = true; + user_newname = optarg; + break; + case 'L': + Lflg = true; + break; + case 'm': + mflg = true; + break; + case 'o': + oflg = true; + break; + case 'p': + user_pass = optarg; + pflg = true; + break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + case 's': + if (!VALID (optarg)) { + fprintf (stderr, + _("%s: invalid field '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + user_newshell = optarg; + sflg = true; + break; + case 'u': + if ( (get_uid (optarg, &user_newid) ==0) + || (user_newid == (uid_t)-1)) { + fprintf (stderr, + _("%s: invalid user ID '%s'\n"), + Prog, optarg); + exit (E_BAD_ARG); + } + uflg = true; + break; + case 'U': + Uflg = true; + break; +#ifdef ENABLE_SUBIDS + case 'v': + if (prepend_range (optarg, &add_sub_uids) == 0) { + fprintf (stderr, + _("%s: invalid subordinate uid range '%s'\n"), + Prog, optarg); + exit(E_BAD_ARG); + } + vflg = true; + break; + case 'V': + if (prepend_range (optarg, &del_sub_uids) == 0) { + fprintf (stderr, + _("%s: invalid subordinate uid range '%s'\n"), + Prog, optarg); + exit(E_BAD_ARG); + } + Vflg = true; + break; + case 'w': + if (prepend_range (optarg, &add_sub_gids) == 0) { + fprintf (stderr, + _("%s: invalid subordinate gid range '%s'\n"), + Prog, optarg); + exit(E_BAD_ARG); + } + wflg = true; + break; + case 'W': + if (prepend_range (optarg, &del_sub_gids) == 0) { + fprintf (stderr, + _("%s: invalid subordinate gid range '%s'\n"), + Prog, optarg); + exit(E_BAD_ARG); + } + Wflg = true; + break; +#endif /* ENABLE_SUBIDS */ +#ifdef WITH_SELINUX + case 'Z': + if (is_selinux_enabled () > 0) { + user_selinux = optarg; + Zflg = true; + } else { + fprintf (stderr, + _("%s: -Z requires SELinux enabled kernel\n"), + Prog); + exit (E_BAD_ARG); + } + break; +#endif /* WITH_SELINUX */ + default: + usage (E_USAGE); + } + anyflag = true; + } + } + + if (optind != argc - 1) { + usage (E_USAGE); + } + + user_name = argv[argc - 1]; + + { + const struct passwd *pwd; + /* local, no need for xgetpwnam */ + pwd = getpwnam (user_name); + if (NULL == pwd) { + fprintf (stderr, + _("%s: user '%s' does not exist\n"), + Prog, user_name); + exit (E_NOTFOUND); + } + + user_id = pwd->pw_uid; + user_gid = pwd->pw_gid; + user_comment = xstrdup (pwd->pw_gecos); + user_home = xstrdup (pwd->pw_dir); + user_shell = xstrdup (pwd->pw_shell); + } + + /* user_newname, user_newid, user_newgid can be used even when the + * options where not specified. */ + if (!lflg) { + user_newname = user_name; + } + if (!uflg) { + user_newid = user_id; + } + if (!gflg) { + user_newgid = user_gid; + } + +#ifdef USE_NIS + /* + * Now make sure it isn't an NIS user. + */ + if (__ispwNIS ()) { + char *nis_domain; + char *nis_master; + + fprintf (stderr, + _("%s: user %s is a NIS user\n"), + Prog, user_name); + + if ( !yp_get_default_domain (&nis_domain) + && !yp_master (nis_domain, "passwd.byname", &nis_master)) { + fprintf (stderr, + _("%s: %s is the NIS master\n"), + Prog, nis_master); + } + exit (E_NOTFOUND); + } +#endif + + { + const struct spwd *spwd = NULL; + /* local, no need for xgetspnam */ + if (is_shadow_pwd && ((spwd = getspnam (user_name)) != NULL)) { + user_expire = spwd->sp_expire; + user_inactive = spwd->sp_inact; + } + } + + if (!anyflag) { + fprintf (stderr, _("%s: no options\n"), Prog); + usage (E_USAGE); + } + + if (aflg && (!Gflg)) { + fprintf (stderr, + _("%s: %s flag is only allowed with the %s flag\n"), + Prog, "-a", "-G"); + usage (E_USAGE); + } + + if ((Lflg && (pflg || Uflg)) || (pflg && Uflg)) { + fprintf (stderr, + _("%s: the -L, -p, and -U flags are exclusive\n"), + Prog); + usage (E_USAGE); + } + + if (oflg && !uflg) { + fprintf (stderr, + _("%s: %s flag is only allowed with the %s flag\n"), + Prog, "-o", "-u"); + usage (E_USAGE); + } + + if (mflg && !dflg) { + fprintf (stderr, + _("%s: %s flag is only allowed with the %s flag\n"), + Prog, "-m", "-d"); + usage (E_USAGE); + } + + if (user_newid == user_id) { + uflg = false; + oflg = false; + } + if (user_newgid == user_gid) { + gflg = false; + } + if ( (NULL != user_newshell) + && (strcmp (user_newshell, user_shell) == 0)) { + sflg = false; + } + if (strcmp (user_newname, user_name) == 0) { + lflg = false; + } + if (user_newinactive == user_inactive) { + fflg = false; + } + if (user_newexpire == user_expire) { + eflg = false; + } + if ( (NULL != user_newhome) + && (strcmp (user_newhome, user_home) == 0)) { + dflg = false; + mflg = false; + } + if ( (NULL != user_newcomment) + && (strcmp (user_newcomment, user_comment) == 0)) { + cflg = false; + } + + if (!(Uflg || uflg || sflg || pflg || mflg || Lflg || + lflg || Gflg || gflg || fflg || eflg || dflg || cflg +#ifdef ENABLE_SUBIDS + || vflg || Vflg || wflg || Wflg +#endif /* ENABLE_SUBIDS */ +#ifdef WITH_SELINUX + || Zflg +#endif /* WITH_SELINUX */ + )) { + fprintf (stderr, _("%s: no changes\n"), Prog); + exit (E_SUCCESS); + } + + if (!is_shadow_pwd && (eflg || fflg)) { + fprintf (stderr, + _("%s: shadow passwords required for -e and -f\n"), + Prog); + exit (E_USAGE); + } + + /* local, no need for xgetpwnam */ + if (lflg && (getpwnam (user_newname) != NULL)) { + fprintf (stderr, + _("%s: user '%s' already exists\n"), + Prog, user_newname); + exit (E_NAME_IN_USE); + } + + /* local, no need for xgetpwuid */ + if (uflg && !oflg && (getpwuid (user_newid) != NULL)) { + fprintf (stderr, + _("%s: UID '%lu' already exists\n"), + Prog, (unsigned long) user_newid); + exit (E_UID_IN_USE); + } + +#ifdef ENABLE_SUBIDS + if ( (vflg || Vflg) + && !is_sub_uid) { + fprintf (stderr, + _("%s: %s does not exist, you cannot use the flags %s or %s\n"), + Prog, sub_uid_dbname (), "-v", "-V"); + exit (E_USAGE); + } + + if ( (wflg || Wflg) + && !is_sub_gid) { + fprintf (stderr, + _("%s: %s does not exist, you cannot use the flags %s or %s\n"), + Prog, sub_gid_dbname (), "-w", "-W"); + exit (E_USAGE); + } +#endif /* ENABLE_SUBIDS */ +} + +/* + * close_files - close all of the files that were opened + * + * close_files() closes all of the files that were opened for this new + * user. This causes any modified entries to be written out. + */ +static void close_files (void) +{ + if (pw_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", pw_dbname ())); + fail_exit (E_PW_UPDATE); + } + if (is_shadow_pwd && (spw_close () == 0)) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, + "failure while writing changes to %s", + spw_dbname ())); + fail_exit (E_PW_UPDATE); + } + + if (Gflg || lflg) { + if (gr_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, + "failure while writing changes to %s", + gr_dbname ())); + fail_exit (E_GRP_UPDATE); + } +#ifdef SHADOWGRP + if (is_shadow_grp) { + if (sgr_close () == 0) { + fprintf (stderr, + _("%s: failure while writing changes to %s\n"), + Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, + "failure while writing changes to %s", + sgr_dbname ())); + fail_exit (E_GRP_UPDATE); + } + } +#endif +#ifdef SHADOWGRP + if (is_shadow_grp) { + if (sgr_unlock () == 0) { + fprintf (stderr, + _("%s: failed to unlock %s\n"), + Prog, sgr_dbname ()); + SYSLOG ((LOG_ERR, + "failed to unlock %s", + sgr_dbname ())); + /* continue */ + } + } +#endif + if (gr_unlock () == 0) { + fprintf (stderr, + _("%s: failed to unlock %s\n"), + Prog, gr_dbname ()); + SYSLOG ((LOG_ERR, + "failed to unlock %s", + gr_dbname ())); + /* continue */ + } + } + + if (is_shadow_pwd) { + if (spw_unlock () == 0) { + fprintf (stderr, + _("%s: failed to unlock %s\n"), + Prog, spw_dbname ()); + SYSLOG ((LOG_ERR, + "failed to unlock %s", + spw_dbname ())); + /* continue */ + } + } + if (pw_unlock () == 0) { + fprintf (stderr, + _("%s: failed to unlock %s\n"), + Prog, pw_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", pw_dbname ())); + /* continue */ + } + + pw_locked = false; + spw_locked = false; + gr_locked = false; +#ifdef SHADOWGRP + sgr_locked = false; +#endif + +#ifdef ENABLE_SUBIDS + if (vflg || Vflg) { + if (sub_uid_close () == 0) { + fprintf (stderr, _("%s: failure while writing changes to %s\n"), Prog, sub_uid_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", sub_uid_dbname ())); + fail_exit (E_SUB_UID_UPDATE); + } + if (sub_uid_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sub_uid_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sub_uid_dbname ())); + /* continue */ + } + sub_uid_locked = false; + } + if (wflg || Wflg) { + if (sub_gid_close () == 0) { + fprintf (stderr, _("%s: failure while writing changes to %s\n"), Prog, sub_gid_dbname ()); + SYSLOG ((LOG_ERR, "failure while writing changes to %s", sub_gid_dbname ())); + fail_exit (E_SUB_GID_UPDATE); + } + if (sub_gid_unlock () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, sub_gid_dbname ()); + SYSLOG ((LOG_ERR, "failed to unlock %s", sub_gid_dbname ())); + /* continue */ + } + sub_gid_locked = false; + } +#endif /* ENABLE_SUBIDS */ + + /* + * Close the DBM and/or flat files + */ + endpwent (); + endspent (); + endgrent (); +#ifdef SHADOWGRP + endsgent (); +#endif +} + +/* + * open_files - lock and open the password files + * + * open_files() opens the two password files. + */ +static void open_files (void) +{ + if (pw_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, pw_dbname ()); + fail_exit (E_PW_UPDATE); + } + pw_locked = true; + if (pw_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, pw_dbname ()); + fail_exit (E_PW_UPDATE); + } + if (is_shadow_pwd && (spw_lock () == 0)) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, spw_dbname ()); + fail_exit (E_PW_UPDATE); + } + spw_locked = true; + if (is_shadow_pwd && (spw_open (O_CREAT | O_RDWR) == 0)) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, spw_dbname ()); + fail_exit (E_PW_UPDATE); + } + + if (Gflg || lflg) { + /* + * Lock and open the group file. This will load all of the + * group entries. + */ + if (gr_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, gr_dbname ()); + fail_exit (E_GRP_UPDATE); + } + gr_locked = true; + if (gr_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, gr_dbname ()); + fail_exit (E_GRP_UPDATE); + } +#ifdef SHADOWGRP + if (is_shadow_grp && (sgr_lock () == 0)) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sgr_dbname ()); + fail_exit (E_GRP_UPDATE); + } + sgr_locked = true; + if (is_shadow_grp && (sgr_open (O_CREAT | O_RDWR) == 0)) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, sgr_dbname ()); + fail_exit (E_GRP_UPDATE); + } +#endif + } +#ifdef ENABLE_SUBIDS + if (vflg || Vflg) { + if (sub_uid_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sub_uid_dbname ()); + fail_exit (E_SUB_UID_UPDATE); + } + sub_uid_locked = true; + if (sub_uid_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, sub_uid_dbname ()); + fail_exit (E_SUB_UID_UPDATE); + } + } + if (wflg || Wflg) { + if (sub_gid_lock () == 0) { + fprintf (stderr, + _("%s: cannot lock %s; try again later.\n"), + Prog, sub_gid_dbname ()); + fail_exit (E_SUB_GID_UPDATE); + } + sub_gid_locked = true; + if (sub_gid_open (O_CREAT | O_RDWR) == 0) { + fprintf (stderr, + _("%s: cannot open %s\n"), + Prog, sub_gid_dbname ()); + fail_exit (E_SUB_GID_UPDATE); + } + } +#endif /* ENABLE_SUBIDS */ +} + +/* + * usr_update - create the user entries + * + * usr_update() creates the password file entries for this user and + * will update the group entries if required. + */ +static void usr_update (void) +{ + struct passwd pwent; + const struct passwd *pwd; + + struct spwd spent; + const struct spwd *spwd = NULL; + + /* + * Locate the entry in /etc/passwd, which MUST exist. + */ + pwd = pw_locate (user_name); + if (NULL == pwd) { + fprintf (stderr, + _("%s: user '%s' does not exist in %s\n"), + Prog, user_name, pw_dbname ()); + fail_exit (E_NOTFOUND); + } + pwent = *pwd; + new_pwent (&pwent); + + + /* If the shadow file does not exist, it won't be created */ + if (is_shadow_pwd) { + spwd = spw_locate (user_name); + if (NULL != spwd) { + /* Update the shadow entry if it exists */ + spent = *spwd; + new_spent (&spent); + } else if ( ( pflg + && (strcmp (pwent.pw_passwd, SHADOW_PASSWD_STRING) == 0)) + || eflg || fflg) { + /* In some cases, we force the creation of a + * shadow entry: + * + new password requested and passwd indicates + * a shadowed password + * + aging information is requested + */ + memset (&spent, 0, sizeof spent); + spent.sp_namp = user_name; + + /* The user explicitly asked for a shadow feature. + * Enable shadowed passwords for this new account. + */ + spent.sp_pwdp = xstrdup (pwent.pw_passwd); + pwent.pw_passwd = xstrdup (SHADOW_PASSWD_STRING); + + spent.sp_lstchg = (long) gettime () / SCALE; + if (0 == spent.sp_lstchg) { + /* Better disable aging than + * requiring a password change */ + spent.sp_lstchg = -1; + } + spent.sp_min = getdef_num ("PASS_MIN_DAYS", -1); + spent.sp_max = getdef_num ("PASS_MAX_DAYS", -1); + spent.sp_warn = getdef_num ("PASS_WARN_AGE", -1); + spent.sp_inact = -1; + spent.sp_expire = -1; + spent.sp_flag = SHADOW_SP_FLAG_UNSET; + new_spent (&spent); + spwd = &spent; /* entry needs to be committed */ + } + } + + if (lflg || uflg || gflg || cflg || dflg || sflg || pflg + || Lflg || Uflg) { + if (pw_update (&pwent) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, pw_dbname (), pwent.pw_name); + fail_exit (E_PW_UPDATE); + } + if (lflg && (pw_remove (user_name) == 0)) { + fprintf (stderr, + _("%s: cannot remove entry '%s' from %s\n"), + Prog, user_name, pw_dbname ()); + fail_exit (E_PW_UPDATE); + } + } + if ((NULL != spwd) && (lflg || eflg || fflg || pflg || Lflg || Uflg)) { + if (spw_update (&spent) == 0) { + fprintf (stderr, + _("%s: failed to prepare the new %s entry '%s'\n"), + Prog, spw_dbname (), spent.sp_namp); + fail_exit (E_PW_UPDATE); + } + if (lflg && (spw_remove (user_name) == 0)) { + fprintf (stderr, + _("%s: cannot remove entry '%s' from %s\n"), + Prog, user_name, spw_dbname ()); + fail_exit (E_PW_UPDATE); + } + } +} + +/* + * move_home - move the user's home directory + * + * move_home() moves the user's home directory to a new location. The + * files will be copied if the directory cannot simply be renamed. + */ +static void move_home (void) +{ + struct stat sb; + + if (access (user_newhome, F_OK) == 0) { + /* + * If the new home directory already exist, the user + * should not use -m. + */ + fprintf (stderr, + _("%s: directory %s exists\n"), + Prog, user_newhome); + fail_exit (E_HOMEDIR); + } + + if (stat (user_home, &sb) == 0) { + /* + * Don't try to move it if it is not a directory + * (but /dev/null for example). --marekm + */ + if (!S_ISDIR (sb.st_mode)) { + fprintf (stderr, + _("%s: The previous home directory (%s) was " + "not a directory. It is not removed and no " + "home directories are created.\n"), + Prog, user_home); + fail_exit (E_HOMEDIR); + } + +#ifdef WITH_AUDIT + if (uflg || gflg) { + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "changing home directory owner", + user_newname, (unsigned int) user_newid, 1); + } +#endif + + if (rename (user_home, user_newhome) == 0) { + /* FIXME: rename above may have broken symlinks + * pointing to the user's home directory + * with an absolute path. */ + if (chown_tree (user_newhome, + user_id, uflg ? user_newid : (uid_t)-1, + user_gid, gflg ? user_newgid : (gid_t)-1) != 0) { + fprintf (stderr, + _("%s: Failed to change ownership of the home directory"), + Prog); + fail_exit (E_HOMEDIR); + } +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "moving home directory", + user_newname, (unsigned int) user_newid, + 1); +#endif + return; + } else { + if (EXDEV == errno) { + if (copy_tree (user_home, user_newhome, true, + true, + user_id, + uflg ? user_newid : (uid_t)-1, + user_gid, + gflg ? user_newgid : (gid_t)-1) == 0) { + if (remove_tree (user_home, true) != 0) { + fprintf (stderr, + _("%s: warning: failed to completely remove old home directory %s"), + Prog, user_home); + } +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, + Prog, + "moving home directory", + user_newname, + (unsigned int) user_newid, + 1); +#endif + return; + } + + (void) remove_tree (user_newhome, true); + } + fprintf (stderr, + _("%s: cannot rename directory %s to %s\n"), + Prog, user_home, user_newhome); + fail_exit (E_HOMEDIR); + } + } +} + +/* + * update_lastlog - update the lastlog file + * + * Relocate the "lastlog" entries for the user. The old entry is + * left alone in case the UID was shared. It doesn't hurt anything + * to just leave it be. + */ +static void update_lastlog (void) +{ + struct lastlog ll; + int fd; + off_t off_uid = (off_t) user_id * sizeof ll; + off_t off_newuid = (off_t) user_newid * sizeof ll; + + if (access (LASTLOG_FILE, F_OK) != 0) { + return; + } + + fd = open (LASTLOG_FILE, O_RDWR); + + if (-1 == fd) { + fprintf (stderr, + _("%s: failed to copy the lastlog entry of user %lu to user %lu: %s\n"), + Prog, (unsigned long) user_id, (unsigned long) user_newid, strerror (errno)); + return; + } + + if ( (lseek (fd, off_uid, SEEK_SET) == off_uid) + && (read (fd, &ll, sizeof ll) == (ssize_t) sizeof ll)) { + /* Copy the old entry to its new location */ + if ( (lseek (fd, off_newuid, SEEK_SET) != off_newuid) + || (write (fd, &ll, sizeof ll) != (ssize_t) sizeof ll) + || (fsync (fd) != 0) + || (close (fd) != 0)) { + fprintf (stderr, + _("%s: failed to copy the lastlog entry of user %lu to user %lu: %s\n"), + Prog, (unsigned long) user_id, (unsigned long) user_newid, strerror (errno)); + } + } else { + /* Assume lseek or read failed because there is + * no entry for the old UID */ + + /* Check if the new UID already has an entry */ + if ( (lseek (fd, off_newuid, SEEK_SET) == off_newuid) + && (read (fd, &ll, sizeof ll) == (ssize_t) sizeof ll)) { + /* Reset the new uid's lastlog entry */ + memzero (&ll, sizeof (ll)); + if ( (lseek (fd, off_newuid, SEEK_SET) != off_newuid) + || (write (fd, &ll, sizeof ll) != (ssize_t) sizeof ll) + || (fsync (fd) != 0) + || (close (fd) != 0)) { + fprintf (stderr, + _("%s: failed to copy the lastlog entry of user %lu to user %lu: %s\n"), + Prog, (unsigned long) user_id, (unsigned long) user_newid, strerror (errno)); + } + } else { + (void) close (fd); + } + } +} + +/* + * update_faillog - update the faillog file + * + * Relocate the "faillog" entries for the user. The old entry is + * left alone in case the UID was shared. It doesn't hurt anything + * to just leave it be. + */ +static void update_faillog (void) +{ + struct faillog fl; + int fd; + off_t off_uid = (off_t) user_id * sizeof fl; + off_t off_newuid = (off_t) user_newid * sizeof fl; + + if (access (FAILLOG_FILE, F_OK) != 0) { + return; + } + + fd = open (FAILLOG_FILE, O_RDWR); + + if (-1 == fd) { + fprintf (stderr, + _("%s: failed to copy the faillog entry of user %lu to user %lu: %s\n"), + Prog, (unsigned long) user_id, (unsigned long) user_newid, strerror (errno)); + return; + } + + if ( (lseek (fd, off_uid, SEEK_SET) == off_uid) + && (read (fd, (char *) &fl, sizeof fl) == (ssize_t) sizeof fl)) { + /* Copy the old entry to its new location */ + if ( (lseek (fd, off_newuid, SEEK_SET) != off_newuid) + || (write (fd, &fl, sizeof fl) != (ssize_t) sizeof fl) + || (fsync (fd) != 0) + || (close (fd) != 0)) { + fprintf (stderr, + _("%s: failed to copy the faillog entry of user %lu to user %lu: %s\n"), + Prog, (unsigned long) user_id, (unsigned long) user_newid, strerror (errno)); + } + } else { + /* Assume lseek or read failed because there is + * no entry for the old UID */ + + /* Check if the new UID already has an entry */ + if ( (lseek (fd, off_newuid, SEEK_SET) == off_newuid) + && (read (fd, &fl, sizeof fl) == (ssize_t) sizeof fl)) { + /* Reset the new uid's faillog entry */ + memzero (&fl, sizeof (fl)); + if ( (lseek (fd, off_newuid, SEEK_SET) != off_newuid) + || (write (fd, &fl, sizeof fl) != (ssize_t) sizeof fl) + || (close (fd) != 0)) { + fprintf (stderr, + _("%s: failed to copy the faillog entry of user %lu to user %lu: %s\n"), + Prog, (unsigned long) user_id, (unsigned long) user_newid, strerror (errno)); + } + } else { + (void) close (fd); + } + } +} + +#ifndef NO_MOVE_MAILBOX +/* + * This is the new and improved code to carefully chown/rename the user's + * mailbox. Maybe I am too paranoid but the mail spool dir sometimes + * happens to be mode 1777 (this makes mail user agents work without + * being setgid mail, but is NOT recommended; they all should be fixed + * to use movemail). --marekm + */ +static void move_mailbox (void) +{ + const char *maildir; + char mailfile[1024], newmailfile[1024]; + int fd; + struct stat st; + + maildir = getdef_str ("MAIL_DIR"); +#ifdef MAIL_SPOOL_DIR + if ((NULL == maildir) && (getdef_str ("MAIL_FILE") == NULL)) { + maildir = MAIL_SPOOL_DIR; + } +#endif + if (NULL == maildir) { + return; + } + + /* + * O_NONBLOCK is to make sure open won't hang on mandatory locks. + * We do fstat/fchown to make sure there are no races (someone + * replacing /var/spool/mail/luser with a hard link to /etc/passwd + * between stat and chown). --marekm + */ + (void) snprintf (mailfile, sizeof mailfile, "%s/%s", + maildir, user_name); + mailfile[(sizeof mailfile) - 1] = '\0'; + fd = open (mailfile, O_RDONLY | O_NONBLOCK, 0); + if (fd < 0) { + /* no need for warnings if the mailbox doesn't exist */ + if (errno != ENOENT) { + perror (mailfile); + } + return; + } + if (fstat (fd, &st) < 0) { + perror ("fstat"); + (void) close (fd); + return; + } + if (st.st_uid != user_id) { + /* better leave it alone */ + fprintf (stderr, _("%s: warning: %s not owned by %s\n"), + Prog, mailfile, user_name); + (void) close (fd); + return; + } + if (uflg) { + if (fchown (fd, user_newid, (gid_t) -1) < 0) { + perror (_("failed to change mailbox owner")); + } +#ifdef WITH_AUDIT + else { + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "changing mail file owner", + user_newname, (unsigned int) user_newid, 1); + } +#endif + } + + (void) close (fd); + + if (lflg) { + (void) snprintf (newmailfile, sizeof newmailfile, "%s/%s", + maildir, user_newname); + newmailfile[(sizeof newmailfile) - 1] = '\0'; + if ( (link (mailfile, newmailfile) != 0) + || (unlink (mailfile) != 0)) { + perror (_("failed to rename mailbox")); + } +#ifdef WITH_AUDIT + else { + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "changing mail file name", + user_newname, (unsigned int) user_newid, 1); + } +#endif + } +} +#endif + +/* + * main - usermod command + */ +int main (int argc, char **argv) +{ +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM + pam_handle_t *pamh = NULL; + int retval; +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ + + /* + * Get my name so that I can use it to report errors. + */ + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + OPENLOG ("usermod"); +#ifdef WITH_AUDIT + audit_help_open (); +#endif + + sys_ngroups = sysconf (_SC_NGROUPS_MAX); + user_groups = (char **) malloc (sizeof (char *) * (1 + sys_ngroups)); + user_groups[0] = (char *) 0; + + is_shadow_pwd = spw_file_present (); +#ifdef SHADOWGRP + is_shadow_grp = sgr_file_present (); +#endif +#ifdef ENABLE_SUBIDS + is_sub_uid = sub_uid_file_present (); + is_sub_gid = sub_gid_file_present (); +#endif /* ENABLE_SUBIDS */ + + process_flags (argc, argv); + + /* + * The home directory, the username and the user's UID should not + * be changed while the user is logged in. + */ + if ( (uflg || lflg || dflg +#ifdef ENABLE_SUBIDS + || Vflg || Wflg +#endif /* ENABLE_SUBIDS */ + ) + && (user_busy (user_name, user_id) != 0)) { + exit (E_USER_BUSY); + } + +#ifdef ACCT_TOOLS_SETUID +#ifdef USE_PAM + { + struct passwd *pampw; + pampw = getpwuid (getuid ()); /* local, no need for xgetpwuid */ + if (pampw == NULL) { + fprintf (stderr, + _("%s: Cannot determine your user name.\n"), + Prog); + exit (1); + } + + retval = pam_start ("usermod", pampw->pw_name, &conv, &pamh); + } + + if (PAM_SUCCESS == retval) { + retval = pam_authenticate (pamh, 0); + } + + if (PAM_SUCCESS == retval) { + retval = pam_acct_mgmt (pamh, 0); + } + + if (PAM_SUCCESS != retval) { + fprintf (stderr, _("%s: PAM: %s\n"), + Prog, pam_strerror (pamh, retval)); + SYSLOG((LOG_ERR, "%s", pam_strerror (pamh, retval))); + if (NULL != pamh) { + (void) pam_end (pamh, retval); + } + exit (1); + } + (void) pam_end (pamh, retval); +#endif /* USE_PAM */ +#endif /* ACCT_TOOLS_SETUID */ + +#ifdef WITH_TCB + if (shadowtcb_set_user (user_name) == SHADOWTCB_FAILURE) { + exit (E_PW_UPDATE); + } +#endif + + /* + * Do the hard stuff - open the files, change the user entries, + * change the home directory, then close and update the files. + */ + open_files (); + if ( cflg || dflg || eflg || fflg || gflg || Lflg || lflg || pflg + || sflg || uflg || Uflg) { + usr_update (); + } + if (Gflg || lflg) { + grp_update (); + } +#ifdef ENABLE_SUBIDS + if (Vflg) { + struct ulong_range_list_entry *ptr; + for (ptr = del_sub_uids; ptr != NULL; ptr = ptr->next) { + unsigned long count = ptr->range.last - ptr->range.first + 1; + if (sub_uid_remove(user_name, ptr->range.first, count) == 0) { + fprintf (stderr, + _("%s: failed to remove uid range %lu-%lu from '%s'\n"), + Prog, ptr->range.first, ptr->range.last, + sub_uid_dbname ()); + fail_exit (E_SUB_UID_UPDATE); + } + } + } + if (vflg) { + struct ulong_range_list_entry *ptr; + for (ptr = add_sub_uids; ptr != NULL; ptr = ptr->next) { + unsigned long count = ptr->range.last - ptr->range.first + 1; + if (sub_uid_add(user_name, ptr->range.first, count) == 0) { + fprintf (stderr, + _("%s: failed to add uid range %lu-%lu to '%s'\n"), + Prog, ptr->range.first, ptr->range.last, + sub_uid_dbname ()); + fail_exit (E_SUB_UID_UPDATE); + } + } + } + if (Wflg) { + struct ulong_range_list_entry *ptr; + for (ptr = del_sub_gids; ptr != NULL; ptr = ptr->next) { + unsigned long count = ptr->range.last - ptr->range.first + 1; + if (sub_gid_remove(user_name, ptr->range.first, count) == 0) { + fprintf (stderr, + _("%s: failed to remove gid range %lu-%lu from '%s'\n"), + Prog, ptr->range.first, ptr->range.last, + sub_gid_dbname ()); + fail_exit (E_SUB_GID_UPDATE); + } + } + } + if (wflg) { + struct ulong_range_list_entry *ptr; + for (ptr = add_sub_gids; ptr != NULL; ptr = ptr->next) { + unsigned long count = ptr->range.last - ptr->range.first + 1; + if (sub_gid_add(user_name, ptr->range.first, count) == 0) { + fprintf (stderr, + _("%s: failed to add gid range %lu-%lu to '%s'\n"), + Prog, ptr->range.first, ptr->range.last, + sub_gid_dbname ()); + fail_exit (E_SUB_GID_UPDATE); + } + } + } +#endif /* ENABLE_SUBIDS */ + close_files (); + +#ifdef WITH_TCB + if ( (lflg || uflg) + && (shadowtcb_move (user_newname, user_newid) == SHADOWTCB_FAILURE) ) { + exit (E_PW_UPDATE); + } +#endif + + nscd_flush_cache ("passwd"); + nscd_flush_cache ("group"); + +#ifdef WITH_SELINUX + if (Zflg) { + if ('\0' != *user_selinux) { + if (set_seuser (user_name, user_selinux) != 0) { + fprintf (stderr, + _("%s: warning: the user name %s to %s SELinux user mapping failed.\n"), + Prog, user_name, user_selinux); +#ifdef WITH_AUDIT + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "modifying User mapping ", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + fail_exit (E_SE_UPDATE); + } + } else { + if (del_seuser (user_name) != 0) { + fprintf (stderr, + _("%s: warning: the user name %s to SELinux user mapping removal failed.\n"), + Prog, user_name); +#ifdef WITH_AUDIT + audit_logger (AUDIT_ADD_USER, Prog, + "removing SELinux user mapping", + user_name, (unsigned int) user_id, + SHADOW_AUDIT_FAILURE); +#endif /* WITH_AUDIT */ + fail_exit (E_SE_UPDATE); + } + } + } +#endif /* WITH_SELINUX */ + + if (mflg) { + move_home (); + } + +#ifndef NO_MOVE_MAILBOX + if (lflg || uflg) { + move_mailbox (); + } +#endif /* NO_MOVE_MAILBOX */ + + if (uflg) { + update_lastlog (); + update_faillog (); + } + + if (!mflg && (uflg || gflg)) { + if (access (dflg ? user_newhome : user_home, F_OK) == 0) { + /* + * Change the UID on all of the files owned by + * `user_id' to `user_newid' in the user's home + * directory. + * + * move_home() already takes care of changing the + * ownership. + * + */ +#ifdef WITH_AUDIT + if (uflg || gflg) { + audit_logger (AUDIT_USER_CHAUTHTOK, Prog, + "changing home directory owner", + user_newname, (unsigned int) user_newid, 1); + } +#endif + if (chown_tree (dflg ? user_newhome : user_home, + user_id, + uflg ? user_newid : (uid_t)-1, + user_gid, + gflg ? user_newgid : (gid_t)-1) != 0) { + fprintf (stderr, + _("%s: Failed to change ownership of the home directory"), + Prog); + fail_exit (E_HOMEDIR); + } + } + } + + return E_SUCCESS; +} + diff --git a/src/vipw.c b/src/vipw.c new file mode 100644 index 0000000..6d730f6 --- /dev/null +++ b/src/vipw.c @@ -0,0 +1,562 @@ +/* + vipw, vigr edit the password or group file + with -s will edit shadow or gshadow file + + Copyright (c) 1997 , Guy Maor <maor@ece.utexas.edu> + Copyright (c) 1999 - 2000, Marek Michałkiewicz + Copyright (c) 2002 - 2006, Tomasz Kłoczko + Copyright (c) 2007 - 2013, Nicolas François + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. */ + +#include <config.h> + +#ident "$Id$" + +#include <errno.h> +#include <getopt.h> +#ifdef WITH_SELINUX +#include <selinux/selinux.h> +#endif /* WITH_SELINUX */ +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> +#include <utime.h> +#include "defines.h" +#include "groupio.h" +#include "nscd.h" +#include "prototypes.h" +#include "pwio.h" +#include "sgroupio.h" +#include "shadowio.h" +/*@-exitarg@*/ +#include "exitcodes.h" +#ifdef WITH_TCB +#include <tcb.h> +#include "tcbfuncs.h" +#endif /* WITH_TCB */ + +#define MSG_WARN_EDIT_OTHER_FILE _( \ + "You have modified %s.\n"\ + "You may need to modify %s for consistency.\n"\ + "Please use the command '%s' to do so.\n") + +/* + * Global variables + */ +const char *Prog; + +static const char *filename, *fileeditname; +static bool filelocked = false; +static bool createedit = false; +static int (*unlock) (void); +static bool quiet = false; +#ifdef WITH_TCB +static const char *user = NULL; +static bool tcb_mode = false; +#define SHADOWTCB_SCRATCHDIR ":tmp" +#endif /* WITH_TCB */ + +/* local function prototypes */ +static void usage (int status); +static int create_backup_file (FILE *, const char *, struct stat *); +static void vipwexit (const char *msg, int syserr, int ret); +static void vipwedit (const char *, int (*)(void), int (*)(void)); + +/* + * usage - display usage message and exit + */ +static void usage (int status) +{ + FILE *usageout = (E_SUCCESS != status) ? stderr : stdout; + (void) fprintf (stderr, + _("Usage: %s [options]\n" + "\n" + "Options:\n"), + Prog); + (void) fputs (_(" -g, --group edit group database\n"), usageout); + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -p, --passwd edit passwd database\n"), usageout); + (void) fputs (_(" -q, --quiet quiet mode\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); + (void) fputs (_(" -s, --shadow edit shadow or gshadow database\n"), usageout); +#ifdef WITH_TCB + (void) fputs (_(" -u, --user which user's tcb shadow file to edit\n"), usageout); +#endif /* WITH_TCB */ + (void) fputs (_("\n"), usageout); + exit (status); +} + +/* + * + */ +static int create_backup_file (FILE * fp, const char *backup, struct stat *sb) +{ + struct utimbuf ub; + FILE *bkfp; + int c; + mode_t mask; + + mask = umask (077); + bkfp = fopen (backup, "w"); + (void) umask (mask); + if (NULL == bkfp) { + return -1; + } + + c = 0; + if (fseeko (fp, 0, SEEK_SET) == 0) + while ((c = getc (fp)) != EOF) { + if (putc (c, bkfp) == EOF) { + break; + } + } + if ((EOF != c) || (ferror (fp) != 0) || (fflush (bkfp) != 0)) { + fclose (bkfp); + unlink (backup); + return -1; + } + if (fsync (fileno (bkfp)) != 0) { + (void) fclose (bkfp); + unlink (backup); + return -1; + } + if (fclose (bkfp) != 0) { + unlink (backup); + return -1; + } + + ub.actime = sb->st_atime; + ub.modtime = sb->st_mtime; + if ( (utime (backup, &ub) != 0) + || (chmod (backup, sb->st_mode) != 0) + || (chown (backup, sb->st_uid, sb->st_gid) != 0)) { + unlink (backup); + return -1; + } + return 0; +} + +/* + * + */ +static void vipwexit (const char *msg, int syserr, int ret) +{ + int err = errno; + + if (createedit) { + if (unlink (fileeditname) != 0) { + fprintf (stderr, _("%s: failed to remove %s\n"), Prog, fileeditname); + /* continue */ + } + } + if (filelocked) { + if ((*unlock) () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, fileeditname); + SYSLOG ((LOG_ERR, "failed to unlock %s", fileeditname)); + /* continue */ + } + } + if (NULL != msg) { + fprintf (stderr, "%s: %s", Prog, msg); + } + if (0 != syserr) { + fprintf (stderr, ": %s", strerror (err)); + } + if ( (NULL != msg) + || (0 != syserr)) { + (void) fputs ("\n", stderr); + } + if (!quiet) { + fprintf (stdout, _("%s: %s is unchanged\n"), Prog, + filename); + } + exit (ret); +} + +#ifndef DEFAULT_EDITOR +#define DEFAULT_EDITOR "vi" +#endif + +/* + * + */ +static void +vipwedit (const char *file, int (*file_lock) (void), int (*file_unlock) (void)) +{ + const char *editor; + pid_t pid; + struct stat st1, st2; + int status; + FILE *f; + /* FIXME: the following should have variable sizes */ + char filebackup[1024], fileedit[1024]; + char *to_rename; + + snprintf (filebackup, sizeof filebackup, "%s-", file); +#ifdef WITH_TCB + if (tcb_mode) { + if ( (mkdir (TCB_DIR "/" SHADOWTCB_SCRATCHDIR, 0700) != 0) + && (errno != EEXIST)) { + vipwexit (_("failed to create scratch directory"), errno, 1); + } + if (shadowtcb_drop_priv () == SHADOWTCB_FAILURE) { + vipwexit (_("failed to drop privileges"), errno, 1); + } + snprintf (fileedit, sizeof fileedit, + TCB_DIR "/" SHADOWTCB_SCRATCHDIR "/.vipw.shadow.%s", + user); + } else { +#endif /* WITH_TCB */ + snprintf (fileedit, sizeof fileedit, "%s.edit", file); +#ifdef WITH_TCB + } +#endif /* WITH_TCB */ + unlock = file_unlock; + filename = file; + fileeditname = fileedit; + + if (access (file, F_OK) != 0) { + vipwexit (file, 1, 1); + } +#ifdef WITH_SELINUX + /* if SE Linux is enabled then set the context of all new files + to be the context of the file we are editing */ + if (is_selinux_enabled () != 0) { + security_context_t passwd_context=NULL; + int ret = 0; + if (getfilecon (file, &passwd_context) < 0) { + vipwexit (_("Couldn't get file context"), errno, 1); + } + ret = setfscreatecon (passwd_context); + freecon (passwd_context); + if (0 != ret) { + vipwexit (_("setfscreatecon () failed"), errno, 1); + } + } +#endif /* WITH_SELINUX */ +#ifdef WITH_TCB + if (tcb_mode && (shadowtcb_gain_priv () == SHADOWTCB_FAILURE)) { + vipwexit (_("failed to gain privileges"), errno, 1); + } +#endif /* WITH_TCB */ + if (file_lock () == 0) { + vipwexit (_("Couldn't lock file"), errno, 5); + } + filelocked = true; +#ifdef WITH_TCB + if (tcb_mode && (shadowtcb_drop_priv () == SHADOWTCB_FAILURE)) { + vipwexit (_("failed to drop privileges"), errno, 1); + } +#endif /* WITH_TCB */ + + /* edited copy has same owners, perm */ + if (stat (file, &st1) != 0) { + vipwexit (file, 1, 1); + } + f = fopen (file, "r"); + if (NULL == f) { + vipwexit (file, 1, 1); + } +#ifdef WITH_TCB + if (tcb_mode && (shadowtcb_gain_priv () == SHADOWTCB_FAILURE)) + vipwexit (_("failed to gain privileges"), errno, 1); +#endif /* WITH_TCB */ + if (create_backup_file (f, fileedit, &st1) != 0) { + vipwexit (_("Couldn't make backup"), errno, 1); + } + (void) fclose (f); + createedit = true; + + editor = getenv ("VISUAL"); + if (NULL == editor) { + editor = getenv ("EDITOR"); + } + if (NULL == editor) { + editor = DEFAULT_EDITOR; + } + + pid = fork (); + if (-1 == pid) { + vipwexit ("fork", 1, 1); + } else if (0 == pid) { + /* use the system() call to invoke the editor so that it accepts + command line args in the EDITOR and VISUAL environment vars */ + char *buf; + int status; + + buf = (char *) malloc (strlen (editor) + strlen (fileedit) + 2); + snprintf (buf, strlen (editor) + strlen (fileedit) + 2, + "%s %s", editor, fileedit); + status = system (buf); + if (-1 == status) { + fprintf (stderr, _("%s: %s: %s\n"), Prog, editor, + strerror (errno)); + exit (1); + } else if ( WIFEXITED (status) + && (WEXITSTATUS (status) != 0)) { + fprintf (stderr, _("%s: %s returned with status %d\n"), + Prog, editor, WEXITSTATUS (status)); + exit (WEXITSTATUS (status)); + } else if (WIFSIGNALED (status)) { + fprintf (stderr, _("%s: %s killed by signal %d\n"), + Prog, editor, WTERMSIG (status)); + exit (1); + } else { + exit (0); + } + } + + for (;;) { + pid = waitpid (pid, &status, WUNTRACED); + if ((pid != -1) && (WIFSTOPPED (status) != 0)) { + /* The child (editor) was suspended. + * Suspend vipw. */ + kill (getpid (), SIGSTOP); + /* wake child when resumed */ + kill (pid, SIGCONT); + } else { + break; + } + } + + if (-1 == pid) { + vipwexit (editor, 1, 1); + } else if ( WIFEXITED (status) + && (WEXITSTATUS (status) != 0)) { + vipwexit (NULL, 0, WEXITSTATUS (status)); + } else if (WIFSIGNALED (status)) { + fprintf (stderr, _("%s: %s killed by signal %d\n"), + Prog, editor, WTERMSIG(status)); + vipwexit (NULL, 0, 1); + } + + if (stat (fileedit, &st2) != 0) { + vipwexit (fileedit, 1, 1); + } + if (st1.st_mtime == st2.st_mtime) { + vipwexit (0, 0, 0); + } +#ifdef WITH_SELINUX + /* unset the fscreatecon */ + if (is_selinux_enabled () != 0) { + if (setfscreatecon (NULL) != 0) { + vipwexit (_("setfscreatecon () failed"), errno, 1); + } + } +#endif /* WITH_SELINUX */ + + /* + * XXX - here we should check fileedit for errors; if there are any, + * ask the user what to do (edit again, save changes anyway, or quit + * without saving). Use pwck or grpck to do the check. --marekm + */ + createedit = false; +#ifdef WITH_TCB + if (tcb_mode) { + f = fopen (fileedit, "r"); + if (NULL == f) { + vipwexit (_("failed to open scratch file"), errno, 1); + } + if (unlink (fileedit) != 0) { + vipwexit (_("failed to unlink scratch file"), errno, 1); + } + if (shadowtcb_drop_priv () == SHADOWTCB_FAILURE) { + vipwexit (_("failed to drop privileges"), errno, 1); + } + if (stat (file, &st1) != 0) { + vipwexit (_("failed to stat edited file"), errno, 1); + } + to_rename = malloc (strlen (file) + 2); + if (NULL == to_rename) { + vipwexit (_("failed to allocate memory"), errno, 1); + } + snprintf (to_rename, strlen (file) + 2, "%s+", file); + if (create_backup_file (f, to_rename, &st1) != 0) { + free (to_rename); + vipwexit (_("failed to create backup file"), errno, 1); + } + (void) fclose (f); + } else { +#endif /* WITH_TCB */ + to_rename = fileedit; +#ifdef WITH_TCB + } +#endif /* WITH_TCB */ + unlink (filebackup); + link (file, filebackup); + if (rename (to_rename, file) == -1) { + fprintf (stderr, + _("%s: can't restore %s: %s (your changes are in %s)\n"), + Prog, file, strerror (errno), to_rename); +#ifdef WITH_TCB + if (tcb_mode) { + free (to_rename); + } +#endif /* WITH_TCB */ + vipwexit (0, 0, 1); + } + +#ifdef WITH_TCB + if (tcb_mode) { + free (to_rename); + if (shadowtcb_gain_priv () == SHADOWTCB_FAILURE) { + vipwexit (_("failed to gain privileges"), errno, 1); + } + } +#endif /* WITH_TCB */ + + if ((*file_unlock) () == 0) { + fprintf (stderr, _("%s: failed to unlock %s\n"), Prog, fileeditname); + SYSLOG ((LOG_ERR, "failed to unlock %s", fileeditname)); + /* continue */ + } + SYSLOG ((LOG_INFO, "file %s edited", fileeditname)); +} + +int main (int argc, char **argv) +{ + bool editshadow = false; + bool do_vipw; + + Prog = Basename (argv[0]); + + (void) setlocale (LC_ALL, ""); + (void) bindtextdomain (PACKAGE, LOCALEDIR); + (void) textdomain (PACKAGE); + + process_root_flag ("-R", argc, argv); + + do_vipw = (strcmp (Prog, "vigr") != 0); + + OPENLOG (do_vipw ? "vipw" : "vigr"); + + { + /* + * Parse the command line options. + */ + int c; + static struct option long_options[] = { + {"group", no_argument, NULL, 'g'}, + {"help", no_argument, NULL, 'h'}, + {"passwd", no_argument, NULL, 'p'}, + {"quiet", no_argument, NULL, 'q'}, + {"root", required_argument, NULL, 'R'}, + {"shadow", no_argument, NULL, 's'}, +#ifdef WITH_TCB + {"user", required_argument, NULL, 'u'}, +#endif /* WITH_TCB */ + {NULL, 0, NULL, '\0'} + }; + while ((c = getopt_long (argc, argv, +#ifdef WITH_TCB + "ghpqR:su:", +#else /* !WITH_TCB */ + "ghpqR:s", +#endif /* !WITH_TCB */ + long_options, NULL)) != -1) { + switch (c) { + case 'g': + do_vipw = false; + break; + case 'h': + usage (E_SUCCESS); + break; + case 'p': + do_vipw = true; + break; + case 'q': + quiet = true; + break; + case 'R': /* no-op, handled in process_root_flag () */ + break; + case 's': + editshadow = true; + break; +#ifdef WITH_TCB + case 'u': + user = optarg; + break; +#endif /* WITH_TCB */ + default: + usage (E_USAGE); + } + } + + if (optind != argc) { + usage (E_USAGE); + } + } + + if (do_vipw) { + if (editshadow) { +#ifdef WITH_TCB + if (getdef_bool ("USE_TCB") && (NULL != user)) { + if (shadowtcb_set_user (user) == SHADOWTCB_FAILURE) { + fprintf (stderr, + _("%s: failed to find tcb directory for %s\n"), + Prog, user); + return E_SHADOW_NOTFOUND; + } + tcb_mode = true; + } +#endif /* WITH_TCB */ + vipwedit (spw_dbname (), spw_lock, spw_unlock); + printf (MSG_WARN_EDIT_OTHER_FILE, + spw_dbname (), + pw_dbname (), + "vipw"); + } else { + vipwedit (pw_dbname (), pw_lock, pw_unlock); + if (spw_file_present ()) { + printf (MSG_WARN_EDIT_OTHER_FILE, + pw_dbname (), + spw_dbname (), + "vipw -s"); + } + } + } else { +#ifdef SHADOWGRP + if (editshadow) { + vipwedit (sgr_dbname (), sgr_lock, sgr_unlock); + printf (MSG_WARN_EDIT_OTHER_FILE, + sgr_dbname (), + gr_dbname (), + "vigr"); + } else { +#endif /* SHADOWGRP */ + vipwedit (gr_dbname (), gr_lock, gr_unlock); +#ifdef SHADOWGRP + if (sgr_file_present ()) { + printf (MSG_WARN_EDIT_OTHER_FILE, + gr_dbname (), + sgr_dbname (), + "vigr -s"); + } + } +#endif /* SHADOWGRP */ + } + + nscd_flush_cache ("passwd"); + nscd_flush_cache ("group"); + + return E_SUCCESS; +} + |