/*
 * SPDX-FileCopyrightText: 1992 - 1994, Julianne Frances Haugh
 * SPDX-FileCopyrightText: 1996 - 2000, Marek Michałkiewicz
 * SPDX-FileCopyrightText: 2001       , Michał Moskal
 * SPDX-FileCopyrightText: 2001 - 2006, Tomasz Kłoczko
 * SPDX-FileCopyrightText: 2007 - 2011, Nicolas François
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

#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"
#include "sssd.h"
#ifdef WITH_TCB
#include "tcbfuncs.h"
#endif				/* WITH_TCB */
#include "shadowlog.h"

/*
 * 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);

extern int allow_bad_names;

/*
 * 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 (_("  -b, --badnames                allow bad names\n"), usageout);
	(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[] = {
		{"badnames",  no_argument,       NULL, 'b'},
		{"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, "behqrR:s",
	                         long_options, NULL)) != -1) {
		switch (c) {
		case 'b':
			allow_bad_names = true;
			break;
		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_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_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;
	uid_t min_sys_id = (uid_t) getdef_ulong ("SYS_UID_MIN", 101UL);
	uid_t max_sys_id = (uid_t) getdef_ulong ("SYS_UID_MAX", 999UL);

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

		/*
		 * If uid is system and has a home directory, then check
		 */
		if (!(pwd->pw_uid >= min_sys_id && pwd->pw_uid <= max_sys_id && pwd->pw_dir && pwd->pw_dir[0])) {
			/*
			 * Make sure the home directory exists
			 */
			if (!quiet && (access (pwd->pw_dir, F_OK) != 0)) {
				const char *nonexistent = getdef_str("NONEXISTENT");

				/*
				 * Home directory does not exist, give a warning (unless intentional)
				 */
				if (NULL == nonexistent || strcmp (pwd->pw_dir, nonexistent) != 0) {
					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_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) gettime () / 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]);
	log_set_progname(Prog);
	log_set_logfd(stderr);

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

	if (!read_only) {
		nscd_flush_cache ("passwd");
		sssd_flush_cache (SSSD_DB_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);
}

