/*
 * Copyright 1991 - 1994, Julianne Frances Haugh
 * 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. Neither the name of Julianne F. Haugh nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY JULIE HAUGH 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 JULIE HAUGH 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 "rcsid.h"
RCSID (PKG_VER "$Id: usermod.c,v 1.47 2005/07/07 15:11:48 kloczek Exp $")
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <pwd.h>
#include <grp.h>
#include <ctype.h>
#include <fcntl.h>
#include <time.h>
#ifdef USE_PAM
#include <security/pam_appl.h>
#include <security/pam_misc.h>
#include <pwd.h>
#endif				/* USE_PAM */
#include "prototypes.h"
#include "defines.h"
#include "chkname.h"
#include "faillog.h"
#include <lastlog.h>
#include "pwauth.h"
#include "getdef.h"
#include "groupio.h"
#include "nscd.h"
#include "pwio.h"
#ifdef	SHADOWGRP
#include "sgroupio.h"
#endif
#include "shadowio.h"
/*
 * 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).
 */
#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	VALID(s)	(strcspn (s, ":\n") == strlen (s))
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_home;
static char *user_newhome;
static char *user_shell;

static long user_expire;
static long user_inactive;
static long sys_ngroups;
static char **user_groups;	/* NULL-terminated list */

static char *Prog;

static int
 aflg = 0,			/* append to existing secondary group set */
    cflg = 0,			/* new comment (GECOS) field */
    dflg = 0,			/* new home directory */
    eflg = 0,			/* days since 1970-01-01 when account becomes expired */
    fflg = 0,			/* days until account with expired password is locked */
    gflg = 0,			/* new primary group ID */
    Gflg = 0,			/* new secondary group set */
    Lflg = 0,			/* lock the password */
    lflg = 0,			/* new user name */
    mflg = 0,			/* create user's home directory if it doesn't exist */
    oflg = 0,			/* permit non-unique user ID to be specified with -u */
    pflg = 0,			/* new encrypted password */
    sflg = 0,			/* new shell program */
    uflg = 0,			/* specify new user ID */
    Uflg = 0;			/* unlock the password */

static int is_shadow_pwd;

#ifdef SHADOWGRP
static int is_shadow_grp;
#endif


/* local function prototypes */
static int get_groups (char *);
static void usage (void);
static void new_pwent (struct passwd *);

static void new_spent (struct spwd *);
static void fail_exit (int);
static int update_group (void);

#ifdef SHADOWGRP
static int update_gshadow (void);
#endif
static int grp_update (void);

static long get_number (const char *);
static uid_t get_id (const char *);
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_files (void);

#ifndef NO_MOVE_MAILBOX
static void move_mailbox (void);
#endif

/*
 * Had to move this over from useradd.c since we have groups named
 * "56k-family"... ergh.
 * --Pac.
 */
static struct group *getgr_nam_gid (const char *name)
{
	gid_t gid;
	char *ep;

	gid = strtoul (name, &ep, 10);
	if (*name != '\0' && *ep == '\0')	/* valid numeric GID */
		return getgrgid (gid);

	return getgrnam (name);
}


/*
 * 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 (!*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
		 */
		if ((cp = strchr (list, ',')))
			*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.
		 */
		if (!grp) {
			fprintf (stderr, _("%s: unknown group %s\n"),
				 Prog, list);
			errors++;
		}
		list = cp;

		/*
		 * If the group doesn't exist, don't dump core. Instead,
		 * try the next one.  --marekm
		 */
		if (!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 (list);

	user_groups[ngroups] = (char *) 0;

	/*
	 * Any errors in finding group names are fatal
	 */

	if (errors)
		return -1;

	return 0;
}

/*
 * usage - display usage message and exit
 */

static void usage (void)
{
	fprintf (stderr,
		 _
		 ("Usage: %s\t[-u uid [-o]] [-g group] [[-G group,...] [-a]] \n"),
		 Prog);
	fprintf (stderr,
		 _
		 ("\t\t[-d home [-m]] [-s shell] [-c comment] [-l new_name]\n"));
	fprintf (stderr, "\t\t");
	fprintf (stderr, _("[-f inactive] [-e expire] "));
	fprintf (stderr, _("[-p passwd] [-L|-U] name\n"));
	exit (E_USAGE);
}

/*
 * update encrypted password string (for both shadow and non-shadow
 * passwords)
 */

static char *new_pw_passwd (char *pw_pass, const char *pw_name)
{
	if (Lflg && pw_pass[0] != '!') {
		char *buf = xmalloc (strlen (pw_pass) + 2);

		SYSLOG ((LOG_INFO, "lock user `%s' password", pw_name));
		strcpy (buf, "!");
		strcat (buf, pw_pass);
		pw_pass = buf;
	} else if (Uflg && pw_pass[0] == '!') {
		char *s;

		SYSLOG ((LOG_INFO, "unlock user `%s' password", pw_name));
		s = pw_pass;
		while (*s) {
			*s = *(s + 1);
			s++;
		}
	} else if (pflg) {
		SYSLOG ((LOG_INFO, "change user `%s' password", pw_name));
		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) {
		SYSLOG ((LOG_INFO, "change user name `%s' to `%s'",
			 pwent->pw_name, user_newname));
		pwent->pw_name = xstrdup (user_newname);
	}
	if (!is_shadow_pwd)
		pwent->pw_passwd =
		    new_pw_passwd (pwent->pw_passwd, pwent->pw_name);

	if (uflg) {
		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) {
		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)
		pwent->pw_gecos = user_comment;

	if (dflg) {
		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) {
		SYSLOG ((LOG_INFO,
			 "change user `%s' shell from `%s' to `%s'",
			 pwent->pw_name, pwent->pw_shell, user_shell));
		pwent->pw_shell = user_shell;
	}
}

/*
 * 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)
		spent->sp_namp = xstrdup (user_newname);

	if (fflg) {
		SYSLOG ((LOG_INFO,
			 "change user `%s' inactive from `%ld' to `%ld'",
			 spent->sp_namp, spent->sp_inact, user_inactive));
		spent->sp_inact = user_inactive;
	}
	if (eflg) {
		/* XXX - dates might be better than numbers of days.  --marekm */
		SYSLOG ((LOG_INFO,
			 "change user `%s' expiration from `%ld' to `%ld'",
			 spent->sp_namp, spent->sp_expire, user_expire));
		spent->sp_expire = user_expire;
	}
	spent->sp_pwdp = new_pw_passwd (spent->sp_pwdp, spent->sp_namp);
}

/*
 * fail_exit - exit with an error code after unlocking files
 */

static void fail_exit (int code)
{
	(void) gr_unlock ();
#ifdef	SHADOWGRP
	if (is_shadow_grp)
		sgr_unlock ();
#endif
	if (is_shadow_pwd)
		spw_unlock ();
	(void) pw_unlock ();
	exit (code);
}


static int update_group (void)
{
	int is_member;
	int was_member;
	int changed;
	const struct group *grp;
	struct group *ngrp;

	/*
	 * Lock and open the group file. This will load all of the group
	 * entries.
	 */
	if (!gr_lock ()) {
		fprintf (stderr, _("%s: error locking group file\n"), Prog);
		SYSLOG ((LOG_ERR, "error locking group file"));
		return -1;
	}
	if (!gr_open (O_RDWR)) {
		fprintf (stderr, _("%s: error opening group file\n"), Prog);
		SYSLOG ((LOG_ERR, "error opening group file"));
		gr_unlock ();
		return -1;
	}

	changed = 0;

	/*
	 * Scan through the entire group file looking for the groups that
	 * the user is a member of.
	 */
	while ((grp = gr_next ())) {

		/*
		 * 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 && is_on_list (user_groups, grp->gr_name);

		if (!was_member && !is_member)
			continue;

		ngrp = __gr_dup (grp);
		if (!ngrp) {
			fprintf (stderr,
				 _("%s: out of memory in update_group\n"),
				 Prog);
			gr_unlock ();
			return -1;
		}

		if (was_member && (!Gflg || is_member)) {
			if (lflg) {
				ngrp->gr_mem = del_list (ngrp->gr_mem,
							 user_name);
				ngrp->gr_mem = add_list (ngrp->gr_mem,
							 user_newname);
				changed = 1;
				SYSLOG ((LOG_INFO,
					 "change `%s' to `%s' in group `%s'",
					 user_name, user_newname,
					 ngrp->gr_name));
			}
		} else if (was_member && !aflg && Gflg && !is_member) {
			ngrp->gr_mem = del_list (ngrp->gr_mem, user_name);
			changed = 1;
			SYSLOG ((LOG_INFO, "delete `%s' from group `%s'",
				 user_name, ngrp->gr_name));
		} else if (!was_member && Gflg && is_member) {
			ngrp->gr_mem = add_list (ngrp->gr_mem,
						 lflg ? user_newname :
						 user_name);
			changed = 1;
			SYSLOG ((LOG_INFO, "add `%s' to group `%s'",
				 lflg ? user_newname : user_name,
				 ngrp->gr_name));
		}
		if (!changed)
			continue;

		changed = 0;
		if (!gr_update (ngrp)) {
			fprintf (stderr,
				 _("%s: error adding new group entry\n"), Prog);
			SYSLOG ((LOG_ERR, "error adding group entry"));
			gr_unlock ();
			return -1;
		}
	}

	if (!gr_close ()) {
		fprintf (stderr, _("%s: cannot rewrite group file\n"), Prog);
		gr_unlock ();
		return -1;
	}
	gr_unlock ();
	return 0;
}

#ifdef SHADOWGRP
static int update_gshadow (void)
{
	int is_member;
	int was_member;
	int was_admin;
	int changed;
	const struct sgrp *sgrp;
	struct sgrp *nsgrp;

	if (!sgr_lock ()) {
		fprintf (stderr,
			 _("%s: error locking shadow group file\n"), Prog);
		SYSLOG ((LOG_ERR, "error locking shadow group file"));
		return -1;
	}
	if (!sgr_open (O_RDWR)) {
		fprintf (stderr,
			 _("%s: error opening shadow group file\n"), Prog);
		SYSLOG ((LOG_ERR, "error opening shadow group file"));
		sgr_unlock ();
		return -1;
	}

	changed = 0;

	/*
	 * Scan through the entire shadow group file looking for the groups
	 * that the user is a member of.
	 */
	while ((sgrp = sgr_next ())) {

		/*
		 * 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 && is_on_list (user_groups, sgrp->sg_name);

		if (!was_member && !was_admin && !is_member)
			continue;

		nsgrp = __sgr_dup (sgrp);
		if (!nsgrp) {
			fprintf (stderr,
				 _
				 ("%s: out of memory in update_gshadow\n"),
				 Prog);
			sgr_unlock ();
			return -1;
		}

		if (was_admin && lflg) {
			nsgrp->sg_adm = del_list (nsgrp->sg_adm, user_name);
			nsgrp->sg_adm = add_list (nsgrp->sg_adm, user_newname);
			changed = 1;
			SYSLOG ((LOG_INFO,
				 "change admin `%s' to `%s' in shadow group `%s'",
				 user_name, user_newname, nsgrp->sg_name));
		}
		if (was_member && (!Gflg || is_member)) {
			if (lflg) {
				nsgrp->sg_mem = del_list (nsgrp->sg_mem,
							  user_name);
				nsgrp->sg_mem = add_list (nsgrp->sg_mem,
							  user_newname);
				changed = 1;
				SYSLOG ((LOG_INFO,
					 "change `%s' to `%s' in shadow group `%s'",
					 user_name, user_newname,
					 nsgrp->sg_name));
			}
		} else if (was_member && !aflg && Gflg && !is_member) {
			nsgrp->sg_mem = del_list (nsgrp->sg_mem, user_name);
			changed = 1;
			SYSLOG ((LOG_INFO,
				 "delete `%s' from shadow group `%s'",
				 user_name, nsgrp->sg_name));
		} else if (!was_member && Gflg && is_member) {
			nsgrp->sg_mem = add_list (nsgrp->sg_mem,
						  lflg ? user_newname :
						  user_name);
			changed = 1;
			SYSLOG ((LOG_INFO, "add `%s' to shadow group `%s'",
				 lflg ? user_newname : user_name,
				 nsgrp->sg_name));
		}
		if (!changed)
			continue;

		changed = 0;

		/* 
		 * Update the group entry to reflect the changes.
		 */
		if (!sgr_update (nsgrp)) {
			fprintf (stderr,
				 _("%s: error adding new group entry\n"), Prog);
			SYSLOG ((LOG_ERR, "error adding shadow group entry"));
			sgr_unlock ();
			return -1;
		}
	}

	if (!sgr_close ()) {
		fprintf (stderr,
			 _("%s: cannot rewrite shadow group file\n"), Prog);
		sgr_unlock ();
		return -1;
	}
	sgr_unlock ();
	return 0;
}
#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 int grp_update (void)
{
	int ret;

	ret = update_group ();
#ifdef SHADOWGRP
	if (!ret && is_shadow_grp)
		ret = update_gshadow ();
#endif
	return ret;
}

static long get_number (const char *cp)
{
	long val;
	char *ep;

	val = strtol (cp, &ep, 10);
	if (*cp != '\0' && *ep == '\0')	/* valid number */
		return val;

	fprintf (stderr, _("%s: invalid numeric argument `%s'\n"), Prog, cp);
	exit (E_BAD_ARG);
}

static uid_t get_id (const char *cp)
{
	uid_t val;
	char *ep;

	val = strtoul (cp, &ep, 10);
	if (*cp != '\0' && *ep == '\0')	/* valid number */
		return val;

	fprintf (stderr, _("%s: invalid numeric argument `%s'\n"), Prog, cp);
	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)
{
	const struct group *grp;
	const struct passwd *pwd;

	const struct spwd *spwd = NULL;
	int anyflag = 0;
	int arg;

	if (argc == 1 || argv[argc - 1][0] == '-')
		usage ();

	if (!(pwd = getpwnam (argv[argc - 1]))) {
		fprintf (stderr, _("%s: user %s does not exist\n"),
			 Prog, argv[argc - 1]);
		exit (E_NOTFOUND);
	}
	user_name = argv[argc - 1];

#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
	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);

	if (is_shadow_pwd && (spwd = getspnam (user_name))) {
		user_expire = spwd->sp_expire;
		user_inactive = spwd->sp_inact;
	}

	while ((arg =
		getopt (argc, argv, "ac:d:e:f:g:G:l:Lmop:s:u:U")) != EOF) {
		switch (arg) {
		case 'a':
			aflg++;
			break;
		case 'c':
			if (!VALID (optarg)) {
				fprintf (stderr,
					 _("%s: invalid field `%s'\n"),
					 Prog, optarg);
				exit (E_BAD_ARG);
			}
			user_comment = optarg;
			cflg++;
			break;
		case 'd':
			if (!VALID (optarg)) {
				fprintf (stderr,
					 _("%s: invalid field `%s'\n"),
					 Prog, optarg);
				exit (E_BAD_ARG);
			}
			dflg++;
			user_newhome = optarg;
			break;
		case 'e':
			if (*optarg) {
				user_expire = strtoday (optarg);
				if (user_expire == -1) {
					fprintf (stderr,
						 _
						 ("%s: invalid date `%s'\n"),
						 Prog, optarg);
					exit (E_BAD_ARG);
				}
				user_expire *= DAY / SCALE;
			} else
				user_expire = -1;
			eflg++;
			break;
		case 'f':
			user_inactive = get_number (optarg);
			fflg++;
			break;
		case 'g':
			grp = getgr_nam_gid (optarg);
			if (!grp) {
				fprintf (stderr,
					 _("%s: unknown group %s\n"),
					 Prog, optarg);
				exit (E_NOTFOUND);
			}
			user_newgid = grp->gr_gid;
			gflg++;
			break;
		case 'G':
			if (get_groups (optarg))
				exit (E_NOTFOUND);
			Gflg++;
			break;
		case 'l':
			if (!check_user_name (optarg)) {
				fprintf (stderr,
					 _("%s: invalid field `%s'\n"),
					 Prog, optarg);
				exit (E_BAD_ARG);
			}

			/*
			 * If the name does not really change, we mustn't
			 * set the flag as this will cause rather serious
			 * problems later!
			 */

			if (strcmp (user_name, optarg))
				lflg++;

			user_newname = optarg;
			break;
		case 'L':
			if (Uflg || pflg)
				usage ();

			Lflg++;
			break;
		case 'm':
			if (!dflg)
				usage ();

			mflg++;
			break;
		case 'o':
			if (!uflg)
				usage ();

			oflg++;
			break;
		case 'p':
			if (Lflg || Uflg)
				usage ();

			user_pass = optarg;
			pflg++;
			break;
		case 's':
			if (!VALID (optarg)) {
				fprintf (stderr,
					 _("%s: invalid field `%s'\n"),
					 Prog, optarg);
				exit (E_BAD_ARG);
			}
			user_shell = optarg;
			sflg++;
			break;
		case 'u':
			user_newid = get_id (optarg);
			uflg++;
			break;
		case 'U':
			if (Lflg && pflg)
				usage ();

			Uflg++;
			break;
		default:
			usage ();
		}
		anyflag++;
	}
	if (anyflag == 0) {
		fprintf (stderr, _("%s: no flags given\n"), Prog);
		exit (E_USAGE);
	}
	if (!is_shadow_pwd && (eflg || fflg)) {
		fprintf (stderr,
			 _
			 ("%s: shadow passwords required for -e and -f\n"),
			 Prog);
		exit (E_USAGE);
	}

	if (optind != argc - 1)
		usage ();

	if (aflg && (!Gflg)) {
		fprintf (stderr,
			 _("%s: -a flag is ONLY allowed with the -G flag\n"),
			 Prog);
		usage ();
		exit (E_USAGE);
	}

	if (dflg && strcmp (user_home, user_newhome) == 0)
		dflg = mflg = 0;

	if (uflg && user_id == user_newid)
		uflg = oflg = 0;

	if (lflg && getpwnam (user_newname)) {
		fprintf (stderr, _("%s: user %s exists\n"), Prog, user_newname);
		exit (E_NAME_IN_USE);
	}

	if (uflg && !oflg && getpwuid (user_newid)) {
		fprintf (stderr, _("%s: uid %lu is not unique\n"),
			 Prog, (unsigned long) user_newid);
		exit (E_UID_IN_USE);
	}
}

/*
 * 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 ()) {
		fprintf (stderr, _("%s: cannot rewrite password file\n"), Prog);
		fail_exit (E_PW_UPDATE);
	}
	if (is_shadow_pwd && !spw_close ()) {
		fprintf (stderr,
			 _("%s: cannot rewrite shadow password file\n"), Prog);
		fail_exit (E_PW_UPDATE);
	}
	if (is_shadow_pwd)
		spw_unlock ();
	(void) pw_unlock ();

	/*
	 * 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 ()) {
		fprintf (stderr, _("%s: unable to lock password file\n"), Prog);
		exit (E_PW_UPDATE);
	}
	if (!pw_open (O_RDWR)) {
		fprintf (stderr, _("%s: unable to open password file\n"), Prog);
		fail_exit (E_PW_UPDATE);
	}
	if (is_shadow_pwd && !spw_lock ()) {
		fprintf (stderr,
			 _("%s: cannot lock shadow password file\n"), Prog);
		fail_exit (E_PW_UPDATE);
	}
	if (is_shadow_pwd && !spw_open (O_RDWR)) {
		fprintf (stderr,
			 _("%s: cannot open shadow password file\n"), Prog);
		fail_exit (E_PW_UPDATE);
	}
}

/*
 * 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 (!pwd) {
		fprintf (stderr, _("%s: %s not found in /etc/passwd\n"),
			 Prog, user_name);
		fail_exit (E_NOTFOUND);
	}
	pwent = *pwd;
	new_pwent (&pwent);


	/* 
	 * Locate the entry in /etc/shadow. It doesn't have to exist, and
	 * won't be created if it doesn't.
	 */

	if (is_shadow_pwd && (spwd = spw_locate (user_name))) {
		spent = *spwd;
		new_spent (&spent);
	}

	if (lflg || uflg || gflg || cflg || dflg || sflg || pflg
	    || Lflg || Uflg) {
		if (!pw_update (&pwent)) {
			fprintf (stderr,
				 _("%s: error changing password entry\n"),
				 Prog);
			fail_exit (E_PW_UPDATE);
		}
		if (lflg && !pw_remove (user_name)) {
			fprintf (stderr,
				 _("%s: error removing password entry\n"),
				 Prog);
			fail_exit (E_PW_UPDATE);
		}
	}
	if (spwd && (lflg || eflg || fflg || pflg || Lflg || Uflg)) {
		if (!spw_update (&spent)) {
			fprintf (stderr,
				 _
				 ("%s: error adding new shadow password entry\n"),
				 Prog);
			fail_exit (E_PW_UPDATE);
		}
		if (lflg && !spw_remove (user_name)) {
			fprintf (stderr,
				 _
				 ("%s: error removing shadow password entry\n"),
				 Prog);
			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 (mflg && 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))
			return;

		if (access (user_newhome, F_OK) == 0) {
			fprintf (stderr, _("%s: directory %s exists\n"),
				 Prog, user_newhome);
			fail_exit (E_HOMEDIR);
		} else if (rename (user_home, user_newhome)) {
			if (errno == EXDEV) {
				if (mkdir (user_newhome, sb.st_mode & 0777)) {
					fprintf (stderr,
						 _
						 ("%s: can't create %s\n"),
						 Prog, user_newhome);
				}
				if (chown (user_newhome, sb.st_uid, sb.st_gid)) {
					fprintf (stderr,
						 _("%s: can't chown %s\n"),
						 Prog, user_newhome);
					rmdir (user_newhome);
					fail_exit (E_HOMEDIR);
				}
				if (copy_tree (user_home, user_newhome,
					       uflg ? user_newid : -1,
					       gflg ? user_newgid : -1) == 0) {
					if (remove_tree (user_home) != 0 ||
					    rmdir (user_home) != 0)
						fprintf (stderr,
							 _
							 ("%s: warning: failed to completely remove old home directory %s"),
							 Prog, user_home);
					return;
				}

				(void) remove_tree (user_newhome);
				(void) rmdir (user_newhome);
			}
			fprintf (stderr,
				 _
				 ("%s: cannot rename directory %s to %s\n"),
				 Prog, user_home, user_newhome);
			fail_exit (E_HOMEDIR);
		}
	}
	if (uflg || gflg)
		chown (dflg ? user_newhome : user_home,
		       uflg ? user_newid : user_id,
		       gflg ? user_newgid : user_gid);
}

/*
 * update_files - update the lastlog and faillog files
 */

static void update_files (void)
{
	struct lastlog ll;
	struct faillog fl;
	int fd;

	/*
	 * 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.
	 */

	if ((fd = open (LASTLOG_FILE, O_RDWR)) != -1) {
		lseek (fd, (off_t) user_id * sizeof ll, SEEK_SET);
		if (read (fd, (char *) &ll, sizeof ll) == sizeof ll) {
			lseek (fd, (off_t) user_newid * sizeof ll, SEEK_SET);
			write (fd, (char *) &ll, sizeof ll);
		}
		close (fd);
	}

	/*
	 * Relocate the "faillog" entries in the same manner.
	 */

	if ((fd = open (FAILLOG_FILE, O_RDWR)) != -1) {
		lseek (fd, (off_t) user_id * sizeof fl, SEEK_SET);
		if (read (fd, (char *) &fl, sizeof fl) == sizeof fl) {
			lseek (fd, (off_t) user_newid * sizeof fl, SEEK_SET);
			write (fd, (char *) &fl, sizeof fl);
		}
		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 (!maildir && !getdef_str ("MAIL_FILE"))
		maildir = MAIL_SPOOL_DIR;
#endif
	if (!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
	 */

	snprintf (mailfile, sizeof mailfile, "%s/%s", maildir, user_name);
	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");
		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);
		close (fd);
		return;
	}
	if (uflg && fchown (fd, user_newid, (gid_t) - 1) < 0)
		perror (_("failed to change mailbox owner"));

	close (fd);

	if (lflg) {
		snprintf (newmailfile, sizeof newmailfile, "%s/%s",
			  maildir, user_newname);
		if (link (mailfile, newmailfile) || unlink (mailfile))
			perror (_("failed to rename mailbox"));
	}
}
#endif

#ifdef USE_PAM
static struct pam_conv conv = {
	misc_conv,
	NULL
};
#endif				/* USE_PAM */

/*
 * main - usermod command
 */

int main (int argc, char **argv)
{
	int grp_err = 0;

#ifdef USE_PAM
	pam_handle_t *pamh = NULL;
	struct passwd *pampw;
	int retval;
#endif

	/*
	 * Get my name so that I can use it to report errors.
	 */
	Prog = Basename (argv[0]);

	setlocale (LC_ALL, "");
	bindtextdomain (PACKAGE, LOCALEDIR);
	textdomain (PACKAGE);

	sys_ngroups = sysconf (_SC_NGROUPS_MAX);
	user_groups = malloc ((1 + sys_ngroups) * sizeof (char *));
	user_groups[0] = (char *) 0;

	OPENLOG ("usermod");

	is_shadow_pwd = spw_file_present ();
#ifdef SHADOWGRP
	is_shadow_grp = sgr_file_present ();
#endif

	process_flags (argc, argv);

#ifdef USE_PAM
	retval = PAM_SUCCESS;

	pampw = getpwuid (getuid ());
	if (pampw == NULL) {
		retval = PAM_USER_UNKNOWN;
	}

	if (retval == PAM_SUCCESS) {
		retval = pam_start ("usermod", pampw->pw_name, &conv, &pamh);
	}

	if (retval == PAM_SUCCESS) {
		retval = pam_authenticate (pamh, 0);
		if (retval != PAM_SUCCESS) {
			pam_end (pamh, retval);
		}
	}

	if (retval == PAM_SUCCESS) {
		retval = pam_acct_mgmt (pamh, 0);
		if (retval != PAM_SUCCESS) {
			pam_end (pamh, retval);
		}
	}

	if (retval != PAM_SUCCESS) {
		fprintf (stderr, _("%s: PAM authentication failed\n"), Prog);
		exit (1);
	}
#endif				/* USE_PAM */

	/*
	 * Do the hard stuff - open the files, change the user entries,
	 * change the home directory, then close and update the files.
	 */

	open_files ();

	usr_update ();
	nscd_flush_cache ("passwd");
	nscd_flush_cache ("group");

	close_files ();

	if (Gflg || lflg)
		grp_err = grp_update ();

	if (mflg)
		move_home ();

#ifndef NO_MOVE_MAILBOX
	if (lflg || uflg)
		move_mailbox ();
#endif

	if (uflg) {
		update_files ();

		/*
		 * Change the UID on all of the files owned by `user_id' to
		 * `user_newid' in the user's home directory.
		 */

		chown_tree (dflg ? user_newhome : user_home,
			    user_id, user_newid,
			    user_gid, gflg ? user_newgid : user_gid);
	}

	if (grp_err)
		exit (E_GRP_UPDATE);

#ifdef USE_PAM
	if (retval == PAM_SUCCESS) {
		retval = pam_chauthtok (pamh, 0);
		if (retval != PAM_SUCCESS) {
			pam_end (pamh, retval);
		}
	}

	if (retval != PAM_SUCCESS) {
		fprintf (stderr, _("%s: PAM authentication failed\n"), Prog);
		exit (1);
	}

	if (retval == PAM_SUCCESS)
		pam_end (pamh, PAM_SUCCESS);
#endif				/* USE_PAM */

	exit (E_SUCCESS);
	/* NOT REACHED */
}
