pw_user.c revision 290153
1178355Ssam/*-
2178355Ssam * Copyright (C) 1996
3178355Ssam *	David L. Nugent.  All rights reserved.
4178355Ssam *
5178355Ssam * Redistribution and use in source and binary forms, with or without
6178355Ssam * modification, are permitted provided that the following conditions
7178355Ssam * are met:
8178355Ssam * 1. Redistributions of source code must retain the above copyright
9178355Ssam *    notice, this list of conditions and the following disclaimer.
10178355Ssam * 2. Redistributions in binary form must reproduce the above copyright
11178355Ssam *    notice, this list of conditions and the following disclaimer in the
12178355Ssam *    documentation and/or other materials provided with the distribution.
13178355Ssam *
14178355Ssam * THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT AND CONTRIBUTORS ``AS IS'' AND
15178355Ssam * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16178355Ssam * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17178355Ssam * ARE DISCLAIMED.  IN NO EVENT SHALL DAVID L. NUGENT OR CONTRIBUTORS BE LIABLE
18178355Ssam * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19178355Ssam * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20178355Ssam * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21178355Ssam * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22178355Ssam * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23178355Ssam * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24178355Ssam * SUCH DAMAGE.
25178355Ssam *
26178355Ssam */
27178355Ssam
28178355Ssam#ifndef lint
29178355Ssamstatic const char rcsid[] =
30178355Ssam  "$FreeBSD: head/usr.sbin/pw/pw_user.c 290153 2015-10-29 18:29:28Z bdrewery $";
31178355Ssam#endif /* not lint */
32178355Ssam
33178355Ssam#include <sys/param.h>
34178355Ssam#include <sys/resource.h>
35178355Ssam#include <sys/time.h>
36178355Ssam#include <sys/types.h>
37178355Ssam
38178355Ssam#include <ctype.h>
39178355Ssam#include <dirent.h>
40178355Ssam#include <err.h>
41178355Ssam#include <errno.h>
42186106Ssam#include <fcntl.h>
43178355Ssam#include <grp.h>
44178355Ssam#include <pwd.h>
45178355Ssam#include <libutil.h>
46178355Ssam#include <login_cap.h>
47178355Ssam#include <paths.h>
48178355Ssam#include <string.h>
49178355Ssam#include <sysexits.h>
50178355Ssam#include <termios.h>
51178355Ssam#include <unistd.h>
52178355Ssam
53178355Ssam#include "pw.h"
54178355Ssam#include "bitmap.h"
55178355Ssam#include "psdate.h"
56178355Ssam
57178355Ssam#define LOGNAMESIZE (MAXLOGNAME-1)
58178355Ssam
59178355Ssamstatic		char locked_str[] = "*LOCKED*";
60178355Ssam
61178355Ssamstatic struct passwd fakeuser = {
62178355Ssam	"nouser",
63178355Ssam	"*",
64178355Ssam	-1,
65178355Ssam	-1,
66178355Ssam	0,
67178355Ssam	"",
68178355Ssam	"User &",
69178355Ssam	"/nonexistent",
70178355Ssam	"/bin/sh",
71178355Ssam	0,
72178355Ssam	0
73178355Ssam};
74178355Ssam
75178355Ssamstatic int	 print_user(struct passwd *pwd, bool pretty, bool v7);
76178355Ssamstatic uid_t	 pw_uidpolicy(struct userconf *cnf, intmax_t id);
77178355Ssamstatic uid_t	 pw_gidpolicy(struct userconf *cnf, char *grname, char *nam,
78178355Ssam    gid_t prefer, bool dryrun);
79178355Ssamstatic char	*pw_homepolicy(struct userconf * cnf, char *homedir,
80178355Ssam    const char *user);
81178355Ssamstatic char	*pw_shellpolicy(struct userconf * cnf);
82178355Ssamstatic char	*pw_password(struct userconf * cnf, char const * user,
83178355Ssam    bool dryrun);
84178355Ssamstatic char	*shell_path(char const * path, char *shells[], char *sh);
85178355Ssamstatic void	rmat(uid_t uid);
86178355Ssamstatic void	rmopie(char const * name);
87178355Ssam
88178355Ssamstatic void
89178355Ssammkdir_home_parents(int dfd, const char *dir)
90178355Ssam{
91178355Ssam	struct stat st;
92178355Ssam	char *dirs, *tmp;
93178355Ssam
94178355Ssam	if (*dir != '/')
95178355Ssam		errx(EX_DATAERR, "invalid base directory for home '%s'", dir);
96178355Ssam
97178355Ssam	dir++;
98178355Ssam
99178355Ssam	if (fstatat(dfd, dir, &st, 0) != -1) {
100178355Ssam		if (S_ISDIR(st.st_mode))
101178355Ssam			return;
102178355Ssam		errx(EX_OSFILE, "root home `/%s' is not a directory", dir);
103178355Ssam	}
104178355Ssam
105178355Ssam	dirs = strdup(dir);
106178355Ssam	if (dirs == NULL)
107178355Ssam		errx(EX_UNAVAILABLE, "out of memory");
108178355Ssam
109178355Ssam	tmp = strrchr(dirs, '/');
110178355Ssam	if (tmp == NULL) {
111178355Ssam		free(dirs);
112178355Ssam		return;
113178355Ssam	}
114178355Ssam	tmp[0] = '\0';
115178355Ssam
116178355Ssam	/*
117178355Ssam	 * This is a kludge especially for Joerg :)
118178355Ssam	 * If the home directory would be created in the root partition, then
119178355Ssam	 * we really create it under /usr which is likely to have more space.
120178355Ssam	 * But we create a symlink from cnf->home -> "/usr" -> cnf->home
121178355Ssam	 */
122178355Ssam	if (strchr(dirs, '/') == NULL) {
123178355Ssam		asprintf(&tmp, "usr/%s", dirs);
124178355Ssam		if (tmp == NULL)
125178355Ssam			errx(EX_UNAVAILABLE, "out of memory");
126178355Ssam		if (mkdirat(dfd, tmp, _DEF_DIRMODE) != -1 || errno == EEXIST) {
127178355Ssam			fchownat(dfd, tmp, 0, 0, 0);
128178355Ssam			symlinkat(tmp, dfd, dirs);
129178355Ssam		}
130178355Ssam		free(tmp);
131178355Ssam	}
132178355Ssam	tmp = dirs;
133178355Ssam	if (fstatat(dfd, dirs, &st, 0) == -1) {
134178355Ssam		while ((tmp = strchr(tmp + 1, '/')) != NULL) {
135178355Ssam			*tmp = '\0';
136178355Ssam			if (fstatat(dfd, dirs, &st, 0) == -1) {
137178355Ssam				if (mkdirat(dfd, dirs, _DEF_DIRMODE) == -1)
138178355Ssam					err(EX_OSFILE,  "'%s' (root home parent) is not a directory", dirs);
139178355Ssam			}
140178355Ssam			*tmp = '/';
141178355Ssam		}
142178355Ssam	}
143178355Ssam	if (fstatat(dfd, dirs, &st, 0) == -1) {
144178355Ssam		if (mkdirat(dfd, dirs, _DEF_DIRMODE) == -1)
145178355Ssam			err(EX_OSFILE,  "'%s' (root home parent) is not a directory", dirs);
146178355Ssam		fchownat(dfd, dirs, 0, 0, 0);
147178355Ssam	}
148178355Ssam
149178355Ssam	free(dirs);
150178355Ssam}
151178355Ssam
152178355Ssamstatic void
153178355Ssamcreate_and_populate_homedir(struct userconf *cnf, struct passwd *pwd,
154178355Ssam    const char *skeldir, mode_t homemode, bool update)
155178355Ssam{
156178355Ssam	int skelfd = -1;
157178355Ssam
158178355Ssam	/* Create home parents directories */
159178355Ssam	mkdir_home_parents(conf.rootfd, pwd->pw_dir);
160178355Ssam
161178355Ssam	if (skeldir != NULL && *skeldir != '\0') {
162178355Ssam		if (*skeldir == '/')
163178355Ssam			skeldir++;
164178355Ssam		skelfd = openat(conf.rootfd, skeldir, O_DIRECTORY|O_CLOEXEC);
165178355Ssam	}
166178355Ssam
167178355Ssam	copymkdir(conf.rootfd, pwd->pw_dir, skelfd, homemode, pwd->pw_uid,
168178355Ssam	    pwd->pw_gid, 0);
169178355Ssam	pw_log(cnf, update ? M_UPDATE : M_ADD, W_USER, "%s(%ju) home %s made",
170178355Ssam	    pwd->pw_name, (uintmax_t)pwd->pw_uid, pwd->pw_dir);
171178355Ssam}
172178355Ssam
173178355Ssamstatic int
174178355Ssampw_set_passwd(struct passwd *pwd, int fd, bool precrypted, bool update)
175178355Ssam{
176178355Ssam	int		 b, istty;
177178355Ssam	struct termios	 t, n;
178178355Ssam	login_cap_t	*lc;
179178355Ssam	char		line[_PASSWORD_LEN+1];
180178355Ssam	char		*p;
181178355Ssam
182178355Ssam	if (fd == '-') {
183178355Ssam		if (!pwd->pw_passwd || *pwd->pw_passwd != '*') {
184178355Ssam			pwd->pw_passwd = "*";	/* No access */
185178355Ssam			return (1);
186178355Ssam		}
187178355Ssam		return (0);
188178355Ssam	}
189178355Ssam
190178355Ssam	if ((istty = isatty(fd))) {
191178355Ssam		if (tcgetattr(fd, &t) == -1)
192178355Ssam			istty = 0;
193187343Ssam		else {
194187343Ssam			n = t;
195187343Ssam			n.c_lflag &= ~(ECHO);
196187343Ssam			tcsetattr(fd, TCSANOW, &n);
197187343Ssam			printf("%s%spassword for user %s:",
198187343Ssam			    update ? "new " : "",
199187343Ssam			    precrypted ? "encrypted " : "",
200187343Ssam			    pwd->pw_name);
201187343Ssam			fflush(stdout);
202187343Ssam		}
203187343Ssam	}
204187343Ssam	b = read(fd, line, sizeof(line) - 1);
205187343Ssam	if (istty) {	/* Restore state */
206187343Ssam		tcsetattr(fd, TCSANOW, &t);
207187343Ssam		fputc('\n', stdout);
208187343Ssam		fflush(stdout);
209187343Ssam	}
210187343Ssam
211187343Ssam	if (b < 0)
212187343Ssam		err(EX_IOERR, "-%c file descriptor",
213187343Ssam		    precrypted ? 'H' : 'h');
214187343Ssam	line[b] = '\0';
215187343Ssam	if ((p = strpbrk(line, "\r\n")) != NULL)
216187343Ssam		*p = '\0';
217187343Ssam	if (!*line)
218187343Ssam		errx(EX_DATAERR, "empty password read on file descriptor %d",
219187343Ssam		    fd);
220187343Ssam	if (precrypted) {
221187343Ssam		if (strchr(line, ':') != NULL)
222187343Ssam			errx(EX_DATAERR, "bad encrypted password");
223187343Ssam		pwd->pw_passwd = strdup(line);
224187343Ssam	} else {
225187343Ssam		lc = login_getpwclass(pwd);
226187343Ssam		if (lc == NULL ||
227187343Ssam				login_setcryptfmt(lc, "sha512", NULL) == NULL)
228187343Ssam			warn("setting crypt(3) format");
229187343Ssam		login_close(lc);
230187343Ssam		pwd->pw_passwd = pw_pwcrypt(line);
231187343Ssam	}
232187343Ssam	return (1);
233187343Ssam}
234187343Ssam
235187343Ssamstatic void
236187343Ssamperform_chgpwent(const char *name, struct passwd *pwd, char *nispasswd)
237187343Ssam{
238187343Ssam	int rc;
239187343Ssam	struct passwd *nispwd;
240187343Ssam
241187343Ssam	/* duplicate for nis so that chgpwent is not modifying before NIS */
242178355Ssam	if (nispasswd && *nispasswd == '/')
243178355Ssam		nispwd = pw_dup(pwd);
244178355Ssam
245178355Ssam	rc = chgpwent(name, pwd);
246178355Ssam	if (rc == -1)
247178355Ssam		errx(EX_IOERR, "user '%s' does not exist (NIS?)", pwd->pw_name);
248178355Ssam	else if (rc != 0)
249178355Ssam		err(EX_IOERR, "passwd file update");
250178355Ssam
251178355Ssam	if (nispasswd && *nispasswd == '/') {
252178355Ssam		rc = chgnispwent(nispasswd, name, nispwd);
253178355Ssam		if (rc == -1)
254178355Ssam			warn("User '%s' not found in NIS passwd", pwd->pw_name);
255178355Ssam		else if (rc != 0)
256178355Ssam			warn("NIS passwd update");
257178355Ssam		/* NOTE: NIS-only update errors are not fatal */
258178355Ssam	}
259178355Ssam}
260178355Ssam
261178355Ssam/*
262178355Ssam * The M_LOCK and M_UNLOCK functions simply add or remove
263178355Ssam * a "*LOCKED*" prefix from in front of the password to
264178355Ssam * prevent it decoding correctly, and therefore prevents
265178355Ssam * access. Of course, this only prevents access via
266178355Ssam * password authentication (not ssh, kerberos or any
267178355Ssam * other method that does not use the UNIX password) but
268178355Ssam * that is a known limitation.
269178355Ssam */
270178355Ssamstatic int
271178355Ssampw_userlock(char *arg1, int mode)
272178355Ssam{
273178355Ssam	struct passwd *pwd = NULL;
274178355Ssam	char *passtmp = NULL;
275178355Ssam	char *name;
276178355Ssam	bool locked = false;
277178355Ssam	uid_t id;
278178355Ssam
279178355Ssam	if (geteuid() != 0)
280178355Ssam		errx(EX_NOPERM, "you must be root");
281178355Ssam
282178355Ssam	if (arg1 == NULL)
283178355Ssam		errx(EX_DATAERR, "username or id required");
284178355Ssam
285178355Ssam	if (arg1[strspn(arg1, "0123456789")] == '\0')
286178355Ssam		id = pw_checkid(arg1, UID_MAX);
287178355Ssam	else
288178355Ssam		name = arg1;
289178355Ssam
290178355Ssam	pwd = (name != NULL) ? GETPWNAM(pw_checkname(name, 0)) : GETPWUID(id);
291178355Ssam	if (pwd == NULL) {
292178355Ssam		if (name == NULL)
293178355Ssam			errx(EX_NOUSER, "no such uid `%ju'", (uintmax_t) id);
294178355Ssam		errx(EX_NOUSER, "no such user `%s'", name);
295178355Ssam	}
296178355Ssam
297178355Ssam	if (name == NULL)
298178355Ssam		name = pwd->pw_name;
299178355Ssam
300178355Ssam	if (strncmp(pwd->pw_passwd, locked_str, sizeof(locked_str) -1) == 0)
301178355Ssam		locked = true;
302178355Ssam	if (mode == M_LOCK && locked)
303178355Ssam		errx(EX_DATAERR, "user '%s' is already locked", pwd->pw_name);
304178355Ssam	if (mode == M_UNLOCK && !locked)
305178355Ssam		errx(EX_DATAERR, "user '%s' is not locked", pwd->pw_name);
306178355Ssam
307178355Ssam	if (mode == M_LOCK) {
308178355Ssam		asprintf(&passtmp, "%s%s", locked_str, pwd->pw_passwd);
309178355Ssam		if (passtmp == NULL)	/* disaster */
310178355Ssam			errx(EX_UNAVAILABLE, "out of memory");
311178355Ssam		pwd->pw_passwd = passtmp;
312178355Ssam	} else {
313178355Ssam		pwd->pw_passwd += sizeof(locked_str)-1;
314178355Ssam	}
315178355Ssam
316178355Ssam	perform_chgpwent(name, pwd, NULL);
317178355Ssam	free(passtmp);
318178355Ssam
319178355Ssam	return (EXIT_SUCCESS);
320178355Ssam}
321178355Ssam
322178355Ssamstatic uid_t
323178355Ssampw_uidpolicy(struct userconf * cnf, intmax_t id)
324178355Ssam{
325178355Ssam	struct passwd  *pwd;
326178355Ssam	struct bitmap   bm;
327178355Ssam	uid_t           uid = (uid_t) - 1;
328178355Ssam
329178355Ssam	/*
330178355Ssam	 * Check the given uid, if any
331178355Ssam	 */
332178355Ssam	if (id >= 0) {
333178355Ssam		uid = (uid_t) id;
334178355Ssam
335178355Ssam		if ((pwd = GETPWUID(uid)) != NULL && conf.checkduplicate)
336178355Ssam			errx(EX_DATAERR, "uid `%ju' has already been allocated",
337178355Ssam			    (uintmax_t)pwd->pw_uid);
338178355Ssam		return (uid);
339178355Ssam	}
340178355Ssam	/*
341178355Ssam	 * We need to allocate the next available uid under one of
342178355Ssam	 * two policies a) Grab the first unused uid b) Grab the
343178355Ssam	 * highest possible unused uid
344178355Ssam	 */
345178355Ssam	if (cnf->min_uid >= cnf->max_uid) {	/* Sanity
346178355Ssam						 * claus^H^H^H^Hheck */
347178355Ssam		cnf->min_uid = 1000;
348178355Ssam		cnf->max_uid = 32000;
349178355Ssam	}
350178355Ssam	bm = bm_alloc(cnf->max_uid - cnf->min_uid + 1);
351178355Ssam
352178355Ssam	/*
353178355Ssam	 * Now, let's fill the bitmap from the password file
354178355Ssam	 */
355178355Ssam	SETPWENT();
356178355Ssam	while ((pwd = GETPWENT()) != NULL)
357178355Ssam		if (pwd->pw_uid >= (uid_t) cnf->min_uid && pwd->pw_uid <= (uid_t) cnf->max_uid)
358178355Ssam			bm_setbit(&bm, pwd->pw_uid - cnf->min_uid);
359178355Ssam	ENDPWENT();
360178355Ssam
361178355Ssam	/*
362178355Ssam	 * Then apply the policy, with fallback to reuse if necessary
363178355Ssam	 */
364178355Ssam	if (cnf->reuse_uids || (uid = (uid_t) (bm_lastset(&bm) + cnf->min_uid + 1)) > cnf->max_uid)
365178355Ssam		uid = (uid_t) (bm_firstunset(&bm) + cnf->min_uid);
366178355Ssam
367178355Ssam	/*
368178355Ssam	 * Another sanity check
369178355Ssam	 */
370178355Ssam	if (uid < cnf->min_uid || uid > cnf->max_uid)
371178355Ssam		errx(EX_SOFTWARE, "unable to allocate a new uid - range fully used");
372178355Ssam	bm_dealloc(&bm);
373178355Ssam	return (uid);
374178355Ssam}
375178355Ssam
376178355Ssamstatic uid_t
377178355Ssampw_gidpolicy(struct userconf *cnf, char *grname, char *nam, gid_t prefer, bool dryrun)
378178355Ssam{
379178355Ssam	struct group   *grp;
380178355Ssam	gid_t           gid = (uid_t) - 1;
381178355Ssam
382178355Ssam	/*
383178355Ssam	 * Check the given gid, if any
384178355Ssam	 */
385178355Ssam	SETGRENT();
386178355Ssam	if (grname) {
387178355Ssam		if ((grp = GETGRNAM(grname)) == NULL) {
388178355Ssam			gid = pw_checkid(grname, GID_MAX);
389178355Ssam			grp = GETGRGID(gid);
390178355Ssam		}
391178355Ssam		gid = grp->gr_gid;
392178355Ssam	} else if ((grp = GETGRNAM(nam)) != NULL &&
393178355Ssam	    (grp->gr_mem == NULL || grp->gr_mem[0] == NULL)) {
394178355Ssam		gid = grp->gr_gid;  /* Already created? Use it anyway... */
395178355Ssam	} else {
396178355Ssam		intmax_t		grid = -1;
397178355Ssam
398178355Ssam		/*
399178355Ssam		 * We need to auto-create a group with the user's name. We
400178355Ssam		 * can send all the appropriate output to our sister routine
401178355Ssam		 * bit first see if we can create a group with gid==uid so we
402178355Ssam		 * can keep the user and group ids in sync. We purposely do
403178355Ssam		 * NOT check the gid range if we can force the sync. If the
404178355Ssam		 * user's name dups an existing group, then the group add
405178355Ssam		 * function will happily handle that case for us and exit.
406178355Ssam		 */
407178355Ssam		if (GETGRGID(prefer) == NULL)
408178355Ssam			grid = prefer;
409178355Ssam		if (dryrun) {
410178355Ssam			gid = pw_groupnext(cnf, true);
411178355Ssam		} else {
412178355Ssam			if (grid == -1)
413178355Ssam				grid =  pw_groupnext(cnf, true);
414178355Ssam			groupadd(cnf, nam, grid, NULL, -1, false, false, false);
415178355Ssam			if ((grp = GETGRNAM(nam)) != NULL)
416178355Ssam				gid = grp->gr_gid;
417178355Ssam		}
418178355Ssam	}
419178355Ssam	ENDGRENT();
420178355Ssam	return (gid);
421178355Ssam}
422178355Ssam
423178355Ssamstatic char *
424178355Ssampw_homepolicy(struct userconf * cnf, char *homedir, const char *user)
425178355Ssam{
426178355Ssam	static char     home[128];
427178355Ssam
428178355Ssam	if (homedir)
429178355Ssam		return (homedir);
430178355Ssam
431178355Ssam	if (cnf->home == NULL || *cnf->home == '\0')
432178355Ssam		errx(EX_CONFIG, "no base home directory set");
433178355Ssam	snprintf(home, sizeof(home), "%s/%s", cnf->home, user);
434178355Ssam
435178355Ssam	return (home);
436178355Ssam}
437178355Ssam
438178355Ssamstatic char *
439178355Ssamshell_path(char const * path, char *shells[], char *sh)
440178355Ssam{
441178355Ssam	if (sh != NULL && (*sh == '/' || *sh == '\0'))
442178355Ssam		return sh;	/* specified full path or forced none */
443178355Ssam	else {
444178355Ssam		char           *p;
445178355Ssam		char            paths[_UC_MAXLINE];
446178355Ssam
447178355Ssam		/*
448178355Ssam		 * We need to search paths
449178355Ssam		 */
450178355Ssam		strlcpy(paths, path, sizeof(paths));
451178355Ssam		for (p = strtok(paths, ": \t\r\n"); p != NULL; p = strtok(NULL, ": \t\r\n")) {
452178355Ssam			int             i;
453178355Ssam			static char     shellpath[256];
454178355Ssam
455178355Ssam			if (sh != NULL) {
456178355Ssam				snprintf(shellpath, sizeof(shellpath), "%s/%s", p, sh);
457178355Ssam				if (access(shellpath, X_OK) == 0)
458178355Ssam					return shellpath;
459178355Ssam			} else
460178355Ssam				for (i = 0; i < _UC_MAXSHELLS && shells[i] != NULL; i++) {
461178355Ssam					snprintf(shellpath, sizeof(shellpath), "%s/%s", p, shells[i]);
462178355Ssam					if (access(shellpath, X_OK) == 0)
463178355Ssam						return shellpath;
464178355Ssam				}
465178355Ssam		}
466178355Ssam		if (sh == NULL)
467178355Ssam			errx(EX_OSFILE, "can't find shell `%s' in shell paths", sh);
468178355Ssam		errx(EX_CONFIG, "no default shell available or defined");
469178355Ssam		return NULL;
470178355Ssam	}
471178355Ssam}
472178355Ssam
473178355Ssamstatic char *
474178355Ssampw_shellpolicy(struct userconf * cnf)
475178355Ssam{
476178355Ssam
477178355Ssam	return shell_path(cnf->shelldir, cnf->shells, cnf->shell_default);
478178355Ssam}
479178355Ssam
480178355Ssam#define	SALTSIZE	32
481178355Ssam
482178355Ssamstatic char const chars[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ./";
483178355Ssam
484178355Ssamchar *
485178355Ssampw_pwcrypt(char *password)
486178355Ssam{
487178355Ssam	int             i;
488178355Ssam	char            salt[SALTSIZE + 1];
489178355Ssam	char		*cryptpw;
490178355Ssam	static char     buf[256];
491178355Ssam
492178355Ssam	/*
493178355Ssam	 * Calculate a salt value
494178355Ssam	 */
495178355Ssam	for (i = 0; i < SALTSIZE; i++)
496178355Ssam		salt[i] = chars[arc4random_uniform(sizeof(chars) - 1)];
497178355Ssam	salt[SALTSIZE] = '\0';
498178355Ssam
499178355Ssam	cryptpw = crypt(password, salt);
500178355Ssam	if (cryptpw == NULL)
501178355Ssam		errx(EX_CONFIG, "crypt(3) failure");
502178355Ssam	return strcpy(buf, cryptpw);
503178355Ssam}
504178355Ssam
505178355Ssamstatic char *
506178355Ssampw_password(struct userconf * cnf, char const * user, bool dryrun)
507178355Ssam{
508178355Ssam	int             i, l;
509178355Ssam	char            pwbuf[32];
510178355Ssam
511178355Ssam	switch (cnf->default_password) {
512178355Ssam	case -1:		/* Random password */
513178355Ssam		l = (arc4random() % 8 + 8);	/* 8 - 16 chars */
514178355Ssam		for (i = 0; i < l; i++)
515178355Ssam			pwbuf[i] = chars[arc4random_uniform(sizeof(chars)-1)];
516178355Ssam		pwbuf[i] = '\0';
517178355Ssam
518178355Ssam		/*
519178355Ssam		 * We give this information back to the user
520178355Ssam		 */
521178355Ssam		if (conf.fd == -1 && !dryrun) {
522178355Ssam			if (isatty(STDOUT_FILENO))
523178355Ssam				printf("Password for '%s' is: ", user);
524178355Ssam			printf("%s\n", pwbuf);
525178355Ssam			fflush(stdout);
526178355Ssam		}
527178355Ssam		break;
528178355Ssam
529178355Ssam	case -2:		/* No password at all! */
530178355Ssam		return "";
531178355Ssam
532178355Ssam	case 0:		/* No login - default */
533178355Ssam	default:
534178355Ssam		return "*";
535178355Ssam
536178355Ssam	case 1:		/* user's name */
537178355Ssam		strlcpy(pwbuf, user, sizeof(pwbuf));
538178355Ssam		break;
539178355Ssam	}
540178355Ssam	return pw_pwcrypt(pwbuf);
541178355Ssam}
542178355Ssam
543178355Ssamstatic int
544178355Ssamprint_user(struct passwd * pwd, bool pretty, bool v7)
545178355Ssam{
546178355Ssam	int		j;
547178355Ssam	char           *p;
548178355Ssam	struct group   *grp = GETGRGID(pwd->pw_gid);
549178355Ssam	char            uname[60] = "User &", office[60] = "[None]",
550178355Ssam			wphone[60] = "[None]", hphone[60] = "[None]";
551178355Ssam	char		acexpire[32] = "[None]", pwexpire[32] = "[None]";
552178355Ssam	struct tm *    tptr;
553178355Ssam
554178355Ssam	if (!pretty) {
555178355Ssam		p = v7 ? pw_make_v7(pwd) : pw_make(pwd);
556178355Ssam		printf("%s\n", p);
557178355Ssam		free(p);
558178355Ssam		return (EXIT_SUCCESS);
559178355Ssam	}
560178355Ssam
561178355Ssam	if ((p = strtok(pwd->pw_gecos, ",")) != NULL) {
562178355Ssam		strlcpy(uname, p, sizeof(uname));
563178355Ssam		if ((p = strtok(NULL, ",")) != NULL) {
564178355Ssam			strlcpy(office, p, sizeof(office));
565178355Ssam			if ((p = strtok(NULL, ",")) != NULL) {
566178355Ssam				strlcpy(wphone, p, sizeof(wphone));
567178355Ssam				if ((p = strtok(NULL, "")) != NULL) {
568178355Ssam					strlcpy(hphone, p, sizeof(hphone));
569178355Ssam				}
570178355Ssam			}
571178355Ssam		}
572178355Ssam	}
573178355Ssam	/*
574178355Ssam	 * Handle '&' in gecos field
575178355Ssam	 */
576178355Ssam	if ((p = strchr(uname, '&')) != NULL) {
577178355Ssam		int             l = strlen(pwd->pw_name);
578178355Ssam		int             m = strlen(p);
579178355Ssam
580178355Ssam		memmove(p + l, p + 1, m);
581178355Ssam		memmove(p, pwd->pw_name, l);
582178355Ssam		*p = (char) toupper((unsigned char)*p);
583178355Ssam	}
584178355Ssam	if (pwd->pw_expire > (time_t)0 && (tptr = localtime(&pwd->pw_expire)) != NULL)
585178355Ssam		strftime(acexpire, sizeof acexpire, "%c", tptr);
586178355Ssam		if (pwd->pw_change > (time_t)0 && (tptr = localtime(&pwd->pw_change)) != NULL)
587178355Ssam		strftime(pwexpire, sizeof pwexpire, "%c", tptr);
588178355Ssam	printf("Login Name: %-15s   #%-12ju Group: %-15s   #%ju\n"
589178355Ssam	       " Full Name: %s\n"
590178355Ssam	       "      Home: %-26.26s      Class: %s\n"
591178355Ssam	       "     Shell: %-26.26s     Office: %s\n"
592178355Ssam	       "Work Phone: %-26.26s Home Phone: %s\n"
593178355Ssam	       "Acc Expire: %-26.26s Pwd Expire: %s\n",
594178355Ssam	       pwd->pw_name, (uintmax_t)pwd->pw_uid,
595178355Ssam	       grp ? grp->gr_name : "(invalid)", (uintmax_t)pwd->pw_gid,
596178355Ssam	       uname, pwd->pw_dir, pwd->pw_class,
597178355Ssam	       pwd->pw_shell, office, wphone, hphone,
598178355Ssam	       acexpire, pwexpire);
599178355Ssam        SETGRENT();
600178355Ssam	j = 0;
601178355Ssam	while ((grp=GETGRENT()) != NULL) {
602178355Ssam		int     i = 0;
603178355Ssam		if (grp->gr_mem != NULL) {
604178355Ssam			while (grp->gr_mem[i] != NULL) {
605178355Ssam				if (strcmp(grp->gr_mem[i], pwd->pw_name)==0) {
606178355Ssam					printf(j++ == 0 ? "    Groups: %s" : ",%s", grp->gr_name);
607178355Ssam					break;
608178355Ssam				}
609178355Ssam				++i;
610178355Ssam			}
611178355Ssam		}
612178355Ssam	}
613178355Ssam	ENDGRENT();
614178355Ssam	printf("%s", j ? "\n" : "");
615178355Ssam	return (EXIT_SUCCESS);
616178355Ssam}
617178355Ssam
618178355Ssamchar *
619178355Ssampw_checkname(char *name, int gecos)
620178355Ssam{
621178355Ssam	char showch[8];
622178355Ssam	const char *badchars, *ch, *showtype;
623178355Ssam	int reject;
624178355Ssam
625178355Ssam	ch = name;
626178355Ssam	reject = 0;
627178355Ssam	if (gecos) {
628178355Ssam		/* See if the name is valid as a gecos (comment) field. */
629178355Ssam		badchars = ":!@";
630178355Ssam		showtype = "gecos field";
631178355Ssam	} else {
632178355Ssam		/* See if the name is valid as a userid or group. */
633178355Ssam		badchars = " ,\t:+&#%$^()!@~*?<>=|\\/\"";
634178355Ssam		showtype = "userid/group name";
635178355Ssam		/* Userids and groups can not have a leading '-'. */
636178355Ssam		if (*ch == '-')
637178355Ssam			reject = 1;
638178355Ssam	}
639178355Ssam	if (!reject) {
640178355Ssam		while (*ch) {
641178355Ssam			if (strchr(badchars, *ch) != NULL || *ch < ' ' ||
642178355Ssam			    *ch == 127) {
643178355Ssam				reject = 1;
644178355Ssam				break;
645178355Ssam			}
646178355Ssam			/* 8-bit characters are only allowed in GECOS fields */
647178355Ssam			if (!gecos && (*ch & 0x80)) {
648178355Ssam				reject = 1;
649178355Ssam				break;
650178355Ssam			}
651178355Ssam			ch++;
652178355Ssam		}
653178355Ssam	}
654178355Ssam	/*
655178355Ssam	 * A `$' is allowed as the final character for userids and groups,
656178355Ssam	 * mainly for the benefit of samba.
657178355Ssam	 */
658178355Ssam	if (reject && !gecos) {
659178355Ssam		if (*ch == '$' && *(ch + 1) == '\0') {
660178355Ssam			reject = 0;
661178355Ssam			ch++;
662178355Ssam		}
663178355Ssam	}
664178355Ssam	if (reject) {
665178355Ssam		snprintf(showch, sizeof(showch), (*ch >= ' ' && *ch < 127)
666178355Ssam		    ? "`%c'" : "0x%02x", *ch);
667178355Ssam		errx(EX_DATAERR, "invalid character %s at position %td in %s",
668178355Ssam		    showch, (ch - name), showtype);
669178355Ssam	}
670178355Ssam	if (!gecos && (ch - name) > LOGNAMESIZE)
671178355Ssam		errx(EX_USAGE, "name too long `%s' (max is %d)", name,
672178355Ssam		    LOGNAMESIZE);
673178355Ssam
674178355Ssam	return (name);
675178355Ssam}
676178355Ssam
677178355Ssamstatic void
678178355Ssamrmat(uid_t uid)
679178355Ssam{
680178355Ssam	DIR            *d = opendir("/var/at/jobs");
681178355Ssam
682178355Ssam	if (d != NULL) {
683178355Ssam		struct dirent  *e;
684178355Ssam
685178355Ssam		while ((e = readdir(d)) != NULL) {
686178355Ssam			struct stat     st;
687178355Ssam
688178355Ssam			if (strncmp(e->d_name, ".lock", 5) != 0 &&
689178355Ssam			    stat(e->d_name, &st) == 0 &&
690178355Ssam			    !S_ISDIR(st.st_mode) &&
691178355Ssam			    st.st_uid == uid) {
692178355Ssam				char            tmp[MAXPATHLEN];
693178355Ssam
694178355Ssam				snprintf(tmp, sizeof(tmp), "/usr/bin/atrm %s",
695178355Ssam				    e->d_name);
696178355Ssam				system(tmp);
697178355Ssam			}
698178355Ssam		}
699178355Ssam		closedir(d);
700178355Ssam	}
701178355Ssam}
702178355Ssam
703178355Ssamstatic void
704178355Ssamrmopie(char const * name)
705178355Ssam{
706178355Ssam	char tmp[1014];
707178355Ssam	FILE *fp;
708178355Ssam	int fd;
709178355Ssam	size_t len;
710178355Ssam	off_t	atofs = 0;
711178355Ssam
712178355Ssam	if ((fd = openat(conf.rootfd, "etc/opiekeys", O_RDWR)) == -1)
713178355Ssam		return;
714178355Ssam
715178355Ssam	fp = fdopen(fd, "r+");
716178355Ssam	len = strlen(name);
717178355Ssam
718178355Ssam	while (fgets(tmp, sizeof(tmp), fp) != NULL) {
719178355Ssam		if (strncmp(name, tmp, len) == 0 && tmp[len]==' ') {
720178355Ssam			/* Comment username out */
721178355Ssam			if (fseek(fp, atofs, SEEK_SET) == 0)
722178355Ssam				fwrite("#", 1, 1, fp);
723178355Ssam			break;
724178355Ssam		}
725178355Ssam		atofs = ftell(fp);
726178355Ssam	}
727178355Ssam	/*
728178355Ssam	 * If we got an error of any sort, don't update!
729178355Ssam	 */
730178355Ssam	fclose(fp);
731178355Ssam}
732178355Ssam
733178355Ssamint
734178355Ssampw_user_next(int argc, char **argv, char *name __unused)
735178355Ssam{
736178355Ssam	struct userconf *cnf = NULL;
737178355Ssam	const char *cfg = NULL;
738178355Ssam	int ch;
739178355Ssam	bool quiet = false;
740178355Ssam	uid_t next;
741178355Ssam
742178355Ssam	while ((ch = getopt(argc, argv, "Cq")) != -1) {
743178355Ssam		switch (ch) {
744178355Ssam		case 'C':
745178355Ssam			cfg = optarg;
746178355Ssam			break;
747178355Ssam		case 'q':
748178355Ssam			quiet = true;
749178355Ssam			break;
750178355Ssam		}
751178355Ssam	}
752178355Ssam
753178355Ssam	if (quiet)
754178355Ssam		freopen(_PATH_DEVNULL, "w", stderr);
755178355Ssam
756178355Ssam	cnf = get_userconfig(cfg);
757178355Ssam
758178355Ssam	next = pw_uidpolicy(cnf, -1);
759178355Ssam
760178355Ssam	printf("%ju:", (uintmax_t)next);
761178355Ssam	pw_groupnext(cnf, quiet);
762178355Ssam
763178355Ssam	return (EXIT_SUCCESS);
764178355Ssam}
765178355Ssam
766178355Ssamint
767178355Ssampw_user_show(int argc, char **argv, char *arg1)
768178355Ssam{
769178355Ssam	struct passwd *pwd = NULL;
770178355Ssam	char *name = NULL;
771178355Ssam	intmax_t id = -1;
772178355Ssam	int ch;
773178355Ssam	bool all = false;
774178355Ssam	bool pretty = false;
775178355Ssam	bool force = false;
776178355Ssam	bool v7 = false;
777178355Ssam	bool quiet = false;
778178355Ssam
779178355Ssam	if (arg1 != NULL) {
780178355Ssam		if (arg1[strspn(arg1, "0123456789")] == '\0')
781178355Ssam			id = pw_checkid(arg1, UID_MAX);
782178355Ssam		else
783178355Ssam			name = arg1;
784178355Ssam	}
785178355Ssam
786178355Ssam	while ((ch = getopt(argc, argv, "C:qn:u:FPa7")) != -1) {
787178355Ssam		switch (ch) {
788178355Ssam		case 'C':
789178355Ssam			/* ignore compatibility */
790178355Ssam			break;
791178355Ssam		case 'q':
792178355Ssam			quiet = true;
793178355Ssam			break;
794178355Ssam		case 'n':
795178355Ssam			name = optarg;
796178355Ssam			break;
797178355Ssam		case 'u':
798178355Ssam			id = pw_checkid(optarg, UID_MAX);
799178355Ssam			break;
800178355Ssam		case 'F':
801178355Ssam			force = true;
802178355Ssam			break;
803178355Ssam		case 'P':
804178355Ssam			pretty = true;
805178355Ssam			break;
806178355Ssam		case 'a':
807178355Ssam			all = true;
808178355Ssam			break;
809178355Ssam		case '7':
810178355Ssam			v7 = true;
811178355Ssam			break;
812178355Ssam		}
813178355Ssam	}
814178355Ssam
815178355Ssam	if (quiet)
816178355Ssam		freopen(_PATH_DEVNULL, "w", stderr);
817178355Ssam
818178355Ssam	if (all) {
819178355Ssam		SETPWENT();
820178355Ssam		while ((pwd = GETPWENT()) != NULL)
821178355Ssam			print_user(pwd, pretty, v7);
822178355Ssam		ENDPWENT();
823178355Ssam		return (EXIT_SUCCESS);
824178355Ssam	}
825178355Ssam
826178355Ssam	if (id < 0 && name == NULL)
827178355Ssam		errx(EX_DATAERR, "username or id required");
828178355Ssam
829178355Ssam	pwd = (name != NULL) ? GETPWNAM(pw_checkname(name, 0)) : GETPWUID(id);
830178355Ssam	if (pwd == NULL) {
831178355Ssam		if (force) {
832178355Ssam			pwd = &fakeuser;
833178355Ssam		} else {
834178355Ssam			if (name == NULL)
835178355Ssam				errx(EX_NOUSER, "no such uid `%ju'",
836178355Ssam				    (uintmax_t) id);
837178355Ssam			errx(EX_NOUSER, "no such user `%s'", name);
838178355Ssam		}
839178355Ssam	}
840178355Ssam
841178355Ssam	return (print_user(pwd, pretty, v7));
842178355Ssam}
843178355Ssam
844178355Ssamint
845178355Ssampw_user_del(int argc, char **argv, char *arg1)
846178355Ssam{
847178355Ssam	struct userconf *cnf = NULL;
848178355Ssam	struct passwd *pwd = NULL;
849178355Ssam	struct group *gr, *grp;
850178355Ssam	char *name = NULL;
851178355Ssam	char grname[MAXLOGNAME];
852178355Ssam	char *nispasswd = NULL;
853178355Ssam	char file[MAXPATHLEN];
854178355Ssam	char home[MAXPATHLEN];
855178355Ssam	const char *cfg = NULL;
856178355Ssam	struct stat st;
857178355Ssam	intmax_t id = -1;
858178355Ssam	int ch, rc;
859178355Ssam	bool nis = false;
860178355Ssam	bool deletehome = false;
861178355Ssam	bool quiet = false;
862178355Ssam
863178355Ssam	if (arg1 != NULL) {
864178355Ssam		if (arg1[strspn(arg1, "0123456789")] == '\0')
865178355Ssam			id = pw_checkid(arg1, UID_MAX);
866178355Ssam		else
867178355Ssam			name = arg1;
868178355Ssam	}
869178355Ssam
870178355Ssam	while ((ch = getopt(argc, argv, "C:qn:u:rYy:")) != -1) {
871178355Ssam		switch (ch) {
872178355Ssam		case 'C':
873178355Ssam			cfg = optarg;
874178355Ssam			break;
875178355Ssam		case 'q':
876178355Ssam			quiet = true;
877178355Ssam			break;
878178355Ssam		case 'n':
879178355Ssam			name = optarg;
880178355Ssam			break;
881178355Ssam		case 'u':
882178355Ssam			id = pw_checkid(optarg, UID_MAX);
883178355Ssam			break;
884178355Ssam		case 'r':
885178355Ssam			deletehome = true;
886178355Ssam			break;
887178355Ssam		case 'y':
888178355Ssam			nispasswd = optarg;
889178355Ssam			break;
890178355Ssam		case 'Y':
891178355Ssam			nis = true;
892178355Ssam			break;
893178355Ssam		}
894178355Ssam	}
895178355Ssam
896178355Ssam	if (quiet)
897178355Ssam		freopen(_PATH_DEVNULL, "w", stderr);
898178355Ssam
899178355Ssam	if (id < 0 && name == NULL)
900178355Ssam		errx(EX_DATAERR, "username or id required");
901178355Ssam
902178355Ssam	cnf = get_userconfig(cfg);
903178355Ssam
904178355Ssam	if (nispasswd == NULL)
905178355Ssam		nispasswd = cnf->nispasswd;
906178355Ssam
907178355Ssam	pwd = (name != NULL) ? GETPWNAM(pw_checkname(name, 0)) : GETPWUID(id);
908178355Ssam	if (pwd == NULL) {
909178355Ssam		if (name == NULL)
910178355Ssam			errx(EX_NOUSER, "no such uid `%ju'", (uintmax_t) id);
911178355Ssam		errx(EX_NOUSER, "no such user `%s'", name);
912178355Ssam	}
913178355Ssam
914178355Ssam	if (PWF._altdir == PWF_REGULAR &&
915178355Ssam	    ((pwd->pw_fields & _PWF_SOURCE) != _PWF_FILES)) {
916178355Ssam		if ((pwd->pw_fields & _PWF_SOURCE) == _PWF_NIS) {
917178355Ssam			if (!nis && nispasswd && *nispasswd != '/')
918178355Ssam				errx(EX_NOUSER, "Cannot remove NIS user `%s'",
919178355Ssam				    name);
920178355Ssam		} else {
921178355Ssam			errx(EX_NOUSER, "Cannot remove non local user `%s'",
922178355Ssam			    name);
923178355Ssam		}
924178355Ssam	}
925178355Ssam
926178355Ssam	id = pwd->pw_uid;
927178355Ssam	if (name == NULL)
928178355Ssam		name = pwd->pw_name;
929178355Ssam
930178355Ssam	if (strcmp(pwd->pw_name, "root") == 0)
931178355Ssam		errx(EX_DATAERR, "cannot remove user 'root'");
932178355Ssam
933178355Ssam	/* Remove opie record from /etc/opiekeys */
934178355Ssam	if (PWALTDIR() != PWF_ALT)
935178355Ssam		rmopie(pwd->pw_name);
936178355Ssam
937178355Ssam	if (!PWALTDIR()) {
938178355Ssam		/* Remove crontabs */
939178355Ssam		snprintf(file, sizeof(file), "/var/cron/tabs/%s", pwd->pw_name);
940178355Ssam		if (access(file, F_OK) == 0) {
941178355Ssam			snprintf(file, sizeof(file), "crontab -u %s -r",
942178355Ssam			    pwd->pw_name);
943178355Ssam			system(file);
944178355Ssam		}
945178355Ssam	}
946178355Ssam
947178355Ssam	/*
948178355Ssam	 * Save these for later, since contents of pwd may be
949178355Ssam	 * invalidated by deletion
950178355Ssam	 */
951178355Ssam	snprintf(file, sizeof(file), "%s/%s", _PATH_MAILDIR, pwd->pw_name);
952178355Ssam	strlcpy(home, pwd->pw_dir, sizeof(home));
953178355Ssam	gr = GETGRGID(pwd->pw_gid);
954178355Ssam	if (gr != NULL)
955178355Ssam		strlcpy(grname, gr->gr_name, LOGNAMESIZE);
956178355Ssam	else
957178355Ssam		grname[0] = '\0';
958178355Ssam
959178355Ssam	rc = delpwent(pwd);
960178355Ssam	if (rc == -1)
961178355Ssam		err(EX_IOERR, "user '%s' does not exist", pwd->pw_name);
962178355Ssam	else if (rc != 0)
963178355Ssam		err(EX_IOERR, "passwd update");
964178355Ssam
965178355Ssam	if (nis && nispasswd && *nispasswd=='/') {
966178355Ssam		rc = delnispwent(nispasswd, name);
967178355Ssam		if (rc == -1)
968178355Ssam			warnx("WARNING: user '%s' does not exist in NIS passwd",
969178355Ssam			    pwd->pw_name);
970178355Ssam		else if (rc != 0)
971178355Ssam			warn("WARNING: NIS passwd update");
972178355Ssam	}
973178355Ssam
974178355Ssam	grp = GETGRNAM(name);
975178355Ssam	if (grp != NULL &&
976178355Ssam	    (grp->gr_mem == NULL || *grp->gr_mem == NULL) &&
977178355Ssam	    strcmp(name, grname) == 0)
978178355Ssam		delgrent(GETGRNAM(name));
979178355Ssam	SETGRENT();
980178355Ssam	while ((grp = GETGRENT()) != NULL) {
981178355Ssam		int i, j;
982178355Ssam		char group[MAXLOGNAME];
983178355Ssam		if (grp->gr_mem == NULL)
984178355Ssam			continue;
985178355Ssam
986178355Ssam		for (i = 0; grp->gr_mem[i] != NULL; i++) {
987178355Ssam			if (strcmp(grp->gr_mem[i], name) != 0)
988178355Ssam				continue;
989178355Ssam
990178355Ssam			for (j = i; grp->gr_mem[j] != NULL; j++)
991178355Ssam				grp->gr_mem[j] = grp->gr_mem[j+1];
992178355Ssam			strlcpy(group, grp->gr_name, MAXLOGNAME);
993178355Ssam			chggrent(group, grp);
994178355Ssam		}
995178355Ssam	}
996178355Ssam	ENDGRENT();
997178355Ssam
998178355Ssam	pw_log(cnf, M_DELETE, W_USER, "%s(%ju) account removed", name,
999178355Ssam	    (uintmax_t)id);
1000178355Ssam
1001178355Ssam	/* Remove mail file */
1002178355Ssam	if (PWALTDIR() != PWF_ALT)
1003178355Ssam		unlinkat(conf.rootfd, file + 1, 0);
1004178355Ssam
1005178355Ssam	/* Remove at jobs */
1006178355Ssam	if (!PWALTDIR() && getpwuid(id) == NULL)
1007178355Ssam		rmat(id);
1008178355Ssam
1009178355Ssam	/* Remove home directory and contents */
1010178355Ssam	if (PWALTDIR() != PWF_ALT && deletehome && *home == '/' &&
1011178355Ssam	    GETPWUID(id) == NULL &&
1012178355Ssam	    fstatat(conf.rootfd, home + 1, &st, 0) != -1) {
1013178355Ssam		rm_r(conf.rootfd, home, id);
1014178355Ssam		pw_log(cnf, M_DELETE, W_USER, "%s(%ju) home '%s' %s"
1015178355Ssam		    "removed", name, (uintmax_t)id, home,
1016178355Ssam		     fstatat(conf.rootfd, home + 1, &st, 0) == -1 ? "" : "not "
1017178355Ssam		     "completely ");
1018178355Ssam	}
1019178355Ssam
1020178355Ssam	return (EXIT_SUCCESS);
1021178355Ssam}
1022178355Ssam
1023178355Ssamint
1024178355Ssampw_user_lock(int argc, char **argv, char *arg1)
1025178355Ssam{
1026178355Ssam	int ch;
1027178355Ssam
1028178355Ssam	while ((ch = getopt(argc, argv, "Cq")) != -1) {
1029178355Ssam		switch (ch) {
1030178355Ssam		case 'C':
1031178355Ssam		case 'q':
1032178355Ssam			/* compatibility */
1033178355Ssam			break;
1034178355Ssam		}
1035178355Ssam	}
1036178355Ssam
1037178355Ssam	return (pw_userlock(arg1, M_LOCK));
1038178355Ssam}
1039178355Ssam
1040178355Ssamint
1041178355Ssampw_user_unlock(int argc, char **argv, char *arg1)
1042178355Ssam{
1043178355Ssam	int ch;
1044178355Ssam
1045178355Ssam	while ((ch = getopt(argc, argv, "Cq")) != -1) {
1046178355Ssam		switch (ch) {
1047178355Ssam		case 'C':
1048178355Ssam		case 'q':
1049178355Ssam			/* compatibility */
1050178355Ssam			break;
1051178355Ssam		}
1052178355Ssam	}
1053178355Ssam
1054178355Ssam	return (pw_userlock(arg1, M_UNLOCK));
1055178355Ssam}
1056178355Ssam
1057178355Ssamstatic struct group *
1058178355Ssamgroup_from_name_or_id(char *name)
1059178355Ssam{
1060178355Ssam	const char *errstr = NULL;
1061178355Ssam	struct group *grp;
1062178355Ssam	uintmax_t id;
1063178355Ssam
1064178355Ssam	if ((grp = GETGRNAM(name)) == NULL) {
1065178355Ssam		id = strtounum(name, 0, GID_MAX, &errstr);
1066178355Ssam		if (errstr)
1067178355Ssam			errx(EX_NOUSER, "group `%s' does not exist", name);
1068178355Ssam		grp = GETGRGID(id);
1069178355Ssam		if (grp == NULL)
1070178355Ssam			errx(EX_NOUSER, "group `%s' does not exist", name);
1071178355Ssam	}
1072178355Ssam
1073178355Ssam	return (grp);
1074178355Ssam}
1075178355Ssam
1076178355Ssamstatic void
1077178355Ssamsplit_groups(StringList **groups, char *groupsstr)
1078178355Ssam{
1079178355Ssam	struct group *grp;
1080178355Ssam	char *p;
1081178355Ssam	char tok[] = ", \t";
1082178355Ssam
1083178355Ssam	for (p = strtok(groupsstr, tok); p != NULL; p = strtok(NULL, tok)) {
1084178355Ssam		grp = group_from_name_or_id(p);
1085178355Ssam		if (*groups == NULL)
1086178355Ssam			*groups = sl_init();
1087178355Ssam		sl_add(*groups, newstr(grp->gr_name));
1088178355Ssam	}
1089178355Ssam}
1090178355Ssam
1091178355Ssamstatic void
1092178355Ssamvalidate_grname(struct userconf *cnf, char *group)
1093178355Ssam{
1094178355Ssam	struct group *grp;
1095178355Ssam
1096178355Ssam	if (group == NULL || *group == '\0') {
1097178355Ssam		cnf->default_group = "";
1098178355Ssam		return;
1099178355Ssam	}
1100178355Ssam	grp = group_from_name_or_id(group);
1101178355Ssam	cnf->default_group = newstr(grp->gr_name);
1102178355Ssam}
1103178355Ssam
1104178355Ssamstatic mode_t
1105178355Ssamvalidate_mode(char *mode)
1106178355Ssam{
1107178355Ssam	mode_t m;
1108178355Ssam	void *set;
1109178355Ssam
1110178355Ssam	if ((set = setmode(mode)) == NULL)
1111178355Ssam		errx(EX_DATAERR, "invalid directory creation mode '%s'", mode);
1112178355Ssam
1113178355Ssam	m = getmode(set, _DEF_DIRMODE);
1114178355Ssam	free(set);
1115178355Ssam	return (m);
1116178355Ssam}
1117178355Ssam
1118178355Ssamstatic void
1119178355Ssammix_config(struct userconf *cmdcnf, struct userconf *cfg)
1120178355Ssam{
1121178355Ssam
1122178355Ssam	if (cmdcnf->default_password == 0)
1123178355Ssam		cmdcnf->default_password = cfg->default_password;
1124178355Ssam	if (cmdcnf->reuse_uids == 0)
1125178355Ssam		cmdcnf->reuse_uids = cfg->reuse_uids;
1126178355Ssam	if (cmdcnf->reuse_gids == 0)
1127178355Ssam		cmdcnf->reuse_gids = cfg->reuse_gids;
1128178355Ssam	if (cmdcnf->nispasswd == NULL)
1129178355Ssam		cmdcnf->nispasswd = cfg->nispasswd;
1130178355Ssam	if (cmdcnf->dotdir == NULL)
1131178355Ssam		cmdcnf->dotdir = cfg->dotdir;
1132186106Ssam	if (cmdcnf->newmail == NULL)
1133186106Ssam		cmdcnf->newmail = cfg->newmail;
1134186106Ssam	if (cmdcnf->logfile == NULL)
1135186106Ssam		cmdcnf->logfile = cfg->logfile;
1136186106Ssam	if (cmdcnf->home == NULL)
1137186106Ssam		cmdcnf->home = cfg->home;
1138186106Ssam	if (cmdcnf->homemode == 0)
1139186106Ssam		cmdcnf->homemode = cfg->homemode;
1140186106Ssam	if (cmdcnf->shelldir == NULL)
1141186106Ssam		cmdcnf->shelldir = cfg->shelldir;
1142186106Ssam	if (cmdcnf->shells == NULL)
1143186106Ssam		cmdcnf->shells = cfg->shells;
1144186106Ssam	if (cmdcnf->shell_default == NULL)
1145186106Ssam		cmdcnf->shell_default = cfg->shell_default;
1146186106Ssam	if (cmdcnf->default_group == NULL)
1147186106Ssam		cmdcnf->default_group = cfg->default_group;
1148186106Ssam	if (cmdcnf->groups == NULL)
1149186106Ssam		cmdcnf->groups = cfg->groups;
1150186106Ssam	if (cmdcnf->default_class == NULL)
1151186106Ssam		cmdcnf->default_class = cfg->default_class;
1152186106Ssam	if (cmdcnf->min_uid == 0)
1153186106Ssam		cmdcnf->min_uid = cfg->min_uid;
1154186106Ssam	if (cmdcnf->max_uid == 0)
1155186106Ssam		cmdcnf->max_uid = cfg->max_uid;
1156186106Ssam	if (cmdcnf->min_gid == 0)
1157186106Ssam		cmdcnf->min_gid = cfg->min_gid;
1158186106Ssam	if (cmdcnf->max_gid == 0)
1159186106Ssam		cmdcnf->max_gid = cfg->max_gid;
1160187846Ssam	if (cmdcnf->expire_days == 0)
1161186106Ssam		cmdcnf->expire_days = cfg->expire_days;
1162186106Ssam	if (cmdcnf->password_days == 0)
1163186106Ssam		cmdcnf->password_days = cfg->password_days;
1164186106Ssam}
1165187846Ssam
1166186106Ssamint
1167186106Ssampw_user_add(int argc, char **argv, char *arg1)
1168186106Ssam{
1169186106Ssam	struct userconf *cnf, *cmdcnf;
1170187846Ssam	struct passwd *pwd;
1171186106Ssam	struct group *grp;
1172186106Ssam	struct stat st;
1173186106Ssam	char args[] = "C:qn:u:c:d:e:p:g:G:mM:k:s:oL:i:w:h:H:Db:NPy:Y";
1174186106Ssam	char line[_PASSWORD_LEN+1], path[MAXPATHLEN];
1175186106Ssam	char *gecos, *homedir, *skel, *walk, *userid, *groupid, *grname;
1176186106Ssam	char *default_passwd, *name, *p;
1177186106Ssam	const char *cfg;
1178186106Ssam	login_cap_t *lc;
1179186106Ssam	FILE *pfp, *fp;
1180186106Ssam	intmax_t id = -1;
1181186106Ssam	time_t now;
1182186106Ssam	int rc, ch, fd = -1;
1183186106Ssam	size_t i;
1184186106Ssam	bool dryrun, nis, pretty, quiet, createhome, precrypted, genconf;
1185186106Ssam
1186186106Ssam	dryrun = nis = pretty = quiet = createhome = precrypted = false;
1187186106Ssam	genconf = false;
1188186106Ssam	gecos = homedir = skel = userid = groupid = default_passwd = NULL;
1189186106Ssam	grname = name = NULL;
1190186106Ssam
1191186106Ssam	if ((cmdcnf = calloc(1, sizeof(struct userconf))) == NULL)
1192186106Ssam		err(EXIT_FAILURE, "calloc()");
1193186106Ssam
1194186106Ssam	if (arg1 != NULL) {
1195186106Ssam		if (arg1[strspn(arg1, "0123456789")] == '\0')
1196186106Ssam			id = pw_checkid(arg1, UID_MAX);
1197186106Ssam		else
1198178355Ssam			name = arg1;
1199178355Ssam	}
1200178355Ssam
1201178355Ssam	while ((ch = getopt(argc, argv, args)) != -1) {
1202178355Ssam		switch (ch) {
1203178355Ssam		case 'C':
1204178355Ssam			cfg = optarg;
1205178355Ssam			break;
1206178355Ssam		case 'q':
1207178355Ssam			quiet = true;
1208178355Ssam			break;
1209178355Ssam		case 'n':
1210178355Ssam			name = optarg;
1211178355Ssam			break;
1212178355Ssam		case 'u':
1213178355Ssam			userid = optarg;
1214178355Ssam			break;
1215178355Ssam		case 'c':
1216178355Ssam			gecos = pw_checkname(optarg, 1);
1217178355Ssam			break;
1218178355Ssam		case 'd':
1219178355Ssam			homedir = optarg;
1220178355Ssam			break;
1221178355Ssam		case 'e':
1222178355Ssam			now = time(NULL);
1223178355Ssam			cmdcnf->expire_days = parse_date(now, optarg);
1224178355Ssam			break;
1225178355Ssam		case 'p':
1226178355Ssam			now = time(NULL);
1227178355Ssam			cmdcnf->password_days = parse_date(now, optarg);
1228178355Ssam			break;
1229178355Ssam		case 'g':
1230178355Ssam			validate_grname(cmdcnf, optarg);
1231178355Ssam			grname = optarg;
1232178355Ssam			break;
1233178355Ssam		case 'G':
1234178355Ssam			split_groups(&cmdcnf->groups, optarg);
1235178355Ssam			break;
1236178355Ssam		case 'm':
1237178355Ssam			createhome = true;
1238178355Ssam			break;
1239178355Ssam		case 'M':
1240178355Ssam			cmdcnf->homemode = validate_mode(optarg);
1241178355Ssam			break;
1242178355Ssam		case 'k':
1243178355Ssam			walk = skel = optarg;
1244178355Ssam			if (*walk == '/')
1245178355Ssam				walk++;
1246178355Ssam			if (fstatat(conf.rootfd, walk, &st, 0) == -1)
1247178355Ssam				errx(EX_OSFILE, "skeleton `%s' does not "
1248178355Ssam				    "exists", skel);
1249178355Ssam			if (!S_ISDIR(st.st_mode))
1250178355Ssam				errx(EX_OSFILE, "skeleton `%s' is not a "
1251178355Ssam				    "directory", skel);
1252178355Ssam			cmdcnf->dotdir = skel;
1253178355Ssam			break;
1254178355Ssam		case 's':
1255178355Ssam			cmdcnf->shell_default = optarg;
1256178355Ssam			break;
1257178355Ssam		case 'o':
1258178355Ssam			conf.checkduplicate = false;
1259178355Ssam			break;
1260178355Ssam		case 'L':
1261178355Ssam			cmdcnf->default_class = pw_checkname(optarg, 0);
1262178355Ssam			break;
1263178355Ssam		case 'i':
1264178355Ssam			groupid = optarg;
1265178355Ssam			break;
1266178355Ssam		case 'w':
1267178355Ssam			default_passwd = optarg;
1268178355Ssam			break;
1269178355Ssam		case 'H':
1270178355Ssam			if (fd != -1)
1271178355Ssam				errx(EX_USAGE, "'-h' and '-H' are mutually "
1272178355Ssam				    "exclusive options");
1273178355Ssam			fd = pw_checkfd(optarg);
1274178355Ssam			precrypted = true;
1275178355Ssam			if (fd == '-')
1276178355Ssam				errx(EX_USAGE, "-H expects a file descriptor");
1277178355Ssam			break;
1278178355Ssam		case 'h':
1279178355Ssam			if (fd != -1)
1280178355Ssam				errx(EX_USAGE, "'-h' and '-H' are mutually "
1281178355Ssam				    "exclusive options");
1282178355Ssam			fd = pw_checkfd(optarg);
1283178355Ssam			break;
1284178355Ssam		case 'D':
1285178355Ssam			genconf = true;
1286178355Ssam			break;
1287178355Ssam		case 'b':
1288178355Ssam			cmdcnf->home = optarg;
1289178355Ssam			break;
1290178355Ssam		case 'N':
1291178355Ssam			dryrun = true;
1292178355Ssam			break;
1293178355Ssam		case 'P':
1294178355Ssam			pretty = true;
1295178355Ssam			break;
1296178355Ssam		case 'y':
1297178355Ssam			cmdcnf->nispasswd = optarg;
1298178355Ssam			break;
1299178355Ssam		case 'Y':
1300178355Ssam			nis = true;
1301178355Ssam			break;
1302178355Ssam		}
1303178355Ssam	}
1304178355Ssam
1305178355Ssam	if (geteuid() != 0 && ! dryrun)
1306178355Ssam		errx(EX_NOPERM, "you must be root");
1307178355Ssam
1308178355Ssam	if (quiet)
1309178355Ssam		freopen(_PATH_DEVNULL, "w", stderr);
1310178355Ssam
1311178355Ssam	cnf = get_userconfig(cfg);
1312178355Ssam
1313178355Ssam	mix_config(cmdcnf, cnf);
1314178355Ssam	if (default_passwd)
1315178355Ssam		cmdcnf->default_password = boolean_val(default_passwd,
1316178355Ssam		    cnf->default_password);
1317178355Ssam	if (genconf) {
1318178355Ssam		if (name != NULL)
1319178355Ssam			errx(EX_DATAERR, "can't combine `-D' with `-n name'");
1320178355Ssam		if (userid != NULL) {
1321178355Ssam			if ((p = strtok(userid, ", \t")) != NULL)
1322178355Ssam				cmdcnf->min_uid = pw_checkid(p, UID_MAX);
1323178355Ssam			if (cmdcnf->min_uid == 0)
1324178355Ssam				cmdcnf->min_uid = 1000;
1325178355Ssam			if ((p = strtok(NULL, " ,\t")) != NULL)
1326178355Ssam				cmdcnf->max_uid = pw_checkid(p, UID_MAX);
1327178355Ssam			if (cmdcnf->max_uid == 0)
1328178355Ssam				cmdcnf->max_uid = 32000;
1329178355Ssam		}
1330178355Ssam		if (groupid != NULL) {
1331178355Ssam			if ((p = strtok(groupid, ", \t")) != NULL)
1332178355Ssam				cmdcnf->min_gid = pw_checkid(p, GID_MAX);
1333178355Ssam			if (cmdcnf->min_gid == 0)
1334178355Ssam				cmdcnf->min_gid = 1000;
1335178355Ssam			if ((p = strtok(NULL, " ,\t")) != NULL)
1336178355Ssam				cmdcnf->max_gid = pw_checkid(p, GID_MAX);
1337178355Ssam			if (cmdcnf->max_gid == 0)
1338178355Ssam				cmdcnf->max_gid = 32000;
1339178355Ssam		}
1340178355Ssam		if (write_userconfig(cmdcnf, cfg))
1341178355Ssam			return (EXIT_SUCCESS);
1342178355Ssam		err(EX_IOERR, "config update");
1343178355Ssam	}
1344178355Ssam
1345178355Ssam	if (userid)
1346178355Ssam		id = pw_checkid(userid, UID_MAX);
1347178355Ssam	if (id < 0 && name == NULL)
1348178355Ssam		errx(EX_DATAERR, "user name or id required");
1349178355Ssam
1350178355Ssam	if (name == NULL)
1351178355Ssam		errx(EX_DATAERR, "login name required");
1352178355Ssam
1353178355Ssam	if (GETPWNAM(name) != NULL)
1354178355Ssam		errx(EX_DATAERR, "login name `%s' already exists", name);
1355178355Ssam
1356178355Ssam	pwd = &fakeuser;
1357178355Ssam	pwd->pw_name = name;
1358178355Ssam	pwd->pw_class = cmdcnf->default_class ? cmdcnf->default_class : "";
1359178355Ssam	pwd->pw_uid = pw_uidpolicy(cmdcnf, id);
1360178355Ssam	pwd->pw_gid = pw_gidpolicy(cnf, grname, pwd->pw_name,
1361178355Ssam	    (gid_t) pwd->pw_uid, dryrun);
1362178355Ssam	pwd->pw_change = cmdcnf->password_days;
1363178355Ssam	pwd->pw_expire = cmdcnf->expire_days;
1364178355Ssam	pwd->pw_dir = pw_homepolicy(cmdcnf, homedir, pwd->pw_name);
1365178355Ssam	pwd->pw_shell = pw_shellpolicy(cmdcnf);
1366178355Ssam	lc = login_getpwclass(pwd);
1367178355Ssam	if (lc == NULL || login_setcryptfmt(lc, "sha512", NULL) == NULL)
1368178355Ssam		warn("setting crypt(3) format");
1369178355Ssam	login_close(lc);
1370178355Ssam	pwd->pw_passwd = pw_password(cmdcnf, pwd->pw_name, dryrun);
1371178355Ssam	if (pwd->pw_uid == 0 && strcmp(pwd->pw_name, "root") != 0)
1372178355Ssam		warnx("WARNING: new account `%s' has a uid of 0 "
1373178355Ssam		    "(superuser access!)", pwd->pw_name);
1374178355Ssam	if (gecos)
1375178355Ssam		pwd->pw_gecos = gecos;
1376178355Ssam
1377178355Ssam	if (fd != -1)
1378178355Ssam		pw_set_passwd(pwd, fd, precrypted, false);
1379178355Ssam
1380178355Ssam	if (dryrun)
1381178355Ssam		return (print_user(pwd, pretty, false));
1382178355Ssam
1383178355Ssam	if ((rc = addpwent(pwd)) != 0) {
1384178355Ssam		if (rc == -1)
1385178355Ssam			errx(EX_IOERR, "user '%s' already exists",
1386178355Ssam			    pwd->pw_name);
1387178355Ssam		else if (rc != 0)
1388178355Ssam			err(EX_IOERR, "passwd file update");
1389178355Ssam	}
1390178355Ssam	if (nis && cmdcnf->nispasswd && *cmdcnf->nispasswd == '/') {
1391178355Ssam		printf("%s\n", cmdcnf->nispasswd);
1392178355Ssam		rc = addnispwent(cmdcnf->nispasswd, pwd);
1393178355Ssam		if (rc == -1)
1394178355Ssam			warnx("User '%s' already exists in NIS passwd",
1395178355Ssam			    pwd->pw_name);
1396178355Ssam		else if (rc != 0)
1397178355Ssam			warn("NIS passwd update");
1398178355Ssam		/* NOTE: we treat NIS-only update errors as non-fatal */
1399178355Ssam	}
1400178355Ssam
1401178355Ssam	if (cmdcnf->groups != NULL) {
1402178355Ssam		for (i = 0; i < cmdcnf->groups->sl_cur; i++) {
1403178355Ssam			grp = GETGRNAM(cmdcnf->groups->sl_str[i]);
1404178355Ssam			grp = gr_add(grp, pwd->pw_name);
1405178355Ssam			/*
1406178355Ssam			 * grp can only be NULL in 2 cases:
1407178355Ssam			 * - the new member is already a member
1408178355Ssam			 * - a problem with memory occurs
1409178355Ssam			 * in both cases we want to skip now.
1410178355Ssam			 */
1411178355Ssam			if (grp == NULL)
1412178355Ssam				continue;
1413178355Ssam			chggrent(grp->gr_name, grp);
1414178355Ssam			free(grp);
1415178355Ssam		}
1416178355Ssam	}
1417178355Ssam
1418178355Ssam	pwd = GETPWNAM(name);
1419178355Ssam	if (pwd == NULL)
1420178355Ssam		errx(EX_NOUSER, "user '%s' disappeared during update", name);
1421178355Ssam
1422178355Ssam	grp = GETGRGID(pwd->pw_gid);
1423178355Ssam	pw_log(cnf, M_ADD, W_USER, "%s(%ju):%s(%ju):%s:%s:%s",
1424178355Ssam	       pwd->pw_name, (uintmax_t)pwd->pw_uid,
1425178355Ssam	    grp ? grp->gr_name : "unknown",
1426178355Ssam	       (uintmax_t)(grp ? grp->gr_gid : (uid_t)-1),
1427178355Ssam	       pwd->pw_gecos, pwd->pw_dir, pwd->pw_shell);
1428178355Ssam
1429178355Ssam	/*
1430178355Ssam	 * let's touch and chown the user's mail file. This is not
1431178355Ssam	 * strictly necessary under BSD with a 0755 maildir but it also
1432178355Ssam	 * doesn't hurt anything to create the empty mailfile
1433178355Ssam	 */
1434178355Ssam	if (PWALTDIR() != PWF_ALT) {
1435178355Ssam		snprintf(path, sizeof(path), "%s/%s", _PATH_MAILDIR,
1436178355Ssam		    pwd->pw_name);
1437178355Ssam		/* Preserve contents & mtime */
1438178355Ssam		close(openat(conf.rootfd, path +1, O_RDWR | O_CREAT, 0600));
1439178355Ssam		fchownat(conf.rootfd, path + 1, pwd->pw_uid, pwd->pw_gid,
1440178355Ssam		    AT_SYMLINK_NOFOLLOW);
1441178355Ssam	}
1442178355Ssam
1443178355Ssam	/*
1444178355Ssam	 * Let's create and populate the user's home directory. Note
1445178355Ssam	 * that this also `works' for editing users if -m is used, but
1446178355Ssam	 * existing files will *not* be overwritten.
1447178355Ssam	 */
1448178355Ssam	if (PWALTDIR() != PWF_ALT && createhome && pwd->pw_dir &&
1449178355Ssam	    *pwd->pw_dir == '/' && pwd->pw_dir[1])
1450178355Ssam		create_and_populate_homedir(cmdcnf, pwd, cmdcnf->dotdir,
1451178355Ssam		    cmdcnf->homemode, false);
1452178355Ssam
1453178355Ssam	if (!PWALTDIR() && cmdcnf->newmail && *cmdcnf->newmail &&
1454178355Ssam	    (fp = fopen(cnf->newmail, "r")) != NULL) {
1455178355Ssam		if ((pfp = popen(_PATH_SENDMAIL " -t", "w")) == NULL)
1456178355Ssam			warn("sendmail");
1457178355Ssam		else {
1458178355Ssam			fprintf(pfp, "From: root\n" "To: %s\n"
1459178355Ssam			    "Subject: Welcome!\n\n", pwd->pw_name);
1460178355Ssam			while (fgets(line, sizeof(line), fp) != NULL) {
1461178355Ssam				/* Do substitutions? */
1462178355Ssam				fputs(line, pfp);
1463178355Ssam			}
1464178355Ssam			pclose(pfp);
1465178355Ssam			pw_log(cnf, M_ADD, W_USER, "%s(%ju) new user mail sent",
1466178355Ssam			    pwd->pw_name, (uintmax_t)pwd->pw_uid);
1467178355Ssam		}
1468178355Ssam		fclose(fp);
1469178355Ssam	}
1470178355Ssam
1471178355Ssam	if (nis && nis_update() == 0)
1472178355Ssam		pw_log(cnf, M_ADD, W_USER, "NIS maps updated");
1473178355Ssam
1474178355Ssam	return (EXIT_SUCCESS);
1475178355Ssam}
1476178355Ssam
1477178355Ssamint
1478178355Ssampw_user_mod(int argc, char **argv, char *arg1)
1479178355Ssam{
1480178355Ssam	struct userconf *cnf;
1481178355Ssam	struct passwd *pwd;
1482178355Ssam	struct group *grp;
1483178355Ssam	StringList *groups = NULL;
1484178355Ssam	char args[] = "C:qn:u:c:d:e:p:g:G:mM:l:k:s:w:L:h:H:NPYy:";
1485178355Ssam	const char *cfg;
1486178355Ssam	char *gecos, *homedir, *grname, *name, *newname, *walk, *skel, *shell;
1487178355Ssam	char *passwd, *class, *nispasswd;
1488178355Ssam	login_cap_t *lc;
1489178355Ssam	struct stat st;
1490178355Ssam	intmax_t id = -1;
1491178355Ssam	int ch, fd = -1;
1492178355Ssam	size_t i, j;
1493178355Ssam	bool quiet, createhome, pretty, dryrun, nis, edited, docreatehome;
1494178355Ssam	bool precrypted;
1495178355Ssam	mode_t homemode = 0;
1496178355Ssam	time_t expire_days, password_days, now;
1497178355Ssam
1498178355Ssam	expire_days = password_days = -1;
1499178355Ssam	gecos = homedir = grname = name = newname = skel = shell =NULL;
1500178355Ssam	passwd = NULL;
1501178355Ssam	class = nispasswd = NULL;
1502178355Ssam	quiet = createhome = pretty = dryrun = nis = precrypted = false;
1503178355Ssam	edited = docreatehome = false;
1504178355Ssam
1505178355Ssam	if (arg1 != NULL) {
1506178355Ssam		if (arg1[strspn(arg1, "0123456789")] == '\0')
1507178355Ssam			id = pw_checkid(arg1, UID_MAX);
1508178355Ssam		else
1509178355Ssam			name = arg1;
1510178355Ssam	}
1511178355Ssam
1512178355Ssam	while ((ch = getopt(argc, argv, args)) != -1) {
1513178355Ssam		switch (ch) {
1514178355Ssam		case 'C':
1515178355Ssam			cfg = optarg;
1516178355Ssam			break;
1517178355Ssam		case 'q':
1518178355Ssam			quiet = true;
1519178355Ssam			break;
1520178355Ssam		case 'n':
1521178355Ssam			name = optarg;
1522178355Ssam			break;
1523178355Ssam		case 'u':
1524178355Ssam			id = pw_checkid(optarg, UID_MAX);
1525178355Ssam			break;
1526178355Ssam		case 'c':
1527178355Ssam			gecos = pw_checkname(optarg, 1);
1528178355Ssam			break;
1529178355Ssam		case 'd':
1530178355Ssam			homedir = optarg;
1531178355Ssam			break;
1532178355Ssam		case 'e':
1533178355Ssam			now = time(NULL);
1534178355Ssam			expire_days = parse_date(now, optarg);
1535178355Ssam			break;
1536178355Ssam		case 'p':
1537178355Ssam			now = time(NULL);
1538178355Ssam			password_days = parse_date(now, optarg);
1539178355Ssam			break;
1540178355Ssam		case 'g':
1541178355Ssam			group_from_name_or_id(optarg);
1542178355Ssam			grname = optarg;
1543178355Ssam			break;
1544186106Ssam		case 'G':
1545186106Ssam			split_groups(&groups, optarg);
1546186106Ssam			break;
1547186106Ssam		case 'm':
1548178355Ssam			createhome = true;
1549178355Ssam			break;
1550178355Ssam		case 'M':
1551178355Ssam			homemode = validate_mode(optarg);
1552178355Ssam			break;
1553178355Ssam		case 'l':
1554178355Ssam			newname = optarg;
1555187343Ssam			break;
1556187343Ssam		case 'k':
1557187343Ssam			walk = skel = optarg;
1558187343Ssam			if (*walk == '/')
1559187343Ssam				walk++;
1560187343Ssam			if (fstatat(conf.rootfd, walk, &st, 0) == -1)
1561187343Ssam				errx(EX_OSFILE, "skeleton `%s' does not "
1562187343Ssam				    "exists", skel);
1563187343Ssam			if (!S_ISDIR(st.st_mode))
1564187343Ssam				errx(EX_OSFILE, "skeleton `%s' is not a "
1565187343Ssam				    "directory", skel);
1566187343Ssam			break;
1567187343Ssam		case 's':
1568187343Ssam			shell = optarg;
1569187343Ssam			break;
1570187343Ssam		case 'w':
1571187343Ssam			passwd = optarg;
1572178355Ssam			break;
1573178355Ssam		case 'L':
1574178355Ssam			class = pw_checkname(optarg, 0);
1575178355Ssam			break;
1576178355Ssam		case 'H':
1577178355Ssam			if (fd != -1)
1578178355Ssam				errx(EX_USAGE, "'-h' and '-H' are mutually "
1579178355Ssam				    "exclusive options");
1580178355Ssam			fd = pw_checkfd(optarg);
1581178355Ssam			precrypted = true;
1582178355Ssam			if (fd == '-')
1583178355Ssam				errx(EX_USAGE, "-H expects a file descriptor");
1584178355Ssam			break;
1585178355Ssam		case 'h':
1586178355Ssam			if (fd != -1)
1587178355Ssam				errx(EX_USAGE, "'-h' and '-H' are mutually "
1588178355Ssam				    "exclusive options");
1589178355Ssam			fd = pw_checkfd(optarg);
1590178355Ssam			break;
1591178355Ssam		case 'N':
1592178355Ssam			dryrun = true;
1593178355Ssam			break;
1594178355Ssam		case 'P':
1595178355Ssam			pretty = true;
1596178355Ssam			break;
1597178355Ssam		case 'y':
1598178355Ssam			nispasswd = optarg;
1599178355Ssam			break;
1600178355Ssam		case 'Y':
1601178355Ssam			nis = true;
1602178355Ssam			break;
1603178355Ssam		}
1604178355Ssam	}
1605178355Ssam
1606178355Ssam	if (geteuid() != 0 && ! dryrun)
1607178355Ssam		errx(EX_NOPERM, "you must be root");
1608178355Ssam
1609178355Ssam	if (quiet)
1610178355Ssam		freopen(_PATH_DEVNULL, "w", stderr);
1611178355Ssam
1612178355Ssam	cnf = get_userconfig(cfg);
1613178355Ssam
1614178355Ssam	if (id < 0 && name == NULL)
1615178355Ssam		errx(EX_DATAERR, "username or id required");
1616178355Ssam
1617178355Ssam	pwd = (name != NULL) ? GETPWNAM(pw_checkname(name, 0)) : GETPWUID(id);
1618178355Ssam	if (pwd == NULL) {
1619178355Ssam		if (name == NULL)
1620178355Ssam			errx(EX_NOUSER, "no such uid `%ju'",
1621178355Ssam			    (uintmax_t) id);
1622178355Ssam		errx(EX_NOUSER, "no such user `%s'", name);
1623178355Ssam	}
1624178355Ssam
1625178355Ssam	if (name == NULL)
1626178355Ssam		name = pwd->pw_name;
1627178355Ssam
1628178355Ssam	if (nis && nispasswd == NULL)
1629178355Ssam		nispasswd = cnf->nispasswd;
1630178355Ssam
1631178355Ssam	if (PWF._altdir == PWF_REGULAR &&
1632178355Ssam	    ((pwd->pw_fields & _PWF_SOURCE) != _PWF_FILES)) {
1633178355Ssam		if ((pwd->pw_fields & _PWF_SOURCE) == _PWF_NIS) {
1634178355Ssam			if (!nis && nispasswd && *nispasswd != '/')
1635178355Ssam				errx(EX_NOUSER, "Cannot modify NIS user `%s'",
1636178355Ssam				    name);
1637178355Ssam		} else {
1638178355Ssam			errx(EX_NOUSER, "Cannot modify non local user `%s'",
1639178355Ssam			    name);
1640178355Ssam		}
1641178355Ssam	}
1642178355Ssam
1643178355Ssam	if (newname) {
1644178355Ssam		if (strcmp(pwd->pw_name, "root") == 0)
1645178355Ssam			errx(EX_DATAERR, "can't rename `root' account");
1646178355Ssam		if (strcmp(pwd->pw_name, newname) != 0) {
1647178355Ssam			pwd->pw_name = pw_checkname(newname, 0);
1648178355Ssam			edited = true;
1649178355Ssam		}
1650178355Ssam	}
1651178355Ssam
1652178355Ssam	if (id > 0 && pwd->pw_uid != id) {
1653178355Ssam		pwd->pw_uid = id;
1654178355Ssam		edited = true;
1655178355Ssam		if (pwd->pw_uid != 0 && strcmp(pwd->pw_name, "root") == 0)
1656178355Ssam			errx(EX_DATAERR, "can't change uid of `root' account");
1657178355Ssam		if (pwd->pw_uid == 0 && strcmp(pwd->pw_name, "root") != 0)
1658178355Ssam			warnx("WARNING: account `%s' will have a uid of 0 "
1659178355Ssam			    "(superuser access!)", pwd->pw_name);
1660178355Ssam	}
1661178355Ssam
1662178355Ssam	if (grname && pwd->pw_uid != 0) {
1663178355Ssam		grp = GETGRNAM(grname);
1664178355Ssam		if (grp == NULL)
1665178355Ssam			grp = GETGRGID(pw_checkid(grname, GID_MAX));
1666178355Ssam		if (grp->gr_gid != pwd->pw_gid) {
1667178355Ssam			pwd->pw_gid = grp->gr_gid;
1668178355Ssam			edited = true;
1669178355Ssam		}
1670178355Ssam	}
1671178355Ssam
1672178355Ssam	if (password_days >= 0 && pwd->pw_change != password_days) {
1673178355Ssam		pwd->pw_change = password_days;
1674178355Ssam		edited = true;
1675178355Ssam	}
1676178355Ssam
1677178355Ssam	if (expire_days >= 0 && pwd->pw_expire != expire_days) {
1678178355Ssam		pwd->pw_expire = expire_days;
1679178355Ssam		edited = true;
1680178355Ssam	}
1681178355Ssam
1682178355Ssam	if (shell) {
1683178355Ssam		shell = shell_path(cnf->shelldir, cnf->shells, shell);
1684178355Ssam		if (shell == NULL)
1685178355Ssam			shell = "";
1686178355Ssam		if (strcmp(shell, pwd->pw_shell) != 0) {
1687178355Ssam			pwd->pw_shell = shell;
1688178355Ssam			edited = true;
1689178355Ssam		}
1690178355Ssam	}
1691178355Ssam
1692178355Ssam	if (class && strcmp(pwd->pw_class, class) != 0) {
1693178355Ssam		pwd->pw_class = class;
1694178355Ssam		edited = true;
1695178355Ssam	}
1696178355Ssam
1697178355Ssam	if (homedir && strcmp(pwd->pw_dir, homedir) != 0) {
1698178355Ssam		pwd->pw_dir = homedir;
1699178355Ssam		edited = true;
1700178355Ssam		if (fstatat(conf.rootfd, pwd->pw_dir, &st, 0) == -1) {
1701178355Ssam			if (!createhome)
1702178355Ssam				warnx("WARNING: home `%s' does not exist",
1703178355Ssam				    pwd->pw_dir);
1704178355Ssam			else
1705178355Ssam				docreatehome = true;
1706178355Ssam		} else if (!S_ISDIR(st.st_mode)) {
1707178355Ssam			warnx("WARNING: home `%s' is not a directory",
1708178355Ssam			    pwd->pw_dir);
1709178355Ssam		}
1710178355Ssam	}
1711178355Ssam
1712178355Ssam	if (passwd && conf.fd == -1) {
1713178355Ssam		lc = login_getpwclass(pwd);
1714186106Ssam		if (lc == NULL || login_setcryptfmt(lc, "sha512", NULL) == NULL)
1715186106Ssam			warn("setting crypt(3) format");
1716187846Ssam		login_close(lc);
1717186106Ssam		cnf->default_password = boolean_val(passwd,
1718186106Ssam		    cnf->default_password);
1719186106Ssam		pwd->pw_passwd = pw_password(cnf, pwd->pw_name, dryrun);
1720186106Ssam		edited = true;
1721186106Ssam	}
1722187846Ssam
1723186106Ssam	if (gecos && strcmp(pwd->pw_gecos, gecos) != 0) {
1724186106Ssam		pwd->pw_gecos = gecos;
1725186106Ssam		edited = true;
1726186106Ssam	}
1727186106Ssam
1728187846Ssam	if (fd != -1)
1729186106Ssam		edited = pw_set_passwd(pwd, fd, precrypted, true);
1730186106Ssam
1731186106Ssam	if (dryrun)
1732186106Ssam		return (print_user(pwd, pretty, false));
1733186106Ssam
1734187846Ssam	if (edited) /* Only updated this if required */
1735186106Ssam		perform_chgpwent(name, pwd, nis ? nispasswd : NULL);
1736186106Ssam	/* Now perform the needed changes concern groups */
1737186106Ssam	if (groups != NULL) {
1738186106Ssam		/* Delete User from groups using old name */
1739186106Ssam		SETGRENT();
1740187846Ssam		while ((grp = GETGRENT()) != NULL) {
1741186106Ssam			if (grp->gr_mem == NULL)
1742186106Ssam				continue;
1743186106Ssam			for (i = 0; grp->gr_mem[i] != NULL; i++) {
1744186106Ssam				if (strcmp(grp->gr_mem[i] , name) != 0)
1745186106Ssam					continue;
1746187846Ssam				for (j = i; grp->gr_mem[j] != NULL ; j++)
1747186106Ssam					grp->gr_mem[j] = grp->gr_mem[j+1];
1748186106Ssam				chggrent(grp->gr_name, grp);
1749186106Ssam				break;
1750186106Ssam			}
1751178355Ssam		}
1752178355Ssam		ENDGRENT();
1753178355Ssam		/* Add the user to the needed groups */
1754		for (i = 0; i < groups->sl_cur; i++) {
1755			grp = GETGRNAM(groups->sl_str[i]);
1756			grp = gr_add(grp, pwd->pw_name);
1757			if (grp == NULL)
1758				continue;
1759			chggrent(grp->gr_name, grp);
1760			free(grp);
1761		}
1762	}
1763	/* In case of rename we need to walk over the different groups */
1764	if (newname) {
1765		SETGRENT();
1766		while ((grp = GETGRENT()) != NULL) {
1767			if (grp->gr_mem == NULL)
1768				continue;
1769			for (i = 0; grp->gr_mem[i] != NULL; i++) {
1770				if (strcmp(grp->gr_mem[i], name) != 0)
1771					continue;
1772				grp->gr_mem[i] = newname;
1773				chggrent(grp->gr_name, grp);
1774				break;
1775			}
1776		}
1777	}
1778
1779	/* go get a current version of pwd */
1780	if (newname)
1781		name = newname;
1782	pwd = GETPWNAM(name);
1783	if (pwd == NULL)
1784		errx(EX_NOUSER, "user '%s' disappeared during update", name);
1785	grp = GETGRGID(pwd->pw_gid);
1786	pw_log(cnf, M_UPDATE, W_USER, "%s(%ju):%s(%ju):%s:%s:%s",
1787	    pwd->pw_name, (uintmax_t)pwd->pw_uid,
1788	    grp ? grp->gr_name : "unknown",
1789	    (uintmax_t)(grp ? grp->gr_gid : (uid_t)-1),
1790	    pwd->pw_gecos, pwd->pw_dir, pwd->pw_shell);
1791
1792	/*
1793	 * Let's create and populate the user's home directory. Note
1794	 * that this also `works' for editing users if -m is used, but
1795	 * existing files will *not* be overwritten.
1796	 */
1797	if (PWALTDIR() != PWF_ALT && docreatehome && pwd->pw_dir &&
1798	    *pwd->pw_dir == '/' && pwd->pw_dir[1]) {
1799		if (!skel)
1800			skel = cnf->dotdir;
1801		if (homemode == 0)
1802			homemode = cnf->homemode;
1803		create_and_populate_homedir(cnf, pwd, skel, homemode, true);
1804	}
1805
1806	if (nis && nis_update() == 0)
1807		pw_log(cnf, M_UPDATE, W_USER, "NIS maps updated");
1808
1809	return (EXIT_SUCCESS);
1810}
1811