summaryrefslogtreecommitdiffstats
path: root/usr.sbin/pw
diff options
context:
space:
mode:
Diffstat (limited to 'usr.sbin/pw')
-rw-r--r--usr.sbin/pw/Makefile19
-rw-r--r--usr.sbin/pw/Makefile.depend21
-rw-r--r--usr.sbin/pw/README22
-rw-r--r--usr.sbin/pw/bitmap.c131
-rw-r--r--usr.sbin/pw/bitmap.h50
-rw-r--r--usr.sbin/pw/cpdir.c124
-rw-r--r--usr.sbin/pw/grupd.c105
-rw-r--r--usr.sbin/pw/psdate.c261
-rw-r--r--usr.sbin/pw/psdate.h40
-rw-r--r--usr.sbin/pw/pw.81049
-rw-r--r--usr.sbin/pw/pw.c381
-rw-r--r--usr.sbin/pw/pw.conf.5318
-rw-r--r--usr.sbin/pw/pw.h106
-rw-r--r--usr.sbin/pw/pw_conf.c524
-rw-r--r--usr.sbin/pw/pw_group.c694
-rw-r--r--usr.sbin/pw/pw_log.c68
-rw-r--r--usr.sbin/pw/pw_nis.c95
-rw-r--r--usr.sbin/pw/pw_user.c1815
-rw-r--r--usr.sbin/pw/pw_utils.c99
-rw-r--r--usr.sbin/pw/pw_vpw.c205
-rw-r--r--usr.sbin/pw/pwupd.c149
-rw-r--r--usr.sbin/pw/pwupd.h152
-rw-r--r--usr.sbin/pw/rm_r.c70
-rw-r--r--usr.sbin/pw/strtounum.c72
-rw-r--r--usr.sbin/pw/tests/Makefile22
-rw-r--r--usr.sbin/pw/tests/group3
-rwxr-xr-xusr.sbin/pw/tests/helper_functions.shin32
-rw-r--r--usr.sbin/pw/tests/master.passwd4
-rw-r--r--usr.sbin/pw/tests/pw-modified.conf62
-rw-r--r--usr.sbin/pw/tests/pw.conf62
-rwxr-xr-xusr.sbin/pw/tests/pw_config.sh26
-rwxr-xr-xusr.sbin/pw/tests/pw_etcdir.sh18
-rwxr-xr-xusr.sbin/pw/tests/pw_groupadd.sh26
-rwxr-xr-xusr.sbin/pw/tests/pw_groupdel.sh24
-rwxr-xr-xusr.sbin/pw/tests/pw_groupmod.sh118
-rwxr-xr-xusr.sbin/pw/tests/pw_lock.sh42
-rwxr-xr-xusr.sbin/pw/tests/pw_useradd.sh385
-rwxr-xr-xusr.sbin/pw/tests/pw_userdel.sh67
-rwxr-xr-xusr.sbin/pw/tests/pw_usermod.sh222
-rwxr-xr-xusr.sbin/pw/tests/pw_usernext.sh45
40 files changed, 7728 insertions, 0 deletions
diff --git a/usr.sbin/pw/Makefile b/usr.sbin/pw/Makefile
new file mode 100644
index 0000000..f26c9de
--- /dev/null
+++ b/usr.sbin/pw/Makefile
@@ -0,0 +1,19 @@
+# $FreeBSD$
+
+PROG= pw
+MAN= pw.conf.5 pw.8
+SRCS= pw.c pw_conf.c pw_user.c pw_group.c pw_log.c pw_nis.c pw_vpw.c \
+ grupd.c pwupd.c psdate.c bitmap.c cpdir.c rm_r.c strtounum.c \
+ pw_utils.c
+
+WARNS?= 3
+
+LIBADD= crypt util sbuf
+
+.include <src.opts.mk>
+
+.if ${MK_TESTS} != "no"
+SUBDIR+= tests
+.endif
+
+.include <bsd.prog.mk>
diff --git a/usr.sbin/pw/Makefile.depend b/usr.sbin/pw/Makefile.depend
new file mode 100644
index 0000000..392fb60
--- /dev/null
+++ b/usr.sbin/pw/Makefile.depend
@@ -0,0 +1,21 @@
+# $FreeBSD$
+# Autogenerated - do NOT edit!
+
+DIRDEPS = \
+ gnu/lib/csu \
+ gnu/lib/libgcc \
+ include \
+ include/xlocale \
+ lib/${CSU_DIR} \
+ lib/libc \
+ lib/libcompiler_rt \
+ lib/libcrypt \
+ lib/libsbuf \
+ lib/libutil \
+
+
+.include <dirdeps.mk>
+
+.if ${DEP_RELDIR} == ${_DEP_RELDIR}
+# local dependencies - needed for -jN in clean tree
+.endif
diff --git a/usr.sbin/pw/README b/usr.sbin/pw/README
new file mode 100644
index 0000000..bbb1539
--- /dev/null
+++ b/usr.sbin/pw/README
@@ -0,0 +1,22 @@
+
+pw is a command-line driven passwd/group editor utility that provides
+an easy and safe means of modifying of any/all fields in the system
+password files, and has an add, modify and delete mode for user and
+group records. Command line options have been fashioned to be similar
+to those used by the Sun/shadow commands: useradd, usermod, userdel,
+groupadd, groupmod, groupdel, but combines all operations within the
+single command `pw'.
+
+User add mode also provides a means of easily setting system useradd
+defaults (see pw.conf.5), so that adding a user is as easy as issuing
+the command "pw useradd <loginid>". Creation of a unique primary
+group for each user and automatic membership in secondary groups
+is fully supported.
+
+This program may be FreeBSD specific, but should be trivial to port to
+other bsd4.4 variants.
+
+Author and maintainer: David L. Nugent, <davidn@blaze.net.au>
+
+$FreeBSD$
+
diff --git a/usr.sbin/pw/bitmap.c b/usr.sbin/pw/bitmap.c
new file mode 100644
index 0000000..8e96bff
--- /dev/null
+++ b/usr.sbin/pw/bitmap.c
@@ -0,0 +1,131 @@
+/*-
+ * Copyright (C) 1996
+ * David L. Nugent. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT 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 DAVID L. NUGENT 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.
+ */
+
+#ifndef lint
+static const char rcsid[] =
+ "$FreeBSD$";
+#endif /* not lint */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "bitmap.h"
+
+struct bitmap
+bm_alloc(int size)
+{
+ struct bitmap bm;
+ int szmap = (size / 8) + !!(size % 8);
+
+ bm.size = size;
+ bm.map = malloc(szmap);
+ if (bm.map)
+ memset(bm.map, 0, szmap);
+ return bm;
+}
+
+void
+bm_dealloc(struct bitmap * bm)
+{
+ free(bm->map);
+}
+
+static void
+bm_getmask(int *pos, unsigned char *bmask)
+{
+ *bmask = (unsigned char) (1 << (*pos % 8));
+ *pos /= 8;
+}
+
+void
+bm_setbit(struct bitmap * bm, int pos)
+{
+ unsigned char bmask;
+
+ bm_getmask(&pos, &bmask);
+ bm->map[pos] |= bmask;
+}
+
+void
+bm_clrbit(struct bitmap * bm, int pos)
+{
+ unsigned char bmask;
+
+ bm_getmask(&pos, &bmask);
+ bm->map[pos] &= ~bmask;
+}
+
+int
+bm_isset(struct bitmap * bm, int pos)
+{
+ unsigned char bmask;
+
+ bm_getmask(&pos, &bmask);
+ return !!(bm->map[pos] & bmask);
+}
+
+int
+bm_firstunset(struct bitmap * bm)
+{
+ int szmap = (bm->size / 8) + !!(bm->size % 8);
+ int at = 0;
+ int pos = 0;
+
+ while (pos < szmap) {
+ unsigned char bmv = bm->map[pos++];
+ unsigned char bmask = 1;
+
+ while (bmask & 0xff) {
+ if ((bmv & bmask) == 0)
+ return at;
+ bmask <<= 1;
+ ++at;
+ }
+ }
+ return at;
+}
+
+int
+bm_lastset(struct bitmap * bm)
+{
+ int szmap = (bm->size / 8) + !!(bm->size % 8);
+ int at = 0;
+ int pos = 0;
+ int ofs = 0;
+
+ while (pos < szmap) {
+ unsigned char bmv = bm->map[pos++];
+ unsigned char bmask = 1;
+
+ while (bmask & 0xff) {
+ if ((bmv & bmask) != 0)
+ ofs = at;
+ bmask <<= 1;
+ ++at;
+ }
+ }
+ return ofs;
+}
diff --git a/usr.sbin/pw/bitmap.h b/usr.sbin/pw/bitmap.h
new file mode 100644
index 0000000..4d6cfe4
--- /dev/null
+++ b/usr.sbin/pw/bitmap.h
@@ -0,0 +1,50 @@
+/*-
+ * Copyright (C) 1996
+ * David L. Nugent. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT 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 DAVID L. NUGENT 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.
+ *
+ * $FreeBSD$
+ */
+
+#ifndef _BITMAP_H_
+#define _BITMAP_H_
+
+#include <sys/cdefs.h>
+
+struct bitmap
+{
+ int size;
+ unsigned char *map;
+};
+
+__BEGIN_DECLS
+struct bitmap bm_alloc(int size);
+void bm_dealloc(struct bitmap * bm);
+void bm_setbit(struct bitmap * bm, int pos);
+void bm_clrbit(struct bitmap * bm, int pos);
+int bm_isset(struct bitmap * bm, int pos);
+int bm_firstunset(struct bitmap * bm);
+int bm_lastset(struct bitmap * bm);
+__END_DECLS
+
+#endif /* !_BITMAP_H */
diff --git a/usr.sbin/pw/cpdir.c b/usr.sbin/pw/cpdir.c
new file mode 100644
index 0000000..334f789
--- /dev/null
+++ b/usr.sbin/pw/cpdir.c
@@ -0,0 +1,124 @@
+/*-
+ * Copyright (C) 1996
+ * David L. Nugent. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT 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 DAVID L. NUGENT 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.
+ */
+
+#ifndef lint
+static const char rcsid[] =
+ "$FreeBSD$";
+#endif /* not lint */
+
+#include <dirent.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "pw.h"
+#include "pwupd.h"
+
+void
+copymkdir(int rootfd, char const * dir, int skelfd, mode_t mode, uid_t uid,
+ gid_t gid, int flags)
+{
+ char *p, lnk[MAXPATHLEN], copybuf[4096];
+ int len, homefd, srcfd, destfd;
+ ssize_t sz;
+ struct stat st;
+ struct dirent *e;
+ DIR *d;
+
+ if (*dir == '/')
+ dir++;
+
+ if (mkdirat(rootfd, dir, mode) != 0 && errno != EEXIST) {
+ warn("mkdir(%s)", dir);
+ return;
+ }
+ fchownat(rootfd, dir, uid, gid, AT_SYMLINK_NOFOLLOW);
+ if (flags > 0)
+ chflagsat(rootfd, dir, flags, AT_SYMLINK_NOFOLLOW);
+
+ if (skelfd == -1)
+ return;
+
+ homefd = openat(rootfd, dir, O_DIRECTORY);
+ if ((d = fdopendir(skelfd)) == NULL) {
+ close(skelfd);
+ close(homefd);
+ return;
+ }
+
+ while ((e = readdir(d)) != NULL) {
+ if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0)
+ continue;
+
+ p = e->d_name;
+ if (fstatat(skelfd, p, &st, AT_SYMLINK_NOFOLLOW) == -1)
+ continue;
+
+ if (strncmp(p, "dot.", 4) == 0) /* Conversion */
+ p += 3;
+
+ if (S_ISDIR(st.st_mode)) {
+ copymkdir(homefd, p, openat(skelfd, e->d_name, O_DIRECTORY),
+ st.st_mode & _DEF_DIRMODE, uid, gid, st.st_flags);
+ continue;
+ }
+
+ if (S_ISLNK(st.st_mode) &&
+ (len = readlinkat(skelfd, e->d_name, lnk, sizeof(lnk) -1))
+ != -1) {
+ lnk[len] = '\0';
+ symlinkat(lnk, homefd, p);
+ fchownat(homefd, p, uid, gid, AT_SYMLINK_NOFOLLOW);
+ continue;
+ }
+
+ if (!S_ISREG(st.st_mode))
+ continue;
+
+ if ((srcfd = openat(skelfd, e->d_name, O_RDONLY)) == -1)
+ continue;
+ destfd = openat(homefd, p, O_RDWR | O_CREAT | O_EXCL,
+ st.st_mode);
+ if (destfd == -1) {
+ close(srcfd);
+ continue;
+ }
+
+ while ((sz = read(srcfd, copybuf, sizeof(copybuf))) > 0)
+ write(destfd, copybuf, sz);
+
+ close(srcfd);
+ /*
+ * Propagate special filesystem flags
+ */
+ fchown(destfd, uid, gid);
+ fchflags(destfd, st.st_flags);
+ close(destfd);
+ }
+ closedir(d);
+}
diff --git a/usr.sbin/pw/grupd.c b/usr.sbin/pw/grupd.c
new file mode 100644
index 0000000..9cbe0cb
--- /dev/null
+++ b/usr.sbin/pw/grupd.c
@@ -0,0 +1,105 @@
+/*-
+ * Copyright (C) 1996
+ * David L. Nugent. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT 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 DAVID L. NUGENT 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.
+ */
+
+#ifndef lint
+static const char rcsid[] =
+ "$FreeBSD$";
+#endif /* not lint */
+
+#include <err.h>
+#include <grp.h>
+#include <libutil.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "pwupd.h"
+
+char *
+getgrpath(const char * file)
+{
+ static char pathbuf[MAXPATHLEN];
+
+ snprintf(pathbuf, sizeof pathbuf, "%s/%s", conf.etcpath, file);
+
+ return (pathbuf);
+}
+
+static int
+gr_update(struct group * grp, char const * group)
+{
+ int pfd, tfd;
+ struct group *gr = NULL;
+ struct group *old_gr = NULL;
+
+ if (grp != NULL)
+ gr = gr_dup(grp);
+
+ if (group != NULL)
+ old_gr = GETGRNAM(group);
+
+ if (gr_init(conf.etcpath, NULL))
+ err(1, "gr_init()");
+
+ if ((pfd = gr_lock()) == -1) {
+ gr_fini();
+ err(1, "gr_lock()");
+ }
+ if ((tfd = gr_tmp(-1)) == -1) {
+ gr_fini();
+ err(1, "gr_tmp()");
+ }
+ if (gr_copy(pfd, tfd, gr, old_gr) == -1) {
+ gr_fini();
+ err(1, "gr_copy()");
+ }
+ if (gr_mkdb() == -1) {
+ gr_fini();
+ err(1, "gr_mkdb()");
+ }
+ free(gr);
+ gr_fini();
+ return 0;
+}
+
+
+int
+addgrent(struct group * grp)
+{
+ return gr_update(grp, NULL);
+}
+
+int
+chggrent(char const * login, struct group * grp)
+{
+ return gr_update(grp, login);
+}
+
+int
+delgrent(struct group * grp)
+{
+
+ return (gr_update(NULL, grp->gr_name));
+}
diff --git a/usr.sbin/pw/psdate.c b/usr.sbin/pw/psdate.c
new file mode 100644
index 0000000..bd2aa15
--- /dev/null
+++ b/usr.sbin/pw/psdate.c
@@ -0,0 +1,261 @@
+/*-
+ * Copyright (C) 1996
+ * David L. Nugent. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT 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 DAVID L. NUGENT 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.
+ */
+
+#ifndef lint
+static const char rcsid[] =
+ "$FreeBSD$";
+#endif /* not lint */
+
+#include <ctype.h>
+#include <err.h>
+#include <stdlib.h>
+#include <string.h>
+#include <xlocale.h>
+
+#include "psdate.h"
+
+
+static int
+numerics(char const * str)
+{
+ int rc = isdigit((unsigned char)*str);
+
+ if (rc)
+ while (isdigit((unsigned char)*str) || *str == 'x')
+ ++str;
+ return rc && !*str;
+}
+
+static int
+aindex(char const * arr[], char const ** str, int len)
+{
+ int l, i;
+ char mystr[32];
+
+ mystr[len] = '\0';
+ l = strlen(strncpy(mystr, *str, len));
+ for (i = 0; i < l; i++)
+ mystr[i] = (char) tolower((unsigned char)mystr[i]);
+ for (i = 0; arr[i] && strcmp(mystr, arr[i]) != 0; i++);
+ if (arr[i] == NULL)
+ i = -1;
+ else { /* Skip past it */
+ while (**str && isalpha((unsigned char)**str))
+ ++(*str);
+ /* And any following whitespace */
+ while (**str && (**str == ',' || isspace((unsigned char)**str)))
+ ++(*str);
+ } /* Return index */
+ return i;
+}
+
+static int
+weekday(char const ** str)
+{
+ static char const *days[] =
+ {"sun", "mon", "tue", "wed", "thu", "fri", "sat", NULL};
+
+ return aindex(days, str, 3);
+}
+
+static void
+parse_datesub(char const * str, struct tm *t)
+{
+ struct tm tm;
+ locale_t l;
+ int i;
+ char *ret;
+ const char *valid_formats[] = {
+ "%d-%b-%y",
+ "%d-%b-%Y",
+ "%d-%m-%y",
+ "%d-%m-%Y",
+ "%H:%M %d-%b-%y",
+ "%H:%M %d-%b-%Y",
+ "%H:%M %d-%m-%y",
+ "%H:%M %d-%m-%Y",
+ "%H:%M:%S %d-%b-%y",
+ "%H:%M:%S %d-%b-%Y",
+ "%H:%M:%S %d-%m-%y",
+ "%H:%M:%S %d-%m-%Y",
+ "%d-%b-%y %H:%M",
+ "%d-%b-%Y %H:%M",
+ "%d-%m-%y %H:%M",
+ "%d-%m-%Y %H:%M",
+ "%d-%b-%y %H:%M:%S",
+ "%d-%b-%Y %H:%M:%S",
+ "%d-%m-%y %H:%M:%S",
+ "%d-%m-%Y %H:%M:%S",
+ "%H:%M\t%d-%b-%y",
+ "%H:%M\t%d-%b-%Y",
+ "%H:%M\t%d-%m-%y",
+ "%H:%M\t%d-%m-%Y",
+ "%H:%M\t%S %d-%b-%y",
+ "%H:%M\t%S %d-%b-%Y",
+ "%H:%M\t%S %d-%m-%y",
+ "%H:%M\t%S %d-%m-%Y",
+ "%d-%b-%y\t%H:%M",
+ "%d-%b-%Y\t%H:%M",
+ "%d-%m-%y\t%H:%M",
+ "%d-%m-%Y\t%H:%M",
+ "%d-%b-%y\t%H:%M:%S",
+ "%d-%b-%Y\t%H:%M:%S",
+ "%d-%m-%y\t%H:%M:%S",
+ "%d-%m-%Y\t%H:%M:%S",
+ NULL,
+ };
+
+ l = newlocale(LC_ALL_MASK, "C", NULL);
+
+ memset(&tm, 0, sizeof(tm));
+ for (i=0; valid_formats[i] != NULL; i++) {
+ ret = strptime_l(str, valid_formats[i], &tm, l);
+ if (ret && *ret == '\0') {
+ t->tm_mday = tm.tm_mday;
+ t->tm_mon = tm.tm_mon;
+ t->tm_year = tm.tm_year;
+ t->tm_hour = tm.tm_hour;
+ t->tm_min = tm.tm_min;
+ t->tm_sec = tm.tm_sec;
+ freelocale(l);
+ return;
+ }
+ }
+
+ freelocale(l);
+
+ errx(EXIT_FAILURE, "Invalid date");
+}
+
+
+/*-
+ * Parse time must be flexible, it handles the following formats:
+ * nnnnnnnnnnn UNIX timestamp (all numeric), 0 = now
+ * 0xnnnnnnnn UNIX timestamp in hexadecimal
+ * 0nnnnnnnnn UNIX timestamp in octal
+ * 0 Given time
+ * +nnnn[smhdwoy] Given time + nnnn hours, mins, days, weeks, months or years
+ * -nnnn[smhdwoy] Given time - nnnn hours, mins, days, weeks, months or years
+ * dd[ ./-]mmm[ ./-]yy Date }
+ * hh:mm:ss Time } May be combined
+ */
+
+time_t
+parse_date(time_t dt, char const * str)
+{
+ char *p;
+ int i;
+ long val;
+ struct tm *T;
+
+ if (dt == 0)
+ dt = time(NULL);
+
+ while (*str && isspace((unsigned char)*str))
+ ++str;
+
+ if (numerics(str)) {
+ dt = strtol(str, &p, 0);
+ } else if (*str == '+' || *str == '-') {
+ val = strtol(str, &p, 0);
+ switch (*p) {
+ case 'h':
+ case 'H': /* hours */
+ dt += (val * 3600L);
+ break;
+ case '\0':
+ case 'm':
+ case 'M': /* minutes */
+ dt += (val * 60L);
+ break;
+ case 's':
+ case 'S': /* seconds */
+ dt += val;
+ break;
+ case 'd':
+ case 'D': /* days */
+ dt += (val * 86400L);
+ break;
+ case 'w':
+ case 'W': /* weeks */
+ dt += (val * 604800L);
+ break;
+ case 'o':
+ case 'O': /* months */
+ T = localtime(&dt);
+ T->tm_mon += (int) val;
+ i = T->tm_mday;
+ goto fixday;
+ case 'y':
+ case 'Y': /* years */
+ T = localtime(&dt);
+ T->tm_year += (int) val;
+ i = T->tm_mday;
+ fixday:
+ dt = mktime(T);
+ T = localtime(&dt);
+ if (T->tm_mday != i) {
+ T->tm_mday = 1;
+ dt = mktime(T);
+ dt -= (time_t) 86400L;
+ }
+ default: /* unknown */
+ break; /* leave untouched */
+ }
+ } else {
+ char *q, tmp[64];
+
+ /*
+ * Skip past any weekday prefix
+ */
+ weekday(&str);
+ strlcpy(tmp, str, sizeof(tmp));
+ str = tmp;
+ T = localtime(&dt);
+
+ /*
+ * See if we can break off any timezone
+ */
+ while ((q = strrchr(tmp, ' ')) != NULL) {
+ if (strchr("(+-", q[1]) != NULL)
+ *q = '\0';
+ else {
+ int j = 1;
+
+ while (q[j] && isupper((unsigned char)q[j]))
+ ++j;
+ if (q[j] == '\0')
+ *q = '\0';
+ else
+ break;
+ }
+ }
+
+ parse_datesub(tmp, T);
+ dt = mktime(T);
+ }
+ return dt;
+}
diff --git a/usr.sbin/pw/psdate.h b/usr.sbin/pw/psdate.h
new file mode 100644
index 0000000..a1e99d4
--- /dev/null
+++ b/usr.sbin/pw/psdate.h
@@ -0,0 +1,40 @@
+/*-
+ * Copyright (C) 1996
+ * David L. Nugent. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT 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 DAVID L. NUGENT 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.
+ *
+ * $FreeBSD$
+ */
+
+#ifndef _PSDATE_H_
+#define _PSDATE_H_
+
+#include <time.h>
+#include <sys/cdefs.h>
+
+__BEGIN_DECLS
+time_t parse_date(time_t dt, char const * str);
+void print_date(char *buf, time_t t, int dotime);
+__END_DECLS
+
+#endif /* !_PSDATE_H_ */
diff --git a/usr.sbin/pw/pw.8 b/usr.sbin/pw/pw.8
new file mode 100644
index 0000000..3a9c0b0
--- /dev/null
+++ b/usr.sbin/pw/pw.8
@@ -0,0 +1,1049 @@
+.\" Copyright (C) 1996
+.\" David L. Nugent. All rights reserved.
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions
+.\" are met:
+.\" 1. Redistributions of source code must retain the above copyright
+.\" notice, this list of conditions and the following disclaimer.
+.\" 2. Redistributions in binary form must reproduce the above copyright
+.\" notice, this list of conditions and the following disclaimer in the
+.\" documentation and/or other materials provided with the distribution.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT 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 DAVID L. NUGENT 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.
+.\"
+.\" $FreeBSD$
+.\"
+.Dd June 14, 2015
+.Dt PW 8
+.Os
+.Sh NAME
+.Nm pw
+.Nd create, remove, modify & display system users and groups
+.Sh SYNOPSIS
+.Nm
+.Op Fl R Ar rootdir
+.Op Fl V Ar etcdir
+.Ar useradd
+.Oo Fl n Oc name Oo Fl u Ar uid Oc
+.Op Fl C Ar config
+.Op Fl q
+.Op Fl c Ar comment
+.Op Fl d Ar dir
+.Op Fl e Ar date
+.Op Fl p Ar date
+.Op Fl g Ar group
+.Op Fl G Ar grouplist
+.Op Fl m
+.Op Fl M Ar mode
+.Op Fl k Ar dir
+.Op Fl w Ar method
+.Op Fl s Ar shell
+.Op Fl o
+.Op Fl L Ar class
+.Op Fl h Ar fd | Fl H Ar fd
+.Op Fl N
+.Op Fl P
+.Op Fl Y
+.Nm
+.Op Fl R Ar rootdir
+.Op Fl V Ar etcdir
+.Ar useradd
+.Fl D
+.Op Fl C Ar config
+.Op Fl q
+.Op Fl b Ar dir
+.Op Fl e Ar days
+.Op Fl p Ar days
+.Op Fl g Ar group
+.Op Fl G Ar grouplist
+.Op Fl k Ar dir
+.Op Fl M Ar mode
+.Op Fl u Ar min , Ns Ar max
+.Op Fl i Ar min , Ns Ar max
+.Op Fl w Ar method
+.Op Fl s Ar shell
+.Op Fl y Ar path
+.Nm
+.Op Fl R Ar rootdir
+.Op Fl V Ar etcdir
+.Ar userdel
+.Oo Fl n Oc name|uid | Fl u Ar uid
+.Op Fl r
+.Op Fl Y
+.Nm
+.Op Fl R Ar rootdir
+.Op Fl V Ar etcdir
+.Ar usermod
+.Oo Fl n Oc name|uid Oo Fl u Ar newuid Oc | Fl u Ar uid
+.Op Fl C Ar config
+.Op Fl q
+.Op Fl c Ar comment
+.Op Fl d Ar dir
+.Op Fl e Ar date
+.Op Fl p Ar date
+.Op Fl g Ar group
+.Op Fl G Ar grouplist
+.Op Fl l Ar newname
+.Op Fl m
+.Op Fl M Ar mode
+.Op Fl k Ar dir
+.Op Fl w Ar method
+.Op Fl s Ar shell
+.Op Fl L Ar class
+.Op Fl h Ar fd | Fl H Ar fd
+.Op Fl N
+.Op Fl P
+.Op Fl Y
+.Nm
+.Op Fl R Ar rootdir
+.Op Fl V Ar etcdir
+.Ar usershow
+.Oo Fl n Oc name|uid | Fl u Ar uid
+.Op Fl F
+.Op Fl P
+.Op Fl 7
+.Op Fl a
+.Nm
+.Op Fl R Ar rootdir
+.Op Fl V Ar etcdir
+.Ar usernext
+.Op Fl C Ar config
+.Op Fl q
+.Nm
+.Op Fl R Ar rootdir
+.Op Fl V Ar etcdir
+.Ar groupadd
+.Oo Fl n Oc name Oo Fl g Ar gid Oc
+.Op Fl C Ar config
+.Op Fl q
+.Op Fl M Ar members
+.Op Fl o
+.Op Fl h Ar fd | Fl H Ar fd
+.Op Fl N
+.Op Fl P
+.Op Fl Y
+.Nm
+.Op Fl R Ar rootdir
+.Op Fl V Ar etcdir
+.Ar groupdel
+.Oo Fl n Oc name|gid | Fl g Ar gid
+.Op Fl Y
+.Nm
+.Op Fl R Ar rootdir
+.Op Fl V Ar etcdir
+.Ar groupmod
+.Oo Fl n Oc name|gid Oo Fl g Ar newgid Oc | Fl g Ar gid
+.Op Fl C Ar config
+.Op Fl q
+.Op Fl l Ar newname
+.Op Fl M Ar members
+.Op Fl m Ar newmembers
+.Op Fl d Ar oldmembers
+.Op Fl h Ar fd | Fl H Ar fd
+.Op Fl N
+.Op Fl P
+.Op Fl Y
+.Nm
+.Op Fl R Ar rootdir
+.Op Fl V Ar etcdir
+.Ar groupshow
+.Oo Fl n Oc name|gid | Fl g Ar gid
+.Op Fl F
+.Op Fl P
+.Op Fl a
+.Nm
+.Op Fl R Ar rootdir
+.Op Fl V Ar etcdir
+.Ar groupnext
+.Op Fl C Ar config
+.Op Fl q
+.Nm
+.Op Fl R Ar rootdir
+.Op Fl V Ar etcdir
+.Ar lock
+.Oo Fl n Oc name|uid | Fl u Ar uid
+.Op Fl C Ar config
+.Op Fl q
+.Nm
+.Op Fl R Ar rootdir
+.Op Fl V Ar etcdir
+.Ar unlock
+.Oo Fl n Oc name|uid | Fl u Ar uid
+.Op Fl C Ar config
+.Op Fl q
+.Sh DESCRIPTION
+The
+.Nm
+utility is a command-line based editor for the system
+.Ar user
+and
+.Ar group
+files, allowing the superuser an easy to use and standardized way of adding,
+modifying and removing users and groups.
+Note that
+.Nm
+only operates on the local user and group files.
+.Tn NIS
+users and groups must be
+maintained on the
+.Tn NIS
+server.
+The
+.Nm
+utility handles updating the
+.Pa passwd ,
+.Pa master.passwd ,
+.Pa group
+and the secure and insecure
+password database files, and must be run as root.
+.Pp
+The first one or two keywords provided to
+.Nm
+on the command line provide the context for the remainder of the arguments.
+The keywords
+.Ar user
+and
+.Ar group
+may be combined with
+.Ar add ,
+.Ar del ,
+.Ar mod ,
+.Ar show ,
+or
+.Ar next
+in any order.
+(For example,
+.Ar showuser ,
+.Ar usershow ,
+.Ar show user ,
+and
+.Ar user show
+all mean the same thing.)
+This flexibility is useful for interactive scripts calling
+.Nm
+for user and group database manipulation.
+Following these keywords,
+the user or group name or numeric id may be optionally specified as an
+alternative to using the
+.Fl n Ar name ,
+.Fl u Ar uid ,
+.Fl g Ar gid
+options.
+.Pp
+The following flags are common to most or all modes of operation:
+.Bl -tag -width "-G grouplist"
+.It Fl R Ar rootdir
+Specifies an alternate root directory within which
+.Nm
+will operate.
+Any paths specified will be relative to
+.Va rootdir .
+.It Fl V Ar etcdir
+Set an alternate location for the password, group, and configuration files.
+Can be used to maintain a user/group database in an alternate location.
+If this switch is specified, the system
+.Pa /etc/pw.conf
+will not be sourced for default configuration data,
+but the file pw.conf in the specified directory will be used instead
+.Pq or none, if it does not exist .
+The
+.Fl C
+flag may be used to override this behaviour.
+As an exception to the general rule where options must follow the operation
+type, the
+.Fl V
+flag must be used on the command line before the operation keyword.
+.It Fl C Ar config
+By default,
+.Nm
+reads the file
+.Pa /etc/pw.conf
+to obtain policy information on how new user accounts and groups are to be created.
+The
+.Fl C
+option specifies a different configuration file.
+While most of the contents of the configuration file may be overridden via
+command-line options, it may be more convenient to keep standard information in a
+configuration file.
+.It Fl q
+Use of this option causes
+.Nm
+to suppress error messages,
+which may be useful in interactive environments where it
+is preferable to interpret status codes returned by
+.Nm
+rather than messing up a carefully formatted display.
+.It Fl N
+This option is available in
+.Ar add
+and
+.Ar modify
+operations, and tells
+.Nm
+to output the result of the operation without updating the user or group
+databases.
+You may use the
+.Fl P
+option to switch between standard passwd and readable formats.
+.It Fl Y
+Using this option with any of the update modes causes
+.Nm
+to run
+.Xr make 1
+after changing to the directory
+.Pa /var/yp .
+This is intended to allow automatic updating of
+.Tn NIS
+database files.
+If separate passwd and group files are being used by
+.Tn NIS ,
+then use the
+.Fl y Ar path
+option to specify the location of the
+.Tn NIS
+passwd database so that
+.Nm
+will concurrently update it with the system password
+databases.
+.El
+.Sh USER OPTIONS
+The following options apply to the
+.Ar useradd
+and
+.Ar usermod
+commands:
+.Bl -tag -width "-G grouplist"
+.It Oo Fl n Oc Ar name
+Required unless
+.Fl u Ar uid
+is given.
+Specify the user/account name.
+In the case of
+.Ar usermod
+can be a uid.
+.It Fl u Ar uid
+Required if
+.Ar name
+is not given.
+Specify the user/account numeric id.
+In the case of
+.Ar usermod
+if paired with
+.Ar name ,
+changes the numeric id of the named user/account.
+.Pp
+Usually, only one of these options is required,
+as the account name will imply the uid, or vice versa.
+However, there are times when both are needed.
+For example, when changing the uid of an existing user with
+.Ar usermod ,
+or overriding the default uid when creating a new account with
+.Ar useradd .
+To automatically allocate the uid to a new user with
+.Ar useradd ,
+then do
+.Em not
+use the
+.Fl u
+option.
+Either the account or userid can also be provided immediately after the
+.Ar useradd ,
+.Ar userdel ,
+.Ar usermod
+or
+.Ar usershow
+keywords on the command line without using the
+.Fl n
+or
+.Fl u
+options.
+.El
+.Bl -tag -width "-G grouplist"
+.It Fl c Ar comment
+This field sets the contents of the passwd GECOS field,
+which normally contains up to four comma-separated fields containing the
+user's full name, office or location,
+and work and home phone numbers.
+These sub-fields are used by convention only, however, and are optional.
+If this field is to contain spaces,
+the comment must be enclosed in double quotes
+.Ql \&" .
+Avoid using commas in this field as these are used as sub-field separators,
+and the colon
+.Ql \&:
+character also cannot be used as this is the field separator for the passwd
+file itself.
+.It Fl d Ar dir
+This option sets the account's home directory.
+Normally,
+this is only used if the home directory is to be different from the
+default determined from
+.Pa /etc/pw.conf
+- normally
+.Pa /home
+with the account name as a subdirectory.
+.It Fl e Ar date
+Set the account's expiration date.
+Format of the date is either a UNIX time in decimal, or a date in
+.Ql dd-mmm-yy[yy]
+format, where dd is the day,
+mmm is the month, either in numeric or alphabetic format
+('Jan', 'Feb', etc) and year is either a two or four digit year.
+This option also accepts a relative date in the form
+.Ql \&+n[mhdwoy]
+where
+.Ql \&n
+is a decimal,
+octal (leading 0) or hexadecimal (leading 0x) digit followed by the
+number of Minutes, Hours, Days, Weeks, Months or Years from the current date at
+which the expiration date is to be set.
+.It Fl p Ar date
+Set the account's password expiration date.
+This field is similar to the account expiration date option, except that it
+applies to forced password changes.
+This is set in the same manner as the
+.Fl e
+option.
+.It Fl g Ar group
+Set the account's primary group to the given group.
+.Ar group
+may be defined by either its name or group number.
+.It Fl G Ar grouplist
+Set additional group memberships for an account.
+.Ar grouplist
+is a comma, space or tab-separated list of group names or group numbers.
+The user's name is added to the group lists in
+.Pa /etc/group ,
+and
+removed from any groups not specified in
+.Ar grouplist .
+Note: a user should not be added to their primary group with
+.Ar grouplist .
+Also, group membership changes do not take effect for current user login
+sessions, requiring the user to reconnect to be affected by the changes.
+.It Fl L Ar class
+This option sets the login class for the user being created.
+See
+.Xr login.conf 5
+and
+.Xr passwd 5
+for more information on user login classes.
+.It Fl m
+This option instructs
+.Nm
+to attempt to create the user's home directory.
+While primarily useful when adding a new account with
+.Ar useradd ,
+this may also be of use when moving an existing user's home directory elsewhere
+on the file system.
+The new home directory is populated with the contents of the
+.Ar skeleton
+directory, which typically contains a set of shell configuration files that the
+user may personalize to taste.
+Files in this directory are usually named
+.Pa dot . Ns Aq Ar config
+where the
+.Pa dot
+prefix will be stripped.
+When
+.Fl m
+is used on an account with
+.Ar usermod ,
+existing configuration files in the user's home directory are
+.Em not
+overwritten from the skeleton files.
+.Pp
+When a user's home directory is created,
+it will by default be a subdirectory of the
+.Ar basehome
+directory as specified by the
+.Fl b
+option (see below), bearing the name of the new account.
+This can be overridden by the
+.Fl d
+option on the command line, if desired.
+.It Fl M Ar mode
+Create the user's home directory with the specified
+.Ar mode ,
+modified by the current
+.Xr umask 2 .
+If omitted, it is derived from the parent process'
+.Xr umask 2 .
+This option is only useful in combination with the
+.Fl m
+flag.
+.It Fl k Ar dir
+Set the
+.Ar skeleton
+directory, from which basic startup and configuration files are copied when
+the user's home directory is created.
+This option only has meaning when used with the
+.Fl d
+or
+.Fl m
+flags.
+.It Fl s Ar shell
+Set or changes the user's login shell to
+.Ar shell .
+If the path to the shell program is omitted,
+.Nm
+searches the
+.Ar shellpath
+specified in
+.Pa /etc/pw.conf
+and fills it in as appropriate.
+Note that unless you have a specific reason to do so, you should avoid
+specifying the path - this will allow
+.Nm
+to validate that the program exists and is executable.
+Specifying a full path (or supplying a blank "" shell) avoids this check
+and allows for such entries as
+.Pa /nonexistent
+that should be set for accounts not intended for interactive login.
+.It Fl h Ar fd
+This option provides a special interface by which interactive scripts can
+set an account password using
+.Nm .
+Because the command line and environment are fundamentally insecure mechanisms
+by which programs can accept information,
+.Nm
+will only allow setting of account and group passwords via a file descriptor
+(usually a pipe between an interactive script and the program).
+.Ar sh ,
+.Ar bash ,
+.Ar ksh
+and
+.Ar perl
+all possess mechanisms by which this can be done.
+Alternatively,
+.Nm
+will prompt for the user's password if
+.Fl h Ar 0
+is given, nominating
+.Em stdin
+as the file descriptor on which to read the password.
+Note that this password will be read only once and is intended
+for use by a script rather than for interactive use.
+If you wish to have new password confirmation along the lines of
+.Xr passwd 1 ,
+this must be implemented as part of an interactive script that calls
+.Nm .
+.Pp
+If a value of
+.Ql \&-
+is given as the argument
+.Ar fd ,
+then the password will be set to
+.Ql \&* ,
+rendering the account inaccessible via password-based login.
+.It Fl H Ar fd
+Read an encrypted password string from the specified file descriptor.
+This is like
+.Fl h ,
+but the password should be supplied already encrypted in a form
+suitable for writing directly to the password database.
+.El
+.Pp
+It is possible to use
+.Ar useradd
+to create a new account that duplicates an existing user id.
+While this is normally considered an error and will be rejected, the
+.Fl o
+option overrides the check for duplicates and allows the duplication of
+the user id.
+This may be useful if you allow the same user to login under
+different contexts (different group allocations, different home
+directory, different shell) while providing basically the same
+permissions for access to the user's files in each account.
+.Pp
+The
+.Ar useradd
+command also has the ability to set new user and group defaults by using the
+.Fl D
+option.
+Instead of adding a new user,
+.Nm
+writes a new set of defaults to its configuration file,
+.Pa /etc/pw.conf .
+When using the
+.Fl D
+option, you must not use either
+.Fl n Ar name
+or
+.Fl u Ar uid
+or an error will result.
+Use of
+.Fl D
+changes the meaning of several command line switches in the
+.Ar useradd
+command.
+These are:
+.Bl -tag -width "-G grouplist"
+.It Fl D
+Set default values in
+.Pa /etc/pw.conf
+configuration file, or a different named configuration file if the
+.Fl C Ar config
+option is used.
+.It Fl b Ar dir
+Set the root directory in which user home directories are created.
+The default value for this is
+.Pa /home ,
+but it may be set elsewhere as desired.
+.It Fl e Ar days
+Set the default account expiration period in days.
+When
+.Fl D
+is used, the
+.Ar days
+argument is interpreted differently.
+It must be numeric and represents the number of days after creation
+that the account expires.
+A value of 0 suppresses automatic calculation of the expiry date.
+.It Fl p Ar days
+Set the default password expiration period in days.
+.It Fl g Ar group
+Set the default group for new users.
+If a blank group is specified using
+.Fl g Ar \&"" ,
+then new users will be allocated their own private primary group
+with the same name as their login name.
+If a group is supplied, either its name or uid may be given as an argument.
+.It Fl G Ar grouplist
+Set the default groups in which new users are granted membership.
+This is a separate set of groups from the primary group.
+Avoid nominating the same group as both primary and extra groups.
+In other words, these extra groups determine membership in groups
+.Em other than
+the primary group.
+.Ar grouplist
+is a comma-separated list of group names or ids, and are always
+stored in
+.Pa /etc/pw.conf
+by their symbolic names.
+.It Fl L Ar class
+This option sets the default login class for new users.
+.It Fl k Ar dir
+Set the default
+.Em skeleton
+directory,
+from which prototype shell and other initialization files are copied when
+.Nm
+creates a user's home directory.
+See description of
+.Fl k
+for naming conventions of these files.
+.It Xo
+.Fl u Ar min , Ns Ar max ,
+.Fl i Ar min , Ns Ar max
+.Xc
+Set the minimum and maximum user and group ids allocated for new
+accounts and groups created by
+.Nm .
+The default values for each is 1000 minimum and 32000 maximum.
+.Ar min
+and
+.Ar max
+are both numbers, where max must be greater than min,
+and both must be between 0 and 32767.
+In general,
+user and group ids less than 100 are reserved for use by the system,
+and numbers greater than 32000 may also be reserved for special purposes
+.Pq used by some system daemons .
+.It Fl w Ar method
+The
+.Fl w
+option selects the default method used to set passwords for newly created user
+accounts.
+.Ar method
+is one of:
+.Pp
+.Bl -tag -width random -offset indent -compact
+.It no
+disable login on newly created accounts
+.It yes
+force the password to be the account name
+.It none
+force a blank password
+.It random
+generate a random password
+.El
+.Pp
+The
+.Ql \&random
+or
+.Ql \&no
+methods are the most secure; in the former case,
+.Nm
+generates a password and prints it to stdout,
+which is suitable when users are issued passwords rather than being allowed
+to select their own
+.Pq possibly poorly chosen
+password.
+The
+.Ql \&no
+method requires that the superuser use
+.Xr passwd 1
+to render the account accessible with a password.
+.It Fl y Ar path
+This sets the pathname of the database used by
+.Tn NIS
+if you are not sharing
+the information from
+.Pa /etc/master.passwd
+directly with
+.Tn NIS .
+You should only set this option for
+.Tn NIS
+servers.
+.El
+.Pp
+The
+.Ar userdel
+command has three distinct options.
+The
+.Fl n Ar name
+and
+.Fl u Ar uid
+options have already been covered above.
+The additional option is:
+.Bl -tag -width "-G grouplist"
+.It Fl r
+This tells
+.Nm
+to remove the user's home directory and all of its contents.
+The
+.Nm
+utility errs on the side of caution when removing files from the system.
+Firstly,
+it will not do so if the uid of the account being removed is also used by
+another account on the system, and the 'home' directory in the password file is
+a valid path that commences with the character
+.Ql \&/ .
+Secondly, it will only remove files and directories that are actually owned by
+the user, or symbolic links owned by anyone under the user's home directory.
+Finally, after deleting all contents owned by the user only empty directories
+will be removed.
+If any additional cleanup work is required, this is left to the administrator.
+.El
+.Pp
+Mail spool files and crontabs are always removed when an account is deleted as
+these are unconditionally attached to the user name.
+Jobs queued for processing by
+.Ar at
+are also removed if the user's uid is unique and not also used by another
+account on the system.
+.Pp
+The
+.Ar usermod
+command adds one additional option:
+.Bl -tag -width "-G grouplist"
+.It Fl l Ar newname
+This option allows changing of an existing account name to
+.Ql \&newname .
+The new name must not already exist, and any attempt to duplicate an
+existing account name will be rejected.
+.El
+.Pp
+The
+.Ar usershow
+command allows viewing of an account in one of two formats.
+By default, the format is identical to the format used in
+.Pa /etc/master.passwd
+with the password field replaced with a
+.Ql \&* .
+If the
+.Fl P
+option is used, then
+.Nm
+outputs the account details in a more human readable form.
+If the
+.Fl 7
+option is used, the account details are shown in v7 format.
+The
+.Fl a
+option lists all users currently on file.
+Using
+.Fl F
+forces
+.Nm
+to print the details of an account even if it does not exist.
+.Pp
+The command
+.Ar usernext
+returns the next available user and group ids separated by a colon.
+This is normally of interest only to interactive scripts or front-ends
+that use
+.Nm .
+.Sh GROUP OPTIONS
+The
+.Fl C
+and
+.Fl q
+options (explained at the start of the previous section) are available
+with the group manipulation commands.
+Other common options to all group-related commands are:
+.Bl -tag -width "-m newmembers"
+.It Oo Fl n Oc Ar name
+Required unless
+.Fl g Ar gid
+is given.
+Specify the group name.
+In the case of
+.Ar groupmod
+can be a gid.
+.It Fl g Ar gid
+Required if
+.Ar name
+is not given.
+Specify the group numeric id.
+In the case of
+.Ar groupmod
+if paired with
+.Ar name ,
+changes the numeric id of the named group.
+.Pp
+As with the account name and id fields, you will usually only need
+to supply one of these, as the group name implies the uid and vice
+versa.
+You will only need to use both when setting a specific group id
+against a new group or when changing the uid of an existing group.
+.It Fl M Ar memberlist
+This option provides an alternative way to add existing users to a
+new group (in groupadd) or replace an existing membership list (in
+groupmod).
+.Ar memberlist
+is a comma separated list of valid and existing user names or uids.
+.It Fl m Ar newmembers
+Similar to
+.Fl M ,
+this option allows the
+.Em addition
+of existing users to a group without replacing the existing list of
+members.
+Login names or user ids may be used, and duplicate users are
+silently eliminated.
+.It Fl d Ar oldmembers
+Similar to
+.Fl M ,
+this option allows the
+.Em deletion
+of existing users from a group without replacing the existing list of
+members.
+Login names or user ids may be used, and duplicate users are
+silently eliminated.
+.El
+.Pp
+.Ar groupadd
+also has a
+.Fl o
+option that allows allocation of an existing group id to a new group.
+The default action is to reject an attempt to add a group,
+and this option overrides the check for duplicate group ids.
+There is rarely any need to duplicate a group id.
+.Pp
+The
+.Ar groupmod
+command adds one additional option:
+.Bl -tag -width "-m newmembers"
+.It Fl l Ar newname
+This option allows changing of an existing group name to
+.Ql \&newname .
+The new name must not already exist,
+and any attempt to duplicate an existing group
+name will be rejected.
+.El
+.Pp
+Options for
+.Ar groupshow
+are the same as for
+.Ar usershow ,
+with the
+.Fl g Ar gid
+replacing
+.Fl u Ar uid
+to specify the group id.
+The
+.Fl 7
+option does not apply to the
+.Ar groupshow
+command.
+.Pp
+The command
+.Ar groupnext
+returns the next available group id on standard output.
+.Sh USER LOCKING
+The
+.Nm
+utility
+supports a simple password locking mechanism for users; it works by
+prepending the string
+.Ql *LOCKED*
+to the beginning of the password field in
+.Pa master.passwd
+to prevent successful authentication.
+.Pp
+The
+.Ar lock
+and
+.Ar unlock
+commands take a user name or uid of the account to lock or unlock,
+respectively.
+The
+.Fl V ,
+.Fl C ,
+and
+.Fl q
+options as described above are accepted by these commands.
+.Sh NOTES
+For a summary of options available with each command, you can use
+.Dl pw [command] help
+For example,
+.Dl pw useradd help
+lists all available options for the useradd operation.
+.Pp
+The
+.Nm
+utility allows 8-bit characters in the passwd GECOS field (user's full name,
+office, work and home phone number subfields), but disallows them in
+user login and group names.
+Use 8-bit characters with caution, as connection to the Internet will
+require that your mail transport program supports 8BITMIME, and will
+convert headers containing 8-bit characters to 7-bit quoted-printable
+format.
+.Xr sendmail 8
+does support this.
+Use of 8-bit characters in the GECOS field should be used in
+conjunction with the user's default locale and character set
+and should not be implemented without their use.
+Using 8-bit characters may also affect other
+programs that transmit the contents of the GECOS field over the
+Internet, such as
+.Xr fingerd 8 ,
+and a small number of TCP/IP clients, such as IRC, where full names
+specified in the passwd file may be used by default.
+.Pp
+The
+.Nm
+utility writes a log to the
+.Pa /var/log/userlog
+file when actions such as user or group additions or deletions occur.
+The location of this logfile can be changed in
+.Xr pw.conf 5 .
+.Sh FILES
+.Bl -tag -width /etc/master.passwd.new -compact
+.It Pa /etc/master.passwd
+The user database
+.It Pa /etc/passwd
+A Version 7 format password file
+.It Pa /etc/login.conf
+The user capabilities database
+.It Pa /etc/group
+The group database
+.It Pa /etc/pw.conf
+Pw default options file
+.It Pa /var/log/userlog
+User/group modification logfile
+.El
+.Sh EXIT STATUS
+The
+.Nm
+utility returns EXIT_SUCCESS on successful operation, otherwise
+.Nm
+returns one of the
+following exit codes defined by
+.Xr sysexits 3
+as follows:
+.Bl -tag -width xxxx
+.It EX_USAGE
+.Bl -bullet -compact
+.It
+Command line syntax errors (invalid keyword, unknown option).
+.El
+.It EX_NOPERM
+.Bl -bullet -compact
+.It
+Attempting to run one of the update modes as non-root.
+.El
+.It EX_OSERR
+.Bl -bullet -compact
+.It
+Memory allocation error.
+.It
+Read error from password file descriptor.
+.El
+.It EX_DATAERR
+.Bl -bullet -compact
+.It
+Bad or invalid data provided or missing on the command line or
+via the password file descriptor.
+.It
+Attempted to remove, rename root account or change its uid.
+.El
+.It EX_OSFILE
+.Bl -bullet -compact
+.It
+Skeleton directory is invalid or does not exist.
+.It
+Base home directory is invalid or does not exist.
+.It
+Invalid or non-existent shell specified.
+.El
+.It EX_NOUSER
+.Bl -bullet -compact
+.It
+User, user id, group or group id specified does not exist.
+.It
+User or group recorded, added, or modified unexpectedly disappeared.
+.El
+.It EX_SOFTWARE
+.Bl -bullet -compact
+.It
+No more group or user ids available within specified range.
+.El
+.It EX_IOERR
+.Bl -bullet -compact
+.It
+Unable to rewrite configuration file.
+.It
+Error updating group or user database files.
+.It
+Update error for passwd or group database files.
+.El
+.It EX_CONFIG
+.Bl -bullet -compact
+.It
+No base home directory configured.
+.El
+.El
+.Sh SEE ALSO
+.Xr chpass 1 ,
+.Xr passwd 1 ,
+.Xr umask 2 ,
+.Xr group 5 ,
+.Xr login.conf 5 ,
+.Xr passwd 5 ,
+.Xr pw.conf 5 ,
+.Xr pwd_mkdb 8 ,
+.Xr vipw 8
+.Sh HISTORY
+The
+.Nm
+utility was written to mimic many of the options used in the SYSV
+.Em shadow
+support suite, but is modified for passwd and group fields specific to
+the
+.Bx 4.4
+operating system, and combines all of the major elements
+into a single command.
diff --git a/usr.sbin/pw/pw.c b/usr.sbin/pw/pw.c
new file mode 100644
index 0000000..700a7b2
--- /dev/null
+++ b/usr.sbin/pw/pw.c
@@ -0,0 +1,381 @@
+/*-
+ * Copyright (C) 1996
+ * David L. Nugent. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT 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 DAVID L. NUGENT 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.
+ */
+
+#ifndef lint
+static const char rcsid[] =
+ "$FreeBSD$";
+#endif /* not lint */
+
+#include <err.h>
+#include <fcntl.h>
+#include <locale.h>
+#include <string.h>
+#include <sysexits.h>
+#include <unistd.h>
+
+#include "pw.h"
+
+const char *Modes[] = {
+ "add", "del", "mod", "show", "next",
+ NULL};
+const char *Which[] = {"user", "group", NULL};
+static const char *Combo1[] = {
+ "useradd", "userdel", "usermod", "usershow", "usernext",
+ "lock", "unlock",
+ "groupadd", "groupdel", "groupmod", "groupshow", "groupnext",
+ NULL};
+static const char *Combo2[] = {
+ "adduser", "deluser", "moduser", "showuser", "nextuser",
+ "lock", "unlock",
+ "addgroup", "delgroup", "modgroup", "showgroup", "nextgroup",
+ NULL};
+
+struct pwf PWF =
+{
+ PWF_REGULAR,
+ setpwent,
+ endpwent,
+ getpwent,
+ getpwuid,
+ getpwnam,
+ setgrent,
+ endgrent,
+ getgrent,
+ getgrgid,
+ getgrnam,
+
+};
+struct pwf VPWF =
+{
+ PWF_ALT,
+ vsetpwent,
+ vendpwent,
+ vgetpwent,
+ vgetpwuid,
+ vgetpwnam,
+ vsetgrent,
+ vendgrent,
+ vgetgrent,
+ vgetgrgid,
+ vgetgrnam,
+};
+
+static int (*cmdfunc[W_NUM][M_NUM])(int argc, char **argv, char *_name) = {
+ { /* user */
+ pw_user_add,
+ pw_user_del,
+ pw_user_mod,
+ pw_user_show,
+ pw_user_next,
+ pw_user_lock,
+ pw_user_unlock,
+ },
+ { /* group */
+ pw_group_add,
+ pw_group_del,
+ pw_group_mod,
+ pw_group_show,
+ pw_group_next,
+ }
+};
+
+struct pwconf conf;
+
+static int getindex(const char *words[], const char *word);
+static void cmdhelp(int mode, int which);
+
+int
+main(int argc, char *argv[])
+{
+ int mode = -1, which = -1, tmp;
+ struct stat st;
+ char arg, *arg1;
+ bool relocated, nis;
+
+ arg1 = NULL;
+ relocated = nis = false;
+ memset(&conf, 0, sizeof(conf));
+ strlcpy(conf.rootdir, "/", sizeof(conf.rootdir));
+ strlcpy(conf.etcpath, _PATH_PWD, sizeof(conf.etcpath));
+ conf.fd = -1;
+ conf.checkduplicate = true;
+
+ setlocale(LC_ALL, "");
+
+ /*
+ * Break off the first couple of words to determine what exactly
+ * we're being asked to do
+ */
+ while (argc > 1) {
+ if (*argv[1] == '-') {
+ /*
+ * Special case, allow pw -V<dir> <operation> [args] for scripts etc.
+ */
+ arg = argv[1][1];
+ if (arg == 'V' || arg == 'R') {
+ if (relocated)
+ errx(EXIT_FAILURE, "Both '-R' and '-V' "
+ "specified, only one accepted");
+ relocated = true;
+ optarg = &argv[1][2];
+ if (*optarg == '\0') {
+ if (stat(argv[2], &st) != 0)
+ errx(EX_OSFILE, \
+ "no such directory `%s'",
+ argv[2]);
+ if (!S_ISDIR(st.st_mode))
+ errx(EX_OSFILE, "`%s' not a "
+ "directory", argv[2]);
+ optarg = argv[2];
+ ++argv;
+ --argc;
+ }
+ memcpy(&PWF, &VPWF, sizeof PWF);
+ if (arg == 'R') {
+ strlcpy(conf.rootdir, optarg,
+ sizeof(conf.rootdir));
+ PWF._altdir = PWF_ROOTDIR;
+ }
+ snprintf(conf.etcpath, sizeof(conf.etcpath),
+ "%s%s", optarg, arg == 'R' ? "/etc" : "");
+ } else
+ break;
+ }
+ else if (mode == -1 && (tmp = getindex(Modes, argv[1])) != -1)
+ mode = tmp;
+ else if (which == -1 && (tmp = getindex(Which, argv[1])) != -1)
+ which = tmp;
+ else if ((mode == -1 && which == -1) &&
+ ((tmp = getindex(Combo1, argv[1])) != -1 ||
+ (tmp = getindex(Combo2, argv[1])) != -1)) {
+ which = tmp / M_NUM;
+ mode = tmp % M_NUM;
+ } else if (strcmp(argv[1], "help") == 0 && argv[2] == NULL)
+ cmdhelp(mode, which);
+ else if (which != -1 && mode != -1)
+ arg1 = argv[1];
+ else
+ errx(EX_USAGE, "unknown keyword `%s'", argv[1]);
+ ++argv;
+ --argc;
+ }
+
+ /*
+ * Bail out unless the user is specific!
+ */
+ if (mode == -1 || which == -1)
+ cmdhelp(mode, which);
+
+ conf.rootfd = open(conf.rootdir, O_DIRECTORY|O_CLOEXEC);
+ if (conf.rootfd == -1)
+ errx(EXIT_FAILURE, "Unable to open '%s'", conf.rootdir);
+
+ return (cmdfunc[which][mode](argc, argv, arg1));
+}
+
+
+static int
+getindex(const char *words[], const char *word)
+{
+ int i = 0;
+
+ while (words[i]) {
+ if (strcmp(words[i], word) == 0)
+ return (i);
+ i++;
+ }
+ return (-1);
+}
+
+
+/*
+ * This is probably an overkill for a cmdline help system, but it reflects
+ * the complexity of the command line.
+ */
+
+static void
+cmdhelp(int mode, int which)
+{
+ if (which == -1)
+ fprintf(stderr, "usage:\n pw [user|group|lock|unlock] [add|del|mod|show|next] [help|switches/values]\n");
+ else if (mode == -1)
+ fprintf(stderr, "usage:\n pw %s [add|del|mod|show|next] [help|switches/values]\n", Which[which]);
+ else {
+
+ /*
+ * We need to give mode specific help
+ */
+ static const char *help[W_NUM][M_NUM] =
+ {
+ {
+ "usage: pw useradd [name] [switches]\n"
+ "\t-V etcdir alternate /etc location\n"
+ "\t-R rootdir alternate root directory\n"
+ "\t-C config configuration file\n"
+ "\t-q quiet operation\n"
+ " Adding users:\n"
+ "\t-n name login name\n"
+ "\t-u uid user id\n"
+ "\t-c comment user name/comment\n"
+ "\t-d directory home directory\n"
+ "\t-e date account expiry date\n"
+ "\t-p date password expiry date\n"
+ "\t-g grp initial group\n"
+ "\t-G grp1,grp2 additional groups\n"
+ "\t-m [ -k dir ] create and set up home\n"
+ "\t-M mode home directory permissions\n"
+ "\t-s shell name of login shell\n"
+ "\t-o duplicate uid ok\n"
+ "\t-L class user class\n"
+ "\t-h fd read password on fd\n"
+ "\t-H fd read encrypted password on fd\n"
+ "\t-Y update NIS maps\n"
+ "\t-N no update\n"
+ " Setting defaults:\n"
+ "\t-V etcdir alternate /etc location\n"
+ "\t-R rootdir alternate root directory\n"
+ "\t-D set user defaults\n"
+ "\t-b dir default home root dir\n"
+ "\t-e period default expiry period\n"
+ "\t-p period default password change period\n"
+ "\t-g group default group\n"
+ "\t-G grp1,grp2 additional groups\n"
+ "\t-L class default user class\n"
+ "\t-k dir default home skeleton\n"
+ "\t-M mode home directory permissions\n"
+ "\t-u min,max set min,max uids\n"
+ "\t-i min,max set min,max gids\n"
+ "\t-w method set default password method\n"
+ "\t-s shell default shell\n"
+ "\t-y path set NIS passwd file path\n",
+ "usage: pw userdel [uid|name] [switches]\n"
+ "\t-V etcdir alternate /etc location\n"
+ "\t-R rootdir alternate root directory\n"
+ "\t-n name login name\n"
+ "\t-u uid user id\n"
+ "\t-Y update NIS maps\n"
+ "\t-y path set NIS passwd file path\n"
+ "\t-r remove home & contents\n",
+ "usage: pw usermod [uid|name] [switches]\n"
+ "\t-V etcdir alternate /etc location\n"
+ "\t-R rootdir alternate root directory\n"
+ "\t-C config configuration file\n"
+ "\t-q quiet operation\n"
+ "\t-F force add if no user\n"
+ "\t-n name login name\n"
+ "\t-u uid user id\n"
+ "\t-c comment user name/comment\n"
+ "\t-d directory home directory\n"
+ "\t-e date account expiry date\n"
+ "\t-p date password expiry date\n"
+ "\t-g grp initial group\n"
+ "\t-G grp1,grp2 additional groups\n"
+ "\t-l name new login name\n"
+ "\t-L class user class\n"
+ "\t-m [ -k dir ] create and set up home\n"
+ "\t-M mode home directory permissions\n"
+ "\t-s shell name of login shell\n"
+ "\t-w method set new password using method\n"
+ "\t-h fd read password on fd\n"
+ "\t-H fd read encrypted password on fd\n"
+ "\t-Y update NIS maps\n"
+ "\t-y path set NIS passwd file path\n"
+ "\t-N no update\n",
+ "usage: pw usershow [uid|name] [switches]\n"
+ "\t-V etcdir alternate /etc location\n"
+ "\t-R rootdir alternate root directory\n"
+ "\t-n name login name\n"
+ "\t-u uid user id\n"
+ "\t-F force print\n"
+ "\t-P prettier format\n"
+ "\t-a print all users\n"
+ "\t-7 print in v7 format\n",
+ "usage: pw usernext [switches]\n"
+ "\t-V etcdir alternate /etc location\n"
+ "\t-R rootdir alternate root directory\n"
+ "\t-C config configuration file\n"
+ "\t-q quiet operation\n",
+ "usage pw: lock [switches]\n"
+ "\t-V etcdir alternate /etc locations\n"
+ "\t-C config configuration file\n"
+ "\t-q quiet operation\n",
+ "usage pw: unlock [switches]\n"
+ "\t-V etcdir alternate /etc locations\n"
+ "\t-C config configuration file\n"
+ "\t-q quiet operation\n"
+ },
+ {
+ "usage: pw groupadd [group|gid] [switches]\n"
+ "\t-V etcdir alternate /etc location\n"
+ "\t-R rootdir alternate root directory\n"
+ "\t-C config configuration file\n"
+ "\t-q quiet operation\n"
+ "\t-n group group name\n"
+ "\t-g gid group id\n"
+ "\t-M usr1,usr2 add users as group members\n"
+ "\t-o duplicate gid ok\n"
+ "\t-Y update NIS maps\n"
+ "\t-N no update\n",
+ "usage: pw groupdel [group|gid] [switches]\n"
+ "\t-V etcdir alternate /etc location\n"
+ "\t-R rootdir alternate root directory\n"
+ "\t-n name group name\n"
+ "\t-g gid group id\n"
+ "\t-Y update NIS maps\n",
+ "usage: pw groupmod [group|gid] [switches]\n"
+ "\t-V etcdir alternate /etc location\n"
+ "\t-R rootdir alternate root directory\n"
+ "\t-C config configuration file\n"
+ "\t-q quiet operation\n"
+ "\t-F force add if not exists\n"
+ "\t-n name group name\n"
+ "\t-g gid group id\n"
+ "\t-M usr1,usr2 replaces users as group members\n"
+ "\t-m usr1,usr2 add users as group members\n"
+ "\t-d usr1,usr2 delete users as group members\n"
+ "\t-l name new group name\n"
+ "\t-Y update NIS maps\n"
+ "\t-N no update\n",
+ "usage: pw groupshow [group|gid] [switches]\n"
+ "\t-V etcdir alternate /etc location\n"
+ "\t-R rootdir alternate root directory\n"
+ "\t-n name group name\n"
+ "\t-g gid group id\n"
+ "\t-F force print\n"
+ "\t-P prettier format\n"
+ "\t-a print all accounting groups\n",
+ "usage: pw groupnext [switches]\n"
+ "\t-V etcdir alternate /etc location\n"
+ "\t-R rootdir alternate root directory\n"
+ "\t-C config configuration file\n"
+ "\t-q quiet operation\n"
+ }
+ };
+
+ fprintf(stderr, "%s", help[which][mode]);
+ }
+ exit(EXIT_FAILURE);
+}
diff --git a/usr.sbin/pw/pw.conf.5 b/usr.sbin/pw/pw.conf.5
new file mode 100644
index 0000000..61c40e8
--- /dev/null
+++ b/usr.sbin/pw/pw.conf.5
@@ -0,0 +1,318 @@
+.\" Copyright (C) 1996
+.\" David L. Nugent. All rights reserved.
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions
+.\" are met:
+.\" 1. Redistributions of source code must retain the above copyright
+.\" notice, this list of conditions and the following disclaimer.
+.\" 2. Redistributions in binary form must reproduce the above copyright
+.\" notice, this list of conditions and the following disclaimer in the
+.\" documentation and/or other materials provided with the distribution.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT 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 DAVID L. NUGENT 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.
+.\"
+.\" $FreeBSD$
+.\"
+.Dd March 30, 2007
+.Dt PW.CONF 5
+.Os
+.Sh NAME
+.Nm pw.conf
+.Nd format of the pw.conf configuration file
+.Sh DESCRIPTION
+The file
+.Pa /etc/pw.conf
+contains configuration data for the
+.Xr pw 8
+utility.
+The
+.Xr pw 8
+utility is used for maintenance of the system password and group
+files, allowing users and groups to be added, deleted and changed.
+This file may be modified via the
+.Xr pw 8
+command using the
+.Ar useradd
+command and the
+.Fl D
+option, or by editing it directly with a text editor.
+.Pp
+Each line in
+.Pa /etc/pw.conf
+is treated either a comment or as configuration data;
+blank lines and lines commencing with a
+.Ql \&#
+character are considered comments, and any remaining lines are
+examined for a leading keyword, followed by corresponding data.
+.Pp
+Keywords recognized by
+.Xr pw 8
+are:
+.Bl -tag -width password_days -offset indent -compact
+.It defaultpasswd
+affect passwords generated for new users
+.It reuseuids
+reuse gaps in uid sequences
+.It reusegids
+reuse gaps in gid sequences
+.It nispasswd
+path to the
+.Tn NIS
+passwd database
+.It skeleton
+where to obtain default home contents
+.It newmail
+mail to send to new users
+.It logfile
+log user/group modifications to this file
+.It home
+root directory for home directories
+.It homemode
+permissions for home directory
+.It shellpath
+paths in which to locate shell programs
+.It shells
+list of valid shells (without path)
+.It defaultshell
+default shell (without path)
+.It defaultgroup
+default group
+.It extragroups
+add new users to this groups
+.It defaultclass
+place new users in this login class
+.It minuid
+.It maxuid
+range of valid default user ids
+.It mingid
+.It maxgid
+range of valid default group ids
+.It expire_days
+days after which account expires
+.It password_days
+days after which password expires
+.El
+.Pp
+Valid values for
+.Ar defaultpasswd
+are:
+.Bl -tag -width password_days -offset indent -compact
+.It no
+disable login on newly created accounts
+.It yes
+force the password to be the account name
+.It none
+force a blank password
+.It random
+generate a random password
+.El
+.Pp
+The second and third options are insecure and should be avoided if
+possible on a publicly accessible system.
+The first option requires that the superuser run
+.Xr passwd 1
+to set a password before the account may be used.
+This may also be useful for creating administrative accounts.
+The final option causes
+.Xr pw 8
+to respond by printing a randomly generated password on stdout.
+This is the preferred and most secure option.
+The
+.Xr pw 8
+utility also provides a method of setting a specific password for the new
+user via a filehandle (command lines are not secure).
+.Pp
+Both
+.Ar reuseuids
+and
+.Ar reusegids
+determine the method by which new user and group id numbers are
+generated.
+A
+.Ql \&yes
+in this field will cause
+.Xr pw 8
+to search for the first unused user or group id within the allowed
+range, whereas a
+.Ql \&no
+will ensure that no other existing user or group id within the range
+is numerically lower than the new one generated, and therefore avoids
+reusing gaps in the user or group id sequence that are caused by
+previous user or group deletions.
+Note that if the default group is not specified using the
+.Ar defaultgroup
+keyword,
+.Xr pw 8
+will create a new group for the user and attempt to keep the new
+user's uid and gid the same.
+If the new user's uid is currently in use as a group id, then the next
+available group id is chosen instead.
+.Pp
+On
+.Tn NIS
+servers which maintain a separate passwd database to
+.Pa /etc/master.passwd ,
+this option allows the additional file to be concurrently updated
+as user records are added, modified or removed.
+If blank or set to 'no', no additional database is updated.
+An absolute pathname must be used.
+.Pp
+The
+.Ar skeleton
+keyword nominates a directory from which the contents of a user's
+new home directory is constructed.
+This is
+.Pa /usr/share/skel
+by default.
+The
+.Xr pw 8 Ns 's
+.Fl m
+option causes the user's home directory to be created and populated
+using the files contained in the
+.Ar skeleton
+directory.
+.Pp
+To send an initial email to new users, the
+.Ar newmail
+keyword may be used to specify a path name to a file containing
+the message body of the message to be sent.
+To avoid sending mail when accounts are created, leave this entry
+blank or specify
+.Ql \&no .
+.Pp
+The
+.Ar logfile
+option allows logging of password file modifications into the
+nominated log file.
+To avoid creating or adding to such a logfile, then leave this
+field blank or specify
+.Ql \&no .
+.Pp
+The
+.Ar home
+keyword is mandatory.
+This specifies the location of the directory in which all new user
+home directories are created.
+.Pp
+The
+.Ar homemode
+keyword is optional.
+It specifies the creation mask of the user's home directory and is modified by
+.Xr umask 2 .
+.Pp
+The
+.Ar shellpath
+keyword specifies a list of directories - separated by colons
+.Ql \&:
+- which contain the programs used by the login shells.
+.Pp
+The
+.Ar shells
+keyword specifies a list of programs available for use as login
+shells.
+This list is a comma-separated list of shell names which should
+not contain a path.
+These shells must exist in one of the directories nominated by
+.Ar shellpath .
+.Pp
+The
+.Ar defaultshell
+keyword nominates which shell program to use for new users when
+none is specified on the
+.Xr pw 8
+command line.
+.Pp
+The
+.Ar defaultgroup
+keyword defines the primary group (the group id number in the
+password file) used for new accounts.
+If left blank, or the word
+.Ql \&no
+is used, then each new user will have a corresponding group of
+their own created automatically.
+This is the recommended procedure for new users as it best secures each
+user's files against interference by other users of the system
+irrespective of the
+.Em umask
+normally used by the user.
+.Pp
+The
+.Ar extragroups
+keyword provides an automatic means of placing new users into groups within
+the
+.Pa /etc/groups
+file.
+This is useful where all users share some resources, and is preferable
+to placing users into the same primary group.
+The effect of this keyword can be overridden using the
+.Fl G
+option on the
+.Xr pw 8
+command line.
+.Pp
+The
+.Ar defaultclass
+field determines the login class (See
+.Xr login.conf 5 )
+that new users will be allocated unless overwritten by
+.Xr pw 8 .
+.Pp
+The
+.Ar minuid ,
+.Ar maxuid ,
+.Ar mingid ,
+.Ar maxgid
+keywords determine the allowed ranges of automatically allocated user
+and group id numbers.
+The default values for both user and group ids are 1000 and 32000 as
+minimum and maximum respectively.
+The user and group id's actually used when creating an account with
+.Xr pw 8
+may be overridden using the
+.Fl u
+and
+.Fl g
+command line options.
+.Pp
+The
+.Ar expire_days
+and
+.Ar password_days
+are used to automatically calculate the number of days from the date
+on which an account is created when the account will expire or the
+user will be forced to change the account's password.
+A value of
+.Ql \&0
+in either field will disable the corresponding (account or password)
+expiration date.
+.Sh LIMITS
+The maximum line length of
+.Pa /etc/pw.conf
+is 1024 characters.
+Longer lines will be skipped and treated
+as comments.
+.Sh FILES
+.Bl -tag -width /etc/master.passwd -compact
+.It Pa /etc/pw.conf
+.It Pa /etc/passwd
+.It Pa /etc/master.passwd
+.It Pa /etc/group
+.El
+.Sh SEE ALSO
+.Xr passwd 1 ,
+.Xr umask 2 ,
+.Xr group 5 ,
+.Xr login.conf 5 ,
+.Xr passwd 5 ,
+.Xr pw 8
diff --git a/usr.sbin/pw/pw.h b/usr.sbin/pw/pw.h
new file mode 100644
index 0000000..b389f12
--- /dev/null
+++ b/usr.sbin/pw/pw.h
@@ -0,0 +1,106 @@
+/*-
+ * Copyright (C) 1996
+ * David L. Nugent. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT 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 DAVID L. NUGENT 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.
+ *
+ * $FreeBSD$
+ */
+
+#include <sys/stat.h>
+
+#define _WITH_GETLINE
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "pwupd.h"
+
+enum _mode
+{
+ M_ADD,
+ M_DELETE,
+ M_UPDATE,
+ M_PRINT,
+ M_NEXT,
+ M_LOCK,
+ M_UNLOCK,
+ M_NUM
+};
+
+enum _which
+{
+ W_USER,
+ W_GROUP,
+ W_NUM
+};
+
+#define _DEF_DIRMODE (S_IRWXU | S_IRWXG | S_IRWXO)
+#define _PATH_PW_CONF "/etc/pw.conf"
+#define _UC_MAXLINE 1024
+#define _UC_MAXSHELLS 32
+
+struct userconf *get_userconfig(const char *cfg);
+struct userconf *read_userconfig(char const * file);
+int write_userconfig(struct userconf *cnf, char const * file);
+
+int pw_group_add(int argc, char **argv, char *name);
+int pw_group_del(int argc, char **argv, char *name);
+int pw_group_mod(int argc, char **argv, char *name);
+int pw_group_next(int argc, char **argv, char *name);
+int pw_group_show(int argc, char **argv, char *name);
+int pw_user_add(int argc, char **argv, char *name);
+int pw_user_add(int argc, char **argv, char *name);
+int pw_user_add(int argc, char **argv, char *name);
+int pw_user_add(int argc, char **argv, char *name);
+int pw_user_del(int argc, char **argv, char *name);
+int pw_user_lock(int argc, char **argv, char *name);
+int pw_user_mod(int argc, char **argv, char *name);
+int pw_user_next(int argc, char **argv, char *name);
+int pw_user_show(int argc, char **argv, char *name);
+int pw_user_unlock(int argc, char **argv, char *name);
+int pw_groupnext(struct userconf *cnf, bool quiet);
+char *pw_checkname(char *name, int gecos);
+uintmax_t pw_checkid(char *nptr, uintmax_t maxval);
+int pw_checkfd(char *nptr);
+
+int addnispwent(const char *path, struct passwd *pwd);
+int delnispwent(const char *path, const char *login);
+int chgnispwent(const char *path, const char *login, struct passwd *pwd);
+
+int groupadd(struct userconf *, char *name, gid_t id, char *members, int fd,
+ bool dryrun, bool pretty, bool precrypted);
+
+int nis_update(void);
+
+int boolean_val(char const * str, int dflt);
+char const *boolean_str(int val);
+char *newstr(char const * p);
+
+void pw_log(struct userconf * cnf, int mode, int which, char const * fmt,...) __printflike(4, 5);
+char *pw_pwcrypt(char *password);
+
+extern const char *Modes[];
+extern const char *Which[];
+
+uintmax_t strtounum(const char * __restrict, uintmax_t, uintmax_t,
+ const char ** __restrict);
diff --git a/usr.sbin/pw/pw_conf.c b/usr.sbin/pw/pw_conf.c
new file mode 100644
index 0000000..d30c80e
--- /dev/null
+++ b/usr.sbin/pw/pw_conf.c
@@ -0,0 +1,524 @@
+/*-
+ * Copyright (C) 1996
+ * David L. Nugent. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT 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 DAVID L. NUGENT 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.
+ */
+
+#ifndef lint
+static const char rcsid[] =
+ "$FreeBSD$";
+#endif /* not lint */
+
+#include <sys/types.h>
+#include <sys/sbuf.h>
+
+#include <err.h>
+#include <fcntl.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "pw.h"
+
+#define debugging 0
+
+enum {
+ _UC_NONE,
+ _UC_DEFAULTPWD,
+ _UC_REUSEUID,
+ _UC_REUSEGID,
+ _UC_NISPASSWD,
+ _UC_DOTDIR,
+ _UC_NEWMAIL,
+ _UC_LOGFILE,
+ _UC_HOMEROOT,
+ _UC_HOMEMODE,
+ _UC_SHELLPATH,
+ _UC_SHELLS,
+ _UC_DEFAULTSHELL,
+ _UC_DEFAULTGROUP,
+ _UC_EXTRAGROUPS,
+ _UC_DEFAULTCLASS,
+ _UC_MINUID,
+ _UC_MAXUID,
+ _UC_MINGID,
+ _UC_MAXGID,
+ _UC_EXPIRE,
+ _UC_PASSWORD,
+ _UC_FIELDS
+};
+
+static char bourne_shell[] = "sh";
+
+static char *system_shells[_UC_MAXSHELLS] =
+{
+ bourne_shell,
+ "csh",
+ "tcsh"
+};
+
+static char const *booltrue[] =
+{
+ "yes", "true", "1", "on", NULL
+};
+static char const *boolfalse[] =
+{
+ "no", "false", "0", "off", NULL
+};
+
+static struct userconf config =
+{
+ 0, /* Default password for new users? (nologin) */
+ 0, /* Reuse uids? */
+ 0, /* Reuse gids? */
+ NULL, /* NIS version of the passwd file */
+ "/usr/share/skel", /* Where to obtain skeleton files */
+ NULL, /* Mail to send to new accounts */
+ "/var/log/userlog", /* Where to log changes */
+ "/home", /* Where to create home directory */
+ _DEF_DIRMODE, /* Home directory perms, modified by umask */
+ "/bin", /* Where shells are located */
+ system_shells, /* List of shells (first is default) */
+ bourne_shell, /* Default shell */
+ NULL, /* Default group name */
+ NULL, /* Default (additional) groups */
+ NULL, /* Default login class */
+ 1000, 32000, /* Allowed range of uids */
+ 1000, 32000, /* Allowed range of gids */
+ 0, /* Days until account expires */
+ 0 /* Days until password expires */
+};
+
+static char const *comments[_UC_FIELDS] =
+{
+ "#\n# pw.conf - user/group configuration defaults\n#\n",
+ "\n# Password for new users? no=nologin yes=loginid none=blank random=random\n",
+ "\n# Reuse gaps in uid sequence? (yes or no)\n",
+ "\n# Reuse gaps in gid sequence? (yes or no)\n",
+ "\n# Path to the NIS passwd file (blank or 'no' for none)\n",
+ "\n# Obtain default dotfiles from this directory\n",
+ "\n# Mail this file to new user (/etc/newuser.msg or no)\n",
+ "\n# Log add/change/remove information in this file\n",
+ "\n# Root directory in which $HOME directory is created\n",
+ "\n# Mode for the new $HOME directory, will be modified by umask\n",
+ "\n# Colon separated list of directories containing valid shells\n",
+ "\n# Comma separated list of available shells (without paths)\n",
+ "\n# Default shell (without path)\n",
+ "\n# Default group (leave blank for new group per user)\n",
+ "\n# Extra groups for new users\n",
+ "\n# Default login class for new users\n",
+ "\n# Range of valid default user ids\n",
+ NULL,
+ "\n# Range of valid default group ids\n",
+ NULL,
+ "\n# Days after which account expires (0=disabled)\n",
+ "\n# Days after which password expires (0=disabled)\n"
+};
+
+static char const *kwds[] =
+{
+ "",
+ "defaultpasswd",
+ "reuseuids",
+ "reusegids",
+ "nispasswd",
+ "skeleton",
+ "newmail",
+ "logfile",
+ "home",
+ "homemode",
+ "shellpath",
+ "shells",
+ "defaultshell",
+ "defaultgroup",
+ "extragroups",
+ "defaultclass",
+ "minuid",
+ "maxuid",
+ "mingid",
+ "maxgid",
+ "expire_days",
+ "password_days",
+ NULL
+};
+
+static char *
+unquote(char const * str)
+{
+ if (str && (*str == '"' || *str == '\'')) {
+ char *p = strchr(str + 1, *str);
+
+ if (p != NULL)
+ *p = '\0';
+ return (char *) (*++str ? str : NULL);
+ }
+ return (char *) str;
+}
+
+int
+boolean_val(char const * str, int dflt)
+{
+ if ((str = unquote(str)) != NULL) {
+ int i;
+
+ for (i = 0; booltrue[i]; i++)
+ if (strcmp(str, booltrue[i]) == 0)
+ return 1;
+ for (i = 0; boolfalse[i]; i++)
+ if (strcmp(str, boolfalse[i]) == 0)
+ return 0;
+
+ /*
+ * Special cases for defaultpassword
+ */
+ if (strcmp(str, "random") == 0)
+ return -1;
+ if (strcmp(str, "none") == 0)
+ return -2;
+ }
+ return dflt;
+}
+
+char const *
+boolean_str(int val)
+{
+ if (val == -1)
+ return "random";
+ else if (val == -2)
+ return "none";
+ else
+ return val ? booltrue[0] : boolfalse[0];
+}
+
+char *
+newstr(char const * p)
+{
+ char *q;
+
+ if ((p = unquote(p)) == NULL)
+ return (NULL);
+
+ if ((q = strdup(p)) == NULL)
+ err(1, "strdup()");
+
+ return (q);
+}
+
+struct userconf *
+read_userconfig(char const * file)
+{
+ FILE *fp;
+ char *buf, *p;
+ const char *errstr;
+ size_t linecap;
+ ssize_t linelen;
+
+ buf = NULL;
+ linecap = 0;
+
+ if (file == NULL)
+ file = _PATH_PW_CONF;
+
+ if ((fp = fopen(file, "r")) == NULL)
+ return (&config);
+
+ while ((linelen = getline(&buf, &linecap, fp)) > 0) {
+ if (*buf && (p = strtok(buf, " \t\r\n=")) != NULL && *p != '#') {
+ static char const toks[] = " \t\r\n,=";
+ char *q = strtok(NULL, toks);
+ int i = 0;
+ mode_t *modeset;
+
+ while (i < _UC_FIELDS && strcmp(p, kwds[i]) != 0)
+ ++i;
+#if debugging
+ if (i == _UC_FIELDS)
+ printf("Got unknown kwd `%s' val=`%s'\n", p, q ? q : "");
+ else
+ printf("Got kwd[%s]=%s\n", p, q);
+#endif
+ switch (i) {
+ case _UC_DEFAULTPWD:
+ config.default_password = boolean_val(q, 1);
+ break;
+ case _UC_REUSEUID:
+ config.reuse_uids = boolean_val(q, 0);
+ break;
+ case _UC_REUSEGID:
+ config.reuse_gids = boolean_val(q, 0);
+ break;
+ case _UC_NISPASSWD:
+ config.nispasswd = (q == NULL || !boolean_val(q, 1))
+ ? NULL : newstr(q);
+ break;
+ case _UC_DOTDIR:
+ config.dotdir = (q == NULL || !boolean_val(q, 1))
+ ? NULL : newstr(q);
+ break;
+ case _UC_NEWMAIL:
+ config.newmail = (q == NULL || !boolean_val(q, 1))
+ ? NULL : newstr(q);
+ break;
+ case _UC_LOGFILE:
+ config.logfile = (q == NULL || !boolean_val(q, 1))
+ ? NULL : newstr(q);
+ break;
+ case _UC_HOMEROOT:
+ config.home = (q == NULL || !boolean_val(q, 1))
+ ? "/home" : newstr(q);
+ break;
+ case _UC_HOMEMODE:
+ modeset = setmode(q);
+ config.homemode = (q == NULL || !boolean_val(q, 1))
+ ? _DEF_DIRMODE : getmode(modeset, _DEF_DIRMODE);
+ free(modeset);
+ break;
+ case _UC_SHELLPATH:
+ config.shelldir = (q == NULL || !boolean_val(q, 1))
+ ? "/bin" : newstr(q);
+ break;
+ case _UC_SHELLS:
+ for (i = 0; i < _UC_MAXSHELLS && q != NULL; i++, q = strtok(NULL, toks))
+ system_shells[i] = newstr(q);
+ if (i > 0)
+ while (i < _UC_MAXSHELLS)
+ system_shells[i++] = NULL;
+ break;
+ case _UC_DEFAULTSHELL:
+ config.shell_default = (q == NULL || !boolean_val(q, 1))
+ ? (char *) bourne_shell : newstr(q);
+ break;
+ case _UC_DEFAULTGROUP:
+ q = unquote(q);
+ config.default_group = (q == NULL || !boolean_val(q, 1) || GETGRNAM(q) == NULL)
+ ? NULL : newstr(q);
+ break;
+ case _UC_EXTRAGROUPS:
+ while ((q = strtok(NULL, toks)) != NULL) {
+ if (config.groups == NULL)
+ config.groups = sl_init();
+ sl_add(config.groups, newstr(q));
+ }
+ break;
+ case _UC_DEFAULTCLASS:
+ config.default_class = (q == NULL || !boolean_val(q, 1))
+ ? NULL : newstr(q);
+ break;
+ case _UC_MINUID:
+ if ((q = unquote(q)) != NULL) {
+ config.min_uid = strtounum(q, 0,
+ UID_MAX, &errstr);
+ if (errstr)
+ warnx("Invalid min_uid: '%s';"
+ " ignoring", q);
+ }
+ break;
+ case _UC_MAXUID:
+ if ((q = unquote(q)) != NULL) {
+ config.max_uid = strtounum(q, 0,
+ UID_MAX, &errstr);
+ if (errstr)
+ warnx("Invalid max_uid: '%s';"
+ " ignoring", q);
+ }
+ break;
+ case _UC_MINGID:
+ if ((q = unquote(q)) != NULL) {
+ config.min_gid = strtounum(q, 0,
+ GID_MAX, &errstr);
+ if (errstr)
+ warnx("Invalid min_gid: '%s';"
+ " ignoring", q);
+ }
+ break;
+ case _UC_MAXGID:
+ if ((q = unquote(q)) != NULL) {
+ config.max_gid = strtounum(q, 0,
+ GID_MAX, &errstr);
+ if (errstr)
+ warnx("Invalid max_gid: '%s';"
+ " ignoring", q);
+ }
+ break;
+ case _UC_EXPIRE:
+ if ((q = unquote(q)) != NULL) {
+ config.expire_days = strtonum(q, 0,
+ INT_MAX, &errstr);
+ if (errstr)
+ warnx("Invalid expire days:"
+ " '%s'; ignoring", q);
+ }
+ break;
+ case _UC_PASSWORD:
+ if ((q = unquote(q)) != NULL) {
+ config.password_days = strtonum(q, 0,
+ INT_MAX, &errstr);
+ if (errstr)
+ warnx("Invalid password days:"
+ " '%s'; ignoring", q);
+ }
+ break;
+ case _UC_FIELDS:
+ case _UC_NONE:
+ break;
+ }
+ }
+ }
+ free(buf);
+ fclose(fp);
+
+ return (&config);
+}
+
+
+int
+write_userconfig(struct userconf *cnf, const char *file)
+{
+ int fd;
+ int i, j;
+ struct sbuf *buf;
+ FILE *fp;
+
+ if (file == NULL)
+ file = _PATH_PW_CONF;
+
+ if ((fd = open(file, O_CREAT|O_RDWR|O_TRUNC|O_EXLOCK, 0644)) == -1)
+ return (0);
+
+ if ((fp = fdopen(fd, "w")) == NULL) {
+ close(fd);
+ return (0);
+ }
+
+ buf = sbuf_new_auto();
+ for (i = _UC_NONE; i < _UC_FIELDS; i++) {
+ int quote = 1;
+
+ sbuf_clear(buf);
+ switch (i) {
+ case _UC_DEFAULTPWD:
+ sbuf_cat(buf, boolean_str(cnf->default_password));
+ break;
+ case _UC_REUSEUID:
+ sbuf_cat(buf, boolean_str(cnf->reuse_uids));
+ break;
+ case _UC_REUSEGID:
+ sbuf_cat(buf, boolean_str(cnf->reuse_gids));
+ break;
+ case _UC_NISPASSWD:
+ sbuf_cat(buf, cnf->nispasswd ? cnf->nispasswd : "");
+ quote = 0;
+ break;
+ case _UC_DOTDIR:
+ sbuf_cat(buf, cnf->dotdir ? cnf->dotdir :
+ boolean_str(0));
+ break;
+ case _UC_NEWMAIL:
+ sbuf_cat(buf, cnf->newmail ? cnf->newmail :
+ boolean_str(0));
+ break;
+ case _UC_LOGFILE:
+ sbuf_cat(buf, cnf->logfile ? cnf->logfile :
+ boolean_str(0));
+ break;
+ case _UC_HOMEROOT:
+ sbuf_cat(buf, cnf->home);
+ break;
+ case _UC_HOMEMODE:
+ sbuf_printf(buf, "%04o", cnf->homemode);
+ quote = 0;
+ break;
+ case _UC_SHELLPATH:
+ sbuf_cat(buf, cnf->shelldir);
+ break;
+ case _UC_SHELLS:
+ for (j = 0; j < _UC_MAXSHELLS &&
+ system_shells[j] != NULL; j++)
+ sbuf_printf(buf, "%s\"%s\"", j ?
+ "," : "", system_shells[j]);
+ quote = 0;
+ break;
+ case _UC_DEFAULTSHELL:
+ sbuf_cat(buf, cnf->shell_default ?
+ cnf->shell_default : bourne_shell);
+ break;
+ case _UC_DEFAULTGROUP:
+ sbuf_cat(buf, cnf->default_group ?
+ cnf->default_group : "");
+ break;
+ case _UC_EXTRAGROUPS:
+ for (j = 0; cnf->groups != NULL &&
+ j < (int)cnf->groups->sl_cur; j++)
+ sbuf_printf(buf, "%s\"%s\"", j ?
+ "," : "", cnf->groups->sl_str[j]);
+ quote = 0;
+ break;
+ case _UC_DEFAULTCLASS:
+ sbuf_cat(buf, cnf->default_class ?
+ cnf->default_class : "");
+ break;
+ case _UC_MINUID:
+ sbuf_printf(buf, "%ju", (uintmax_t)cnf->min_uid);
+ quote = 0;
+ break;
+ case _UC_MAXUID:
+ sbuf_printf(buf, "%ju", (uintmax_t)cnf->max_uid);
+ quote = 0;
+ break;
+ case _UC_MINGID:
+ sbuf_printf(buf, "%ju", (uintmax_t)cnf->min_gid);
+ quote = 0;
+ break;
+ case _UC_MAXGID:
+ sbuf_printf(buf, "%ju", (uintmax_t)cnf->max_gid);
+ quote = 0;
+ break;
+ case _UC_EXPIRE:
+ sbuf_printf(buf, "%jd", (intmax_t)cnf->expire_days);
+ quote = 0;
+ break;
+ case _UC_PASSWORD:
+ sbuf_printf(buf, "%jd", (intmax_t)cnf->password_days);
+ quote = 0;
+ break;
+ case _UC_NONE:
+ break;
+ }
+ sbuf_finish(buf);
+
+ if (comments[i])
+ fputs(comments[i], fp);
+
+ if (*kwds[i]) {
+ if (quote)
+ fprintf(fp, "%s = \"%s\"\n", kwds[i],
+ sbuf_data(buf));
+ else
+ fprintf(fp, "%s = %s\n", kwds[i], sbuf_data(buf));
+#if debugging
+ printf("WROTE: %s = %s\n", kwds[i], sbuf_data(buf));
+#endif
+ }
+ }
+ sbuf_delete(buf);
+ return (fclose(fp) != EOF);
+}
diff --git a/usr.sbin/pw/pw_group.c b/usr.sbin/pw/pw_group.c
new file mode 100644
index 0000000..289a4c8
--- /dev/null
+++ b/usr.sbin/pw/pw_group.c
@@ -0,0 +1,694 @@
+/*-
+ * Copyright (C) 1996
+ * David L. Nugent. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT 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 DAVID L. NUGENT 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.
+ */
+
+#ifndef lint
+static const char rcsid[] =
+ "$FreeBSD$";
+#endif /* not lint */
+
+#include <ctype.h>
+#include <err.h>
+#include <grp.h>
+#include <libutil.h>
+#include <paths.h>
+#include <string.h>
+#include <sysexits.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include "pw.h"
+#include "bitmap.h"
+
+static struct passwd *lookup_pwent(const char *user);
+static void delete_members(struct group *grp, char *list);
+static int print_group(struct group * grp, bool pretty);
+static gid_t gr_gidpolicy(struct userconf * cnf, intmax_t id);
+
+static void
+grp_set_passwd(struct group *grp, bool update, int fd, bool precrypted)
+{
+ int b;
+ int istty;
+ struct termios t, n;
+ char *p, line[256];
+
+ if (fd == -1)
+ return;
+
+ if (fd == '-') {
+ grp->gr_passwd = "*"; /* No access */
+ return;
+ }
+
+ if ((istty = isatty(fd))) {
+ n = t;
+ /* Disable echo */
+ n.c_lflag &= ~(ECHO);
+ tcsetattr(fd, TCSANOW, &n);
+ printf("%sassword for group %s:", update ? "New p" : "P",
+ grp->gr_name);
+ fflush(stdout);
+ }
+ b = read(fd, line, sizeof(line) - 1);
+ if (istty) { /* Restore state */
+ tcsetattr(fd, TCSANOW, &t);
+ fputc('\n', stdout);
+ fflush(stdout);
+ }
+ if (b < 0)
+ err(EX_OSERR, "-h file descriptor");
+ line[b] = '\0';
+ if ((p = strpbrk(line, " \t\r\n")) != NULL)
+ *p = '\0';
+ if (!*line)
+ errx(EX_DATAERR, "empty password read on file descriptor %d",
+ conf.fd);
+ if (precrypted) {
+ if (strchr(line, ':') != 0)
+ errx(EX_DATAERR, "wrong encrypted passwrd");
+ grp->gr_passwd = line;
+ } else
+ grp->gr_passwd = pw_pwcrypt(line);
+}
+
+int
+pw_groupnext(struct userconf *cnf, bool quiet)
+{
+ gid_t next = gr_gidpolicy(cnf, -1);
+
+ if (quiet)
+ return (next);
+ printf("%ju\n", (uintmax_t)next);
+
+ return (EXIT_SUCCESS);
+}
+
+static struct group *
+getgroup(char *name, intmax_t id, bool fatal)
+{
+ struct group *grp;
+
+ if (id < 0 && name == NULL)
+ errx(EX_DATAERR, "groupname or id required");
+ grp = (name != NULL) ? GETGRNAM(name) : GETGRGID(id);
+ if (grp == NULL) {
+ if (!fatal)
+ return (NULL);
+ if (name == NULL)
+ errx(EX_DATAERR, "unknown gid `%ju'", id);
+ errx(EX_DATAERR, "unknown group `%s'", name);
+ }
+ return (grp);
+}
+
+/*
+ * Lookup a passwd entry using a name or UID.
+ */
+static struct passwd *
+lookup_pwent(const char *user)
+{
+ struct passwd *pwd;
+
+ if ((pwd = GETPWNAM(user)) == NULL &&
+ (!isdigit((unsigned char)*user) ||
+ (pwd = getpwuid((uid_t) atoi(user))) == NULL))
+ errx(EX_NOUSER, "user `%s' does not exist", user);
+
+ return (pwd);
+}
+
+
+/*
+ * Delete requested members from a group.
+ */
+static void
+delete_members(struct group *grp, char *list)
+{
+ char *p;
+ int k;
+
+ if (grp->gr_mem == NULL)
+ return;
+
+ for (p = strtok(list, ", \t"); p != NULL; p = strtok(NULL, ", \t")) {
+ for (k = 0; grp->gr_mem[k] != NULL; k++) {
+ if (strcmp(grp->gr_mem[k], p) == 0)
+ break;
+ }
+ if (grp->gr_mem[k] == NULL) /* No match */
+ continue;
+
+ for (; grp->gr_mem[k] != NULL; k++)
+ grp->gr_mem[k] = grp->gr_mem[k+1];
+ }
+}
+
+static gid_t
+gr_gidpolicy(struct userconf * cnf, intmax_t id)
+{
+ struct group *grp;
+ struct bitmap bm;
+ gid_t gid = (gid_t) - 1;
+
+ /*
+ * Check the given gid, if any
+ */
+ if (id > 0) {
+ gid = (gid_t) id;
+
+ if ((grp = GETGRGID(gid)) != NULL && conf.checkduplicate)
+ errx(EX_DATAERR, "gid `%ju' has already been allocated",
+ (uintmax_t)grp->gr_gid);
+ return (gid);
+ }
+
+ /*
+ * We need to allocate the next available gid under one of
+ * two policies a) Grab the first unused gid b) Grab the
+ * highest possible unused gid
+ */
+ if (cnf->min_gid >= cnf->max_gid) { /* Sanity claus^H^H^H^Hheck */
+ cnf->min_gid = 1000;
+ cnf->max_gid = 32000;
+ }
+ bm = bm_alloc(cnf->max_gid - cnf->min_gid + 1);
+
+ /*
+ * Now, let's fill the bitmap from the password file
+ */
+ SETGRENT();
+ while ((grp = GETGRENT()) != NULL)
+ if ((gid_t)grp->gr_gid >= (gid_t)cnf->min_gid &&
+ (gid_t)grp->gr_gid <= (gid_t)cnf->max_gid)
+ bm_setbit(&bm, grp->gr_gid - cnf->min_gid);
+ ENDGRENT();
+
+ /*
+ * Then apply the policy, with fallback to reuse if necessary
+ */
+ if (cnf->reuse_gids)
+ gid = (gid_t) (bm_firstunset(&bm) + cnf->min_gid);
+ else {
+ gid = (gid_t) (bm_lastset(&bm) + 1);
+ if (!bm_isset(&bm, gid))
+ gid += cnf->min_gid;
+ else
+ gid = (gid_t) (bm_firstunset(&bm) + cnf->min_gid);
+ }
+
+ /*
+ * Another sanity check
+ */
+ if (gid < cnf->min_gid || gid > cnf->max_gid)
+ errx(EX_SOFTWARE, "unable to allocate a new gid - range fully "
+ "used");
+ bm_dealloc(&bm);
+ return (gid);
+}
+
+static int
+print_group(struct group * grp, bool pretty)
+{
+ char *buf = NULL;
+ int i;
+
+ if (pretty) {
+ printf("Group Name: %-15s #%lu\n"
+ " Members: ",
+ grp->gr_name, (long) grp->gr_gid);
+ if (grp->gr_mem != NULL) {
+ for (i = 0; grp->gr_mem[i]; i++)
+ printf("%s%s", i ? "," : "", grp->gr_mem[i]);
+ }
+ fputs("\n\n", stdout);
+ return (EXIT_SUCCESS);
+ }
+
+ buf = gr_make(grp);
+ printf("%s\n", buf);
+ free(buf);
+ return (EXIT_SUCCESS);
+}
+
+int
+pw_group_next(int argc, char **argv, char *arg1 __unused)
+{
+ struct userconf *cnf;
+ const char *cfg = NULL;
+ int ch;
+ bool quiet = false;
+
+ while ((ch = getopt(argc, argv, "Cq")) != -1) {
+ switch (ch) {
+ case 'C':
+ cfg = optarg;
+ break;
+ case 'q':
+ quiet = true;
+ break;
+ }
+ }
+
+ if (quiet)
+ freopen(_PATH_DEVNULL, "w", stderr);
+ cnf = get_userconfig(cfg);
+ return (pw_groupnext(cnf, quiet));
+}
+
+int
+pw_group_show(int argc, char **argv, char *arg1)
+{
+ struct group *grp = NULL;
+ char *name;
+ intmax_t id = -1;
+ int ch;
+ bool all, force, quiet, pretty;
+
+ all = force = quiet = pretty = false;
+
+ struct group fakegroup = {
+ "nogroup",
+ "*",
+ -1,
+ NULL
+ };
+
+ if (arg1 != NULL) {
+ if (arg1[strspn(arg1, "0123456789")] == '\0')
+ id = pw_checkid(arg1, GID_MAX);
+ else
+ name = arg1;
+ }
+
+ while ((ch = getopt(argc, argv, "C:qn:g:FPa")) != -1) {
+ switch (ch) {
+ case 'C':
+ /* ignore compatibility */
+ break;
+ case 'q':
+ quiet = true;
+ break;
+ case 'n':
+ name = optarg;
+ break;
+ case 'g':
+ id = pw_checkid(optarg, GID_MAX);
+ break;
+ case 'F':
+ force = true;
+ break;
+ case 'P':
+ pretty = true;
+ break;
+ case 'a':
+ all = true;
+ break;
+ }
+ }
+
+ if (quiet)
+ freopen(_PATH_DEVNULL, "w", stderr);
+
+ if (all) {
+ SETGRENT();
+ while ((grp = GETGRENT()) != NULL)
+ print_group(grp, pretty);
+ ENDGRENT();
+ return (EXIT_SUCCESS);
+ }
+
+ grp = getgroup(name, id, !force);
+ if (grp == NULL)
+ grp = &fakegroup;
+
+ return (print_group(grp, pretty));
+}
+
+int
+pw_group_del(int argc, char **argv, char *arg1)
+{
+ struct userconf *cnf = NULL;
+ struct group *grp = NULL;
+ char *name;
+ const char *cfg = NULL;
+ intmax_t id = -1;
+ int ch, rc;
+ bool quiet = false;
+ bool nis = false;
+
+ if (arg1 != NULL) {
+ if (arg1[strspn(arg1, "0123456789")] == '\0')
+ id = pw_checkid(arg1, GID_MAX);
+ else
+ name = arg1;
+ }
+
+ while ((ch = getopt(argc, argv, "C:qn:g:Y")) != -1) {
+ switch (ch) {
+ case 'C':
+ cfg = optarg;
+ break;
+ case 'q':
+ quiet = true;
+ break;
+ case 'n':
+ name = optarg;
+ break;
+ case 'g':
+ id = pw_checkid(optarg, GID_MAX);
+ break;
+ case 'Y':
+ nis = true;
+ break;
+ }
+ }
+
+ if (quiet)
+ freopen(_PATH_DEVNULL, "w", stderr);
+ grp = getgroup(name, id, true);
+ cnf = get_userconfig(cfg);
+ rc = delgrent(grp);
+ if (rc == -1)
+ err(EX_IOERR, "group '%s' not available (NIS?)", name);
+ else if (rc != 0)
+ err(EX_IOERR, "group update");
+ pw_log(cnf, M_DELETE, W_GROUP, "%s(%ju) removed", name,
+ (uintmax_t)id);
+
+ if (nis && nis_update() == 0)
+ pw_log(cnf, M_DELETE, W_GROUP, "NIS maps updated");
+
+ return (EXIT_SUCCESS);
+}
+
+static bool
+grp_has_member(struct group *grp, const char *name)
+{
+ int j;
+
+ for (j = 0; grp->gr_mem != NULL && grp->gr_mem[j] != NULL; j++)
+ if (strcmp(grp->gr_mem[j], name) == 0)
+ return (true);
+ return (false);
+}
+
+static void
+grp_add_members(struct group **grp, char *members)
+{
+ struct passwd *pwd;
+ char *p;
+ char tok[] = ", \t";
+
+ if (members == NULL)
+ return;
+ for (p = strtok(members, tok); p != NULL; p = strtok(NULL, tok)) {
+ pwd = lookup_pwent(p);
+ if (grp_has_member(*grp, pwd->pw_name))
+ continue;
+ *grp = gr_add(*grp, pwd->pw_name);
+ }
+}
+
+int
+groupadd(struct userconf *cnf, char *name, gid_t id, char *members, int fd,
+ bool dryrun, bool pretty, bool precrypted)
+{
+ struct group *grp;
+ int rc;
+
+ struct group fakegroup = {
+ "nogroup",
+ "*",
+ -1,
+ NULL
+ };
+
+ grp = &fakegroup;
+ grp->gr_name = pw_checkname(name, 0);
+ grp->gr_passwd = "*";
+ grp->gr_gid = gr_gidpolicy(cnf, id);
+ grp->gr_mem = NULL;
+
+ /*
+ * This allows us to set a group password Group passwords is an
+ * antique idea, rarely used and insecure (no secure database) Should
+ * be discouraged, but it is apparently still supported by some
+ * software.
+ */
+ grp_set_passwd(grp, false, fd, precrypted);
+ grp_add_members(&grp, members);
+ if (dryrun)
+ return (print_group(grp, pretty));
+
+ if ((rc = addgrent(grp)) != 0) {
+ if (rc == -1)
+ errx(EX_IOERR, "group '%s' already exists",
+ grp->gr_name);
+ else
+ err(EX_IOERR, "group update");
+ }
+
+ pw_log(cnf, M_ADD, W_GROUP, "%s(%ju)", grp->gr_name,
+ (uintmax_t)grp->gr_gid);
+
+ return (EXIT_SUCCESS);
+}
+
+int
+pw_group_add(int argc, char **argv, char *arg1)
+{
+ struct userconf *cnf = NULL;
+ char *name = NULL;
+ char *members = NULL;
+ const char *cfg = NULL;
+ intmax_t id = -1;
+ int ch, rc, fd = -1;
+ bool quiet, precrypted, dryrun, pretty, nis;
+
+ quiet = precrypted = dryrun = pretty = nis = false;
+
+ if (arg1 != NULL) {
+ if (arg1[strspn(arg1, "0123456789")] == '\0')
+ id = pw_checkid(arg1, GID_MAX);
+ else
+ name = arg1;
+ }
+
+ while ((ch = getopt(argc, argv, "C:qn:g:h:H:M:oNPY")) != -1) {
+ switch (ch) {
+ case 'C':
+ cfg = optarg;
+ break;
+ case 'q':
+ quiet = true;
+ break;
+ case 'n':
+ name = optarg;
+ break;
+ case 'g':
+ id = pw_checkid(optarg, GID_MAX);
+ break;
+ case 'H':
+ if (fd != -1)
+ errx(EX_USAGE, "'-h' and '-H' are mutually "
+ "exclusive options");
+ fd = pw_checkfd(optarg);
+ precrypted = true;
+ if (fd == '-')
+ errx(EX_USAGE, "-H expects a file descriptor");
+ break;
+ case 'h':
+ if (fd != -1)
+ errx(EX_USAGE, "'-h' and '-H' are mutually "
+ "exclusive options");
+ fd = pw_checkfd(optarg);
+ break;
+ case 'M':
+ members = optarg;
+ break;
+ case 'o':
+ conf.checkduplicate = false;
+ break;
+ case 'N':
+ dryrun = true;
+ break;
+ case 'P':
+ pretty = true;
+ break;
+ case 'Y':
+ nis = true;
+ break;
+ }
+ }
+
+ if (quiet)
+ freopen(_PATH_DEVNULL, "w", stderr);
+ if (name == NULL)
+ errx(EX_DATAERR, "group name required");
+ if (GETGRNAM(name) != NULL)
+ errx(EX_DATAERR, "group name `%s' already exists", name);
+ cnf = get_userconfig(cfg);
+ rc = groupadd(cnf, name, gr_gidpolicy(cnf, id), members, fd, dryrun,
+ pretty, precrypted);
+ if (nis && rc == EXIT_SUCCESS && nis_update() == 0)
+ pw_log(cnf, M_ADD, W_GROUP, "NIS maps updated");
+
+ return (rc);
+}
+
+int
+pw_group_mod(int argc, char **argv, char *arg1)
+{
+ struct userconf *cnf;
+ struct group *grp = NULL;
+ const char *cfg = NULL;
+ char *oldmembers = NULL;
+ char *members = NULL;
+ char *newmembers = NULL;
+ char *newname = NULL;
+ char *name = NULL;
+ intmax_t id = -1;
+ int ch, rc, fd = -1;
+ bool quiet, pretty, dryrun, nis, precrypted;
+
+ quiet = pretty = dryrun = nis = precrypted = false;
+
+ if (arg1 != NULL) {
+ if (arg1[strspn(arg1, "0123456789")] == '\0')
+ id = pw_checkid(arg1, GID_MAX);
+ else
+ name = arg1;
+ }
+
+ while ((ch = getopt(argc, argv, "C:qn:d:g:l:h:H:M:m:NPY")) != -1) {
+ switch (ch) {
+ case 'C':
+ cfg = optarg;
+ break;
+ case 'q':
+ quiet = true;
+ break;
+ case 'n':
+ name = optarg;
+ break;
+ case 'g':
+ id = pw_checkid(optarg, GID_MAX);
+ break;
+ case 'd':
+ oldmembers = optarg;
+ break;
+ case 'l':
+ newname = optarg;
+ break;
+ case 'H':
+ if (fd != -1)
+ errx(EX_USAGE, "'-h' and '-H' are mutually "
+ "exclusive options");
+ fd = pw_checkfd(optarg);
+ precrypted = true;
+ if (fd == '-')
+ errx(EX_USAGE, "-H expects a file descriptor");
+ break;
+ case 'h':
+ if (fd != -1)
+ errx(EX_USAGE, "'-h' and '-H' are mutually "
+ "exclusive options");
+ fd = pw_checkfd(optarg);
+ break;
+ case 'M':
+ members = optarg;
+ break;
+ case 'm':
+ newmembers = optarg;
+ break;
+ case 'N':
+ dryrun = true;
+ break;
+ case 'P':
+ pretty = true;
+ break;
+ case 'Y':
+ nis = true;
+ break;
+ }
+ }
+ if (quiet)
+ freopen(_PATH_DEVNULL, "w", stderr);
+ cnf = get_userconfig(cfg);
+ grp = getgroup(name, id, true);
+ if (name == NULL)
+ name = grp->gr_name;
+ if (id > 0)
+ grp->gr_gid = id;
+
+ if (newname != NULL)
+ grp->gr_name = pw_checkname(newname, 0);
+
+ grp_set_passwd(grp, true, fd, precrypted);
+ /*
+ * Keep the same logic as old code for now:
+ * if -M is passed, -d and -m are ignored
+ * then id -d, -m is ignored
+ * last is -m
+ */
+
+ if (members) {
+ grp->gr_mem = NULL;
+ grp_add_members(&grp, members);
+ } else if (oldmembers) {
+ delete_members(grp, oldmembers);
+ } else if (newmembers) {
+ grp_add_members(&grp, newmembers);
+ }
+
+ if (dryrun) {
+ print_group(grp, pretty);
+ return (EXIT_SUCCESS);
+ }
+
+ if ((rc = chggrent(name, grp)) != 0) {
+ if (rc == -1)
+ errx(EX_IOERR, "group '%s' not available (NIS?)",
+ grp->gr_name);
+ else
+ err(EX_IOERR, "group update");
+ }
+
+ if (newname)
+ name = newname;
+
+ /* grp may have been invalidated */
+ if ((grp = GETGRNAM(name)) == NULL)
+ errx(EX_SOFTWARE, "group disappeared during update");
+
+ pw_log(cnf, M_UPDATE, W_GROUP, "%s(%ju)", grp->gr_name,
+ (uintmax_t)grp->gr_gid);
+
+ if (nis && nis_update() == 0)
+ pw_log(cnf, M_UPDATE, W_GROUP, "NIS maps updated");
+
+ return (EXIT_SUCCESS);
+}
diff --git a/usr.sbin/pw/pw_log.c b/usr.sbin/pw/pw_log.c
new file mode 100644
index 0000000..29038d9
--- /dev/null
+++ b/usr.sbin/pw/pw_log.c
@@ -0,0 +1,68 @@
+/*-
+ * Copyright (C) 1996
+ * David L. Nugent. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT 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 DAVID L. NUGENT 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.
+ */
+
+#ifndef lint
+static const char rcsid[] =
+ "$FreeBSD$";
+#endif /* not lint */
+
+#include <fcntl.h>
+#include <string.h>
+#include <stdarg.h>
+
+#include "pw.h"
+
+static FILE *logfile = NULL;
+
+void
+pw_log(struct userconf * cnf, int mode, int which, char const * fmt,...)
+{
+ if (cnf->logfile && *cnf->logfile) {
+ if (logfile == NULL) { /* With umask==0 we need to control file access modes on create */
+ int fd = open(cnf->logfile, O_WRONLY | O_CREAT | O_APPEND, 0600);
+
+ if (fd != -1)
+ logfile = fdopen(fd, "a");
+ }
+ if (logfile != NULL) {
+ va_list argp;
+ time_t now = time(NULL);
+ struct tm *t = localtime(&now);
+ char nfmt[256];
+ const char *name;
+
+ if ((name = getenv("LOGNAME")) == NULL && (name = getenv("USER")) == NULL)
+ name = "unknown";
+ /* ISO 8601 International Standard Date format */
+ strftime(nfmt, sizeof nfmt, "%Y-%m-%d %T ", t);
+ sprintf(nfmt + strlen(nfmt), "[%s:%s%s] %s\n", name, Which[which], Modes[mode], fmt);
+ va_start(argp, fmt);
+ vfprintf(logfile, nfmt, argp);
+ va_end(argp);
+ fflush(logfile);
+ }
+ }
+}
diff --git a/usr.sbin/pw/pw_nis.c b/usr.sbin/pw/pw_nis.c
new file mode 100644
index 0000000..6cc361b
--- /dev/null
+++ b/usr.sbin/pw/pw_nis.c
@@ -0,0 +1,95 @@
+/*-
+ * Copyright (C) 1996
+ * David L. Nugent. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT 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 DAVID L. NUGENT 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.
+ */
+
+#ifndef lint
+static const char rcsid[] =
+ "$FreeBSD$";
+#endif /* not lint */
+
+#include <sys/types.h>
+
+#include <err.h>
+#include <pwd.h>
+#include <libutil.h>
+
+#include "pw.h"
+
+static int
+pw_nisupdate(const char * path, struct passwd * pwd, char const * user)
+{
+ int pfd, tfd;
+ struct passwd *pw = NULL;
+ struct passwd *old_pw = NULL;
+
+ printf("===> %s\n", path);
+ if (pwd != NULL)
+ pw = pw_dup(pwd);
+
+ if (user != NULL)
+ old_pw = GETPWNAM(user);
+
+ if (pw_init(NULL, path))
+ err(1,"pw_init()");
+ if ((pfd = pw_lock()) == -1) {
+ pw_fini();
+ err(1, "pw_lock()");
+ }
+ if ((tfd = pw_tmp(-1)) == -1) {
+ pw_fini();
+ err(1, "pw_tmp()");
+ }
+ if (pw_copy(pfd, tfd, pw, old_pw) == -1) {
+ pw_fini();
+ err(1, "pw_copy()");
+ }
+ if (chmod(pw_tempname(), 0644) == -1)
+ err(1, "chmod()");
+ if (rename(pw_tempname(), path) == -1)
+ err(1, "rename()");
+
+ free(pw);
+ pw_fini();
+
+ return (0);
+}
+
+int
+addnispwent(const char *path, struct passwd * pwd)
+{
+ return pw_nisupdate(path, pwd, NULL);
+}
+
+int
+chgnispwent(const char *path, char const * login, struct passwd * pwd)
+{
+ return pw_nisupdate(path, pwd, login);
+}
+
+int
+delnispwent(const char *path, const char *login)
+{
+ return pw_nisupdate(path, NULL, login);
+}
diff --git a/usr.sbin/pw/pw_user.c b/usr.sbin/pw/pw_user.c
new file mode 100644
index 0000000..30a2749
--- /dev/null
+++ b/usr.sbin/pw/pw_user.c
@@ -0,0 +1,1815 @@
+/*-
+ * Copyright (C) 1996
+ * David L. Nugent. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT 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 DAVID L. NUGENT 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.
+ *
+ */
+
+#ifndef lint
+static const char rcsid[] =
+ "$FreeBSD$";
+#endif /* not lint */
+
+#include <sys/param.h>
+#include <sys/resource.h>
+#include <sys/time.h>
+#include <sys/types.h>
+
+#include <ctype.h>
+#include <dirent.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <grp.h>
+#include <pwd.h>
+#include <libutil.h>
+#include <login_cap.h>
+#include <paths.h>
+#include <string.h>
+#include <sysexits.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include "pw.h"
+#include "bitmap.h"
+#include "psdate.h"
+
+#define LOGNAMESIZE (MAXLOGNAME-1)
+
+static char locked_str[] = "*LOCKED*";
+
+static struct passwd fakeuser = {
+ "nouser",
+ "*",
+ -1,
+ -1,
+ 0,
+ "",
+ "User &",
+ "/nonexistent",
+ "/bin/sh",
+ 0,
+ 0
+};
+
+static int print_user(struct passwd *pwd, bool pretty, bool v7);
+static uid_t pw_uidpolicy(struct userconf *cnf, intmax_t id);
+static uid_t pw_gidpolicy(struct userconf *cnf, char *grname, char *nam,
+ gid_t prefer, bool dryrun);
+static char *pw_homepolicy(struct userconf * cnf, char *homedir,
+ const char *user);
+static char *pw_shellpolicy(struct userconf * cnf);
+static char *pw_password(struct userconf * cnf, char const * user,
+ bool dryrun);
+static char *shell_path(char const * path, char *shells[], char *sh);
+static void rmat(uid_t uid);
+static void rmopie(char const * name);
+
+static void
+mkdir_home_parents(int dfd, const char *dir)
+{
+ struct stat st;
+ char *dirs, *tmp;
+
+ if (*dir != '/')
+ errx(EX_DATAERR, "invalid base directory for home '%s'", dir);
+
+ dir++;
+
+ if (fstatat(dfd, dir, &st, 0) != -1) {
+ if (S_ISDIR(st.st_mode))
+ return;
+ errx(EX_OSFILE, "root home `/%s' is not a directory", dir);
+ }
+
+ dirs = strdup(dir);
+ if (dirs == NULL)
+ errx(EX_UNAVAILABLE, "out of memory");
+
+ tmp = strrchr(dirs, '/');
+ if (tmp == NULL) {
+ free(dirs);
+ return;
+ }
+ tmp[0] = '\0';
+
+ /*
+ * This is a kludge especially for Joerg :)
+ * If the home directory would be created in the root partition, then
+ * we really create it under /usr which is likely to have more space.
+ * But we create a symlink from cnf->home -> "/usr" -> cnf->home
+ */
+ if (strchr(dirs, '/') == NULL) {
+ asprintf(&tmp, "usr/%s", dirs);
+ if (tmp == NULL)
+ errx(EX_UNAVAILABLE, "out of memory");
+ if (mkdirat(dfd, tmp, _DEF_DIRMODE) != -1 || errno == EEXIST) {
+ fchownat(dfd, tmp, 0, 0, 0);
+ symlinkat(tmp, dfd, dirs);
+ }
+ free(tmp);
+ }
+ tmp = dirs;
+ if (fstatat(dfd, dirs, &st, 0) == -1) {
+ while ((tmp = strchr(tmp + 1, '/')) != NULL) {
+ *tmp = '\0';
+ if (fstatat(dfd, dirs, &st, 0) == -1) {
+ if (mkdirat(dfd, dirs, _DEF_DIRMODE) == -1)
+ err(EX_OSFILE, "'%s' (root home parent) is not a directory", dirs);
+ }
+ *tmp = '/';
+ }
+ }
+ if (fstatat(dfd, dirs, &st, 0) == -1) {
+ if (mkdirat(dfd, dirs, _DEF_DIRMODE) == -1)
+ err(EX_OSFILE, "'%s' (root home parent) is not a directory", dirs);
+ fchownat(dfd, dirs, 0, 0, 0);
+ }
+
+ free(dirs);
+}
+
+static void
+create_and_populate_homedir(struct userconf *cnf, struct passwd *pwd,
+ const char *skeldir, mode_t homemode, bool update)
+{
+ int skelfd = -1;
+
+ /* Create home parents directories */
+ mkdir_home_parents(conf.rootfd, pwd->pw_dir);
+
+ if (skeldir != NULL && *skeldir != '\0') {
+ if (*skeldir == '/')
+ skeldir++;
+ skelfd = openat(conf.rootfd, skeldir, O_DIRECTORY|O_CLOEXEC);
+ }
+
+ copymkdir(conf.rootfd, pwd->pw_dir, skelfd, homemode, pwd->pw_uid,
+ pwd->pw_gid, 0);
+ pw_log(cnf, update ? M_UPDATE : M_ADD, W_USER, "%s(%ju) home %s made",
+ pwd->pw_name, (uintmax_t)pwd->pw_uid, pwd->pw_dir);
+}
+
+static int
+pw_set_passwd(struct passwd *pwd, int fd, bool precrypted, bool update)
+{
+ int b, istty;
+ struct termios t, n;
+ login_cap_t *lc;
+ char line[_PASSWORD_LEN+1];
+ char *p;
+
+ if (fd == '-') {
+ if (!pwd->pw_passwd || *pwd->pw_passwd != '*') {
+ pwd->pw_passwd = "*"; /* No access */
+ return (1);
+ }
+ return (0);
+ }
+
+ if ((istty = isatty(fd))) {
+ if (tcgetattr(fd, &t) == -1)
+ istty = 0;
+ else {
+ n = t;
+ n.c_lflag &= ~(ECHO);
+ tcsetattr(fd, TCSANOW, &n);
+ printf("%s%spassword for user %s:",
+ update ? "new " : "",
+ precrypted ? "encrypted " : "",
+ pwd->pw_name);
+ fflush(stdout);
+ }
+ }
+ b = read(fd, line, sizeof(line) - 1);
+ if (istty) { /* Restore state */
+ tcsetattr(fd, TCSANOW, &t);
+ fputc('\n', stdout);
+ fflush(stdout);
+ }
+
+ if (b < 0)
+ err(EX_IOERR, "-%c file descriptor",
+ precrypted ? 'H' : 'h');
+ line[b] = '\0';
+ if ((p = strpbrk(line, "\r\n")) != NULL)
+ *p = '\0';
+ if (!*line)
+ errx(EX_DATAERR, "empty password read on file descriptor %d",
+ fd);
+ if (precrypted) {
+ if (strchr(line, ':') != NULL)
+ errx(EX_DATAERR, "bad encrypted password");
+ pwd->pw_passwd = strdup(line);
+ } else {
+ lc = login_getpwclass(pwd);
+ if (lc == NULL ||
+ login_setcryptfmt(lc, "sha512", NULL) == NULL)
+ warn("setting crypt(3) format");
+ login_close(lc);
+ pwd->pw_passwd = pw_pwcrypt(line);
+ }
+ return (1);
+}
+
+static void
+perform_chgpwent(const char *name, struct passwd *pwd, char *nispasswd)
+{
+ int rc;
+ struct passwd *nispwd;
+
+ /* duplicate for nis so that chgpwent is not modifying before NIS */
+ if (nispasswd && *nispasswd == '/')
+ nispwd = pw_dup(pwd);
+
+ rc = chgpwent(name, pwd);
+ if (rc == -1)
+ errx(EX_IOERR, "user '%s' does not exist (NIS?)", pwd->pw_name);
+ else if (rc != 0)
+ err(EX_IOERR, "passwd file update");
+
+ if (nispasswd && *nispasswd == '/') {
+ rc = chgnispwent(nispasswd, name, nispwd);
+ if (rc == -1)
+ warn("User '%s' not found in NIS passwd", pwd->pw_name);
+ else if (rc != 0)
+ warn("NIS passwd update");
+ /* NOTE: NIS-only update errors are not fatal */
+ }
+}
+
+/*
+ * The M_LOCK and M_UNLOCK functions simply add or remove
+ * a "*LOCKED*" prefix from in front of the password to
+ * prevent it decoding correctly, and therefore prevents
+ * access. Of course, this only prevents access via
+ * password authentication (not ssh, kerberos or any
+ * other method that does not use the UNIX password) but
+ * that is a known limitation.
+ */
+static int
+pw_userlock(char *arg1, int mode)
+{
+ struct passwd *pwd = NULL;
+ char *passtmp = NULL;
+ char *name;
+ bool locked = false;
+ uid_t id = (uid_t)-1;
+
+ if (geteuid() != 0)
+ errx(EX_NOPERM, "you must be root");
+
+ if (arg1 == NULL)
+ errx(EX_DATAERR, "username or id required");
+
+ name = arg1;
+ if (arg1[strspn(name, "0123456789")] == '\0')
+ id = pw_checkid(name, UID_MAX);
+
+ pwd = GETPWNAM(pw_checkname(name, 0));
+ if (pwd == NULL && id != (uid_t)-1) {
+ pwd = GETPWUID(id);
+ if (pwd != NULL)
+ name = pwd->pw_name;
+ }
+ if (pwd == NULL) {
+ if (id == (uid_t)-1)
+ errx(EX_NOUSER, "no such name or uid `%ju'", (uintmax_t) id);
+ errx(EX_NOUSER, "no such user `%s'", name);
+ }
+
+ if (name == NULL)
+ name = pwd->pw_name;
+
+ if (strncmp(pwd->pw_passwd, locked_str, sizeof(locked_str) -1) == 0)
+ locked = true;
+ if (mode == M_LOCK && locked)
+ errx(EX_DATAERR, "user '%s' is already locked", pwd->pw_name);
+ if (mode == M_UNLOCK && !locked)
+ errx(EX_DATAERR, "user '%s' is not locked", pwd->pw_name);
+
+ if (mode == M_LOCK) {
+ asprintf(&passtmp, "%s%s", locked_str, pwd->pw_passwd);
+ if (passtmp == NULL) /* disaster */
+ errx(EX_UNAVAILABLE, "out of memory");
+ pwd->pw_passwd = passtmp;
+ } else {
+ pwd->pw_passwd += sizeof(locked_str)-1;
+ }
+
+ perform_chgpwent(name, pwd, NULL);
+ free(passtmp);
+
+ return (EXIT_SUCCESS);
+}
+
+static uid_t
+pw_uidpolicy(struct userconf * cnf, intmax_t id)
+{
+ struct passwd *pwd;
+ struct bitmap bm;
+ uid_t uid = (uid_t) - 1;
+
+ /*
+ * Check the given uid, if any
+ */
+ if (id >= 0) {
+ uid = (uid_t) id;
+
+ if ((pwd = GETPWUID(uid)) != NULL && conf.checkduplicate)
+ errx(EX_DATAERR, "uid `%ju' has already been allocated",
+ (uintmax_t)pwd->pw_uid);
+ return (uid);
+ }
+ /*
+ * We need to allocate the next available uid under one of
+ * two policies a) Grab the first unused uid b) Grab the
+ * highest possible unused uid
+ */
+ if (cnf->min_uid >= cnf->max_uid) { /* Sanity
+ * claus^H^H^H^Hheck */
+ cnf->min_uid = 1000;
+ cnf->max_uid = 32000;
+ }
+ bm = bm_alloc(cnf->max_uid - cnf->min_uid + 1);
+
+ /*
+ * Now, let's fill the bitmap from the password file
+ */
+ SETPWENT();
+ while ((pwd = GETPWENT()) != NULL)
+ if (pwd->pw_uid >= (uid_t) cnf->min_uid && pwd->pw_uid <= (uid_t) cnf->max_uid)
+ bm_setbit(&bm, pwd->pw_uid - cnf->min_uid);
+ ENDPWENT();
+
+ /*
+ * Then apply the policy, with fallback to reuse if necessary
+ */
+ if (cnf->reuse_uids || (uid = (uid_t) (bm_lastset(&bm) + cnf->min_uid + 1)) > cnf->max_uid)
+ uid = (uid_t) (bm_firstunset(&bm) + cnf->min_uid);
+
+ /*
+ * Another sanity check
+ */
+ if (uid < cnf->min_uid || uid > cnf->max_uid)
+ errx(EX_SOFTWARE, "unable to allocate a new uid - range fully used");
+ bm_dealloc(&bm);
+ return (uid);
+}
+
+static uid_t
+pw_gidpolicy(struct userconf *cnf, char *grname, char *nam, gid_t prefer, bool dryrun)
+{
+ struct group *grp;
+ gid_t gid = (uid_t) - 1;
+
+ /*
+ * Check the given gid, if any
+ */
+ SETGRENT();
+ if (grname) {
+ if ((grp = GETGRNAM(grname)) == NULL) {
+ gid = pw_checkid(grname, GID_MAX);
+ grp = GETGRGID(gid);
+ }
+ gid = grp->gr_gid;
+ } else if ((grp = GETGRNAM(nam)) != NULL &&
+ (grp->gr_mem == NULL || grp->gr_mem[0] == NULL)) {
+ gid = grp->gr_gid; /* Already created? Use it anyway... */
+ } else {
+ intmax_t grid = -1;
+
+ /*
+ * We need to auto-create a group with the user's name. We
+ * can send all the appropriate output to our sister routine
+ * bit first see if we can create a group with gid==uid so we
+ * can keep the user and group ids in sync. We purposely do
+ * NOT check the gid range if we can force the sync. If the
+ * user's name dups an existing group, then the group add
+ * function will happily handle that case for us and exit.
+ */
+ if (GETGRGID(prefer) == NULL)
+ grid = prefer;
+ if (dryrun) {
+ gid = pw_groupnext(cnf, true);
+ } else {
+ if (grid == -1)
+ grid = pw_groupnext(cnf, true);
+ groupadd(cnf, nam, grid, NULL, -1, false, false, false);
+ if ((grp = GETGRNAM(nam)) != NULL)
+ gid = grp->gr_gid;
+ }
+ }
+ ENDGRENT();
+ return (gid);
+}
+
+static char *
+pw_homepolicy(struct userconf * cnf, char *homedir, const char *user)
+{
+ static char home[128];
+
+ if (homedir)
+ return (homedir);
+
+ if (cnf->home == NULL || *cnf->home == '\0')
+ errx(EX_CONFIG, "no base home directory set");
+ snprintf(home, sizeof(home), "%s/%s", cnf->home, user);
+
+ return (home);
+}
+
+static char *
+shell_path(char const * path, char *shells[], char *sh)
+{
+ if (sh != NULL && (*sh == '/' || *sh == '\0'))
+ return sh; /* specified full path or forced none */
+ else {
+ char *p;
+ char paths[_UC_MAXLINE];
+
+ /*
+ * We need to search paths
+ */
+ strlcpy(paths, path, sizeof(paths));
+ for (p = strtok(paths, ": \t\r\n"); p != NULL; p = strtok(NULL, ": \t\r\n")) {
+ int i;
+ static char shellpath[256];
+
+ if (sh != NULL) {
+ snprintf(shellpath, sizeof(shellpath), "%s/%s", p, sh);
+ if (access(shellpath, X_OK) == 0)
+ return shellpath;
+ } else
+ for (i = 0; i < _UC_MAXSHELLS && shells[i] != NULL; i++) {
+ snprintf(shellpath, sizeof(shellpath), "%s/%s", p, shells[i]);
+ if (access(shellpath, X_OK) == 0)
+ return shellpath;
+ }
+ }
+ if (sh == NULL)
+ errx(EX_OSFILE, "can't find shell `%s' in shell paths", sh);
+ errx(EX_CONFIG, "no default shell available or defined");
+ return NULL;
+ }
+}
+
+static char *
+pw_shellpolicy(struct userconf * cnf)
+{
+
+ return shell_path(cnf->shelldir, cnf->shells, cnf->shell_default);
+}
+
+#define SALTSIZE 32
+
+static char const chars[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ./";
+
+char *
+pw_pwcrypt(char *password)
+{
+ int i;
+ char salt[SALTSIZE + 1];
+ char *cryptpw;
+ static char buf[256];
+
+ /*
+ * Calculate a salt value
+ */
+ for (i = 0; i < SALTSIZE; i++)
+ salt[i] = chars[arc4random_uniform(sizeof(chars) - 1)];
+ salt[SALTSIZE] = '\0';
+
+ cryptpw = crypt(password, salt);
+ if (cryptpw == NULL)
+ errx(EX_CONFIG, "crypt(3) failure");
+ return strcpy(buf, cryptpw);
+}
+
+static char *
+pw_password(struct userconf * cnf, char const * user, bool dryrun)
+{
+ int i, l;
+ char pwbuf[32];
+
+ switch (cnf->default_password) {
+ case -1: /* Random password */
+ l = (arc4random() % 8 + 8); /* 8 - 16 chars */
+ for (i = 0; i < l; i++)
+ pwbuf[i] = chars[arc4random_uniform(sizeof(chars)-1)];
+ pwbuf[i] = '\0';
+
+ /*
+ * We give this information back to the user
+ */
+ if (conf.fd == -1 && !dryrun) {
+ if (isatty(STDOUT_FILENO))
+ printf("Password for '%s' is: ", user);
+ printf("%s\n", pwbuf);
+ fflush(stdout);
+ }
+ break;
+
+ case -2: /* No password at all! */
+ return "";
+
+ case 0: /* No login - default */
+ default:
+ return "*";
+
+ case 1: /* user's name */
+ strlcpy(pwbuf, user, sizeof(pwbuf));
+ break;
+ }
+ return pw_pwcrypt(pwbuf);
+}
+
+static int
+print_user(struct passwd * pwd, bool pretty, bool v7)
+{
+ int j;
+ char *p;
+ struct group *grp = GETGRGID(pwd->pw_gid);
+ char uname[60] = "User &", office[60] = "[None]",
+ wphone[60] = "[None]", hphone[60] = "[None]";
+ char acexpire[32] = "[None]", pwexpire[32] = "[None]";
+ struct tm * tptr;
+
+ if (!pretty) {
+ p = v7 ? pw_make_v7(pwd) : pw_make(pwd);
+ printf("%s\n", p);
+ free(p);
+ return (EXIT_SUCCESS);
+ }
+
+ if ((p = strtok(pwd->pw_gecos, ",")) != NULL) {
+ strlcpy(uname, p, sizeof(uname));
+ if ((p = strtok(NULL, ",")) != NULL) {
+ strlcpy(office, p, sizeof(office));
+ if ((p = strtok(NULL, ",")) != NULL) {
+ strlcpy(wphone, p, sizeof(wphone));
+ if ((p = strtok(NULL, "")) != NULL) {
+ strlcpy(hphone, p, sizeof(hphone));
+ }
+ }
+ }
+ }
+ /*
+ * Handle '&' in gecos field
+ */
+ if ((p = strchr(uname, '&')) != NULL) {
+ int l = strlen(pwd->pw_name);
+ int m = strlen(p);
+
+ memmove(p + l, p + 1, m);
+ memmove(p, pwd->pw_name, l);
+ *p = (char) toupper((unsigned char)*p);
+ }
+ if (pwd->pw_expire > (time_t)0 && (tptr = localtime(&pwd->pw_expire)) != NULL)
+ strftime(acexpire, sizeof acexpire, "%c", tptr);
+ if (pwd->pw_change > (time_t)0 && (tptr = localtime(&pwd->pw_change)) != NULL)
+ strftime(pwexpire, sizeof pwexpire, "%c", tptr);
+ printf("Login Name: %-15s #%-12ju Group: %-15s #%ju\n"
+ " Full Name: %s\n"
+ " Home: %-26.26s Class: %s\n"
+ " Shell: %-26.26s Office: %s\n"
+ "Work Phone: %-26.26s Home Phone: %s\n"
+ "Acc Expire: %-26.26s Pwd Expire: %s\n",
+ pwd->pw_name, (uintmax_t)pwd->pw_uid,
+ grp ? grp->gr_name : "(invalid)", (uintmax_t)pwd->pw_gid,
+ uname, pwd->pw_dir, pwd->pw_class,
+ pwd->pw_shell, office, wphone, hphone,
+ acexpire, pwexpire);
+ SETGRENT();
+ j = 0;
+ while ((grp=GETGRENT()) != NULL) {
+ int i = 0;
+ if (grp->gr_mem != NULL) {
+ while (grp->gr_mem[i] != NULL) {
+ if (strcmp(grp->gr_mem[i], pwd->pw_name)==0) {
+ printf(j++ == 0 ? " Groups: %s" : ",%s", grp->gr_name);
+ break;
+ }
+ ++i;
+ }
+ }
+ }
+ ENDGRENT();
+ printf("%s", j ? "\n" : "");
+ return (EXIT_SUCCESS);
+}
+
+char *
+pw_checkname(char *name, int gecos)
+{
+ char showch[8];
+ const char *badchars, *ch, *showtype;
+ int reject;
+
+ ch = name;
+ reject = 0;
+ if (gecos) {
+ /* See if the name is valid as a gecos (comment) field. */
+ badchars = ":!@";
+ showtype = "gecos field";
+ } else {
+ /* See if the name is valid as a userid or group. */
+ badchars = " ,\t:+&#%$^()!@~*?<>=|\\/\"";
+ showtype = "userid/group name";
+ /* Userids and groups can not have a leading '-'. */
+ if (*ch == '-')
+ reject = 1;
+ }
+ if (!reject) {
+ while (*ch) {
+ if (strchr(badchars, *ch) != NULL ||
+ (!gecos && *ch < ' ') ||
+ *ch == 127) {
+ reject = 1;
+ break;
+ }
+ /* 8-bit characters are only allowed in GECOS fields */
+ if (!gecos && (*ch & 0x80)) {
+ reject = 1;
+ break;
+ }
+ ch++;
+ }
+ }
+ /*
+ * A `$' is allowed as the final character for userids and groups,
+ * mainly for the benefit of samba.
+ */
+ if (reject && !gecos) {
+ if (*ch == '$' && *(ch + 1) == '\0') {
+ reject = 0;
+ ch++;
+ }
+ }
+ if (reject) {
+ snprintf(showch, sizeof(showch), (*ch >= ' ' && *ch < 127)
+ ? "`%c'" : "0x%02x", *ch);
+ errx(EX_DATAERR, "invalid character %s at position %td in %s",
+ showch, (ch - name), showtype);
+ }
+ if (!gecos && (ch - name) > LOGNAMESIZE)
+ errx(EX_USAGE, "name too long `%s' (max is %d)", name,
+ LOGNAMESIZE);
+
+ return (name);
+}
+
+static void
+rmat(uid_t uid)
+{
+ DIR *d = opendir("/var/at/jobs");
+
+ if (d != NULL) {
+ struct dirent *e;
+
+ while ((e = readdir(d)) != NULL) {
+ struct stat st;
+
+ if (strncmp(e->d_name, ".lock", 5) != 0 &&
+ stat(e->d_name, &st) == 0 &&
+ !S_ISDIR(st.st_mode) &&
+ st.st_uid == uid) {
+ char tmp[MAXPATHLEN];
+
+ snprintf(tmp, sizeof(tmp), "/usr/bin/atrm %s",
+ e->d_name);
+ system(tmp);
+ }
+ }
+ closedir(d);
+ }
+}
+
+static void
+rmopie(char const * name)
+{
+ char tmp[1014];
+ FILE *fp;
+ int fd;
+ size_t len;
+ off_t atofs = 0;
+
+ if ((fd = openat(conf.rootfd, "etc/opiekeys", O_RDWR)) == -1)
+ return;
+
+ fp = fdopen(fd, "r+");
+ len = strlen(name);
+
+ while (fgets(tmp, sizeof(tmp), fp) != NULL) {
+ if (strncmp(name, tmp, len) == 0 && tmp[len]==' ') {
+ /* Comment username out */
+ if (fseek(fp, atofs, SEEK_SET) == 0)
+ fwrite("#", 1, 1, fp);
+ break;
+ }
+ atofs = ftell(fp);
+ }
+ /*
+ * If we got an error of any sort, don't update!
+ */
+ fclose(fp);
+}
+
+int
+pw_user_next(int argc, char **argv, char *name __unused)
+{
+ struct userconf *cnf = NULL;
+ const char *cfg = NULL;
+ int ch;
+ bool quiet = false;
+ uid_t next;
+
+ while ((ch = getopt(argc, argv, "Cq")) != -1) {
+ switch (ch) {
+ case 'C':
+ cfg = optarg;
+ break;
+ case 'q':
+ quiet = true;
+ break;
+ }
+ }
+
+ if (quiet)
+ freopen(_PATH_DEVNULL, "w", stderr);
+
+ cnf = get_userconfig(cfg);
+
+ next = pw_uidpolicy(cnf, -1);
+
+ printf("%ju:", (uintmax_t)next);
+ pw_groupnext(cnf, quiet);
+
+ return (EXIT_SUCCESS);
+}
+
+int
+pw_user_show(int argc, char **argv, char *arg1)
+{
+ struct passwd *pwd = NULL;
+ char *name = NULL;
+ intmax_t id = -1;
+ int ch;
+ bool all = false;
+ bool pretty = false;
+ bool force = false;
+ bool v7 = false;
+ bool quiet = false;
+
+ if (arg1 != NULL) {
+ if (arg1[strspn(arg1, "0123456789")] == '\0')
+ id = pw_checkid(arg1, UID_MAX);
+ else
+ name = arg1;
+ }
+
+ while ((ch = getopt(argc, argv, "C:qn:u:FPa7")) != -1) {
+ switch (ch) {
+ case 'C':
+ /* ignore compatibility */
+ break;
+ case 'q':
+ quiet = true;
+ break;
+ case 'n':
+ name = optarg;
+ break;
+ case 'u':
+ id = pw_checkid(optarg, UID_MAX);
+ break;
+ case 'F':
+ force = true;
+ break;
+ case 'P':
+ pretty = true;
+ break;
+ case 'a':
+ all = true;
+ break;
+ case '7':
+ v7 = true;
+ break;
+ }
+ }
+
+ if (quiet)
+ freopen(_PATH_DEVNULL, "w", stderr);
+
+ if (all) {
+ SETPWENT();
+ while ((pwd = GETPWENT()) != NULL)
+ print_user(pwd, pretty, v7);
+ ENDPWENT();
+ return (EXIT_SUCCESS);
+ }
+
+ if (id < 0 && name == NULL)
+ errx(EX_DATAERR, "username or id required");
+
+ pwd = (name != NULL) ? GETPWNAM(pw_checkname(name, 0)) : GETPWUID(id);
+ if (pwd == NULL) {
+ if (force) {
+ pwd = &fakeuser;
+ } else {
+ if (name == NULL)
+ errx(EX_NOUSER, "no such uid `%ju'",
+ (uintmax_t) id);
+ errx(EX_NOUSER, "no such user `%s'", name);
+ }
+ }
+
+ return (print_user(pwd, pretty, v7));
+}
+
+int
+pw_user_del(int argc, char **argv, char *arg1)
+{
+ struct userconf *cnf = NULL;
+ struct passwd *pwd = NULL;
+ struct group *gr, *grp;
+ char *name = NULL;
+ char grname[MAXLOGNAME];
+ char *nispasswd = NULL;
+ char file[MAXPATHLEN];
+ char home[MAXPATHLEN];
+ const char *cfg = NULL;
+ struct stat st;
+ intmax_t id = -1;
+ int ch, rc;
+ bool nis = false;
+ bool deletehome = false;
+ bool quiet = false;
+
+ if (arg1 != NULL) {
+ if (arg1[strspn(arg1, "0123456789")] == '\0')
+ id = pw_checkid(arg1, UID_MAX);
+ else
+ name = arg1;
+ }
+
+ while ((ch = getopt(argc, argv, "C:qn:u:rYy:")) != -1) {
+ switch (ch) {
+ case 'C':
+ cfg = optarg;
+ break;
+ case 'q':
+ quiet = true;
+ break;
+ case 'n':
+ name = optarg;
+ break;
+ case 'u':
+ id = pw_checkid(optarg, UID_MAX);
+ break;
+ case 'r':
+ deletehome = true;
+ break;
+ case 'y':
+ nispasswd = optarg;
+ break;
+ case 'Y':
+ nis = true;
+ break;
+ }
+ }
+
+ if (quiet)
+ freopen(_PATH_DEVNULL, "w", stderr);
+
+ if (id < 0 && name == NULL)
+ errx(EX_DATAERR, "username or id required");
+
+ cnf = get_userconfig(cfg);
+
+ if (nispasswd == NULL)
+ nispasswd = cnf->nispasswd;
+
+ pwd = (name != NULL) ? GETPWNAM(pw_checkname(name, 0)) : GETPWUID(id);
+ if (pwd == NULL) {
+ if (name == NULL)
+ errx(EX_NOUSER, "no such uid `%ju'", (uintmax_t) id);
+ errx(EX_NOUSER, "no such user `%s'", name);
+ }
+
+ if (PWF._altdir == PWF_REGULAR &&
+ ((pwd->pw_fields & _PWF_SOURCE) != _PWF_FILES)) {
+ if ((pwd->pw_fields & _PWF_SOURCE) == _PWF_NIS) {
+ if (!nis && nispasswd && *nispasswd != '/')
+ errx(EX_NOUSER, "Cannot remove NIS user `%s'",
+ name);
+ } else {
+ errx(EX_NOUSER, "Cannot remove non local user `%s'",
+ name);
+ }
+ }
+
+ id = pwd->pw_uid;
+ if (name == NULL)
+ name = pwd->pw_name;
+
+ if (strcmp(pwd->pw_name, "root") == 0)
+ errx(EX_DATAERR, "cannot remove user 'root'");
+
+ /* Remove opie record from /etc/opiekeys */
+ if (PWALTDIR() != PWF_ALT)
+ rmopie(pwd->pw_name);
+
+ if (!PWALTDIR()) {
+ /* Remove crontabs */
+ snprintf(file, sizeof(file), "/var/cron/tabs/%s", pwd->pw_name);
+ if (access(file, F_OK) == 0) {
+ snprintf(file, sizeof(file), "crontab -u %s -r",
+ pwd->pw_name);
+ system(file);
+ }
+ }
+
+ /*
+ * Save these for later, since contents of pwd may be
+ * invalidated by deletion
+ */
+ snprintf(file, sizeof(file), "%s/%s", _PATH_MAILDIR, pwd->pw_name);
+ strlcpy(home, pwd->pw_dir, sizeof(home));
+ gr = GETGRGID(pwd->pw_gid);
+ if (gr != NULL)
+ strlcpy(grname, gr->gr_name, LOGNAMESIZE);
+ else
+ grname[0] = '\0';
+
+ rc = delpwent(pwd);
+ if (rc == -1)
+ err(EX_IOERR, "user '%s' does not exist", pwd->pw_name);
+ else if (rc != 0)
+ err(EX_IOERR, "passwd update");
+
+ if (nis && nispasswd && *nispasswd=='/') {
+ rc = delnispwent(nispasswd, name);
+ if (rc == -1)
+ warnx("WARNING: user '%s' does not exist in NIS passwd",
+ pwd->pw_name);
+ else if (rc != 0)
+ warn("WARNING: NIS passwd update");
+ }
+
+ grp = GETGRNAM(name);
+ if (grp != NULL &&
+ (grp->gr_mem == NULL || *grp->gr_mem == NULL) &&
+ strcmp(name, grname) == 0)
+ delgrent(GETGRNAM(name));
+ SETGRENT();
+ while ((grp = GETGRENT()) != NULL) {
+ int i, j;
+ char group[MAXLOGNAME];
+ if (grp->gr_mem == NULL)
+ continue;
+
+ for (i = 0; grp->gr_mem[i] != NULL; i++) {
+ if (strcmp(grp->gr_mem[i], name) != 0)
+ continue;
+
+ for (j = i; grp->gr_mem[j] != NULL; j++)
+ grp->gr_mem[j] = grp->gr_mem[j+1];
+ strlcpy(group, grp->gr_name, MAXLOGNAME);
+ chggrent(group, grp);
+ }
+ }
+ ENDGRENT();
+
+ pw_log(cnf, M_DELETE, W_USER, "%s(%ju) account removed", name,
+ (uintmax_t)id);
+
+ /* Remove mail file */
+ if (PWALTDIR() != PWF_ALT)
+ unlinkat(conf.rootfd, file + 1, 0);
+
+ /* Remove at jobs */
+ if (!PWALTDIR() && getpwuid(id) == NULL)
+ rmat(id);
+
+ /* Remove home directory and contents */
+ if (PWALTDIR() != PWF_ALT && deletehome && *home == '/' &&
+ GETPWUID(id) == NULL &&
+ fstatat(conf.rootfd, home + 1, &st, 0) != -1) {
+ rm_r(conf.rootfd, home, id);
+ pw_log(cnf, M_DELETE, W_USER, "%s(%ju) home '%s' %s"
+ "removed", name, (uintmax_t)id, home,
+ fstatat(conf.rootfd, home + 1, &st, 0) == -1 ? "" : "not "
+ "completely ");
+ }
+
+ return (EXIT_SUCCESS);
+}
+
+int
+pw_user_lock(int argc, char **argv, char *arg1)
+{
+ int ch;
+
+ while ((ch = getopt(argc, argv, "Cq")) != -1) {
+ switch (ch) {
+ case 'C':
+ case 'q':
+ /* compatibility */
+ break;
+ }
+ }
+
+ return (pw_userlock(arg1, M_LOCK));
+}
+
+int
+pw_user_unlock(int argc, char **argv, char *arg1)
+{
+ int ch;
+
+ while ((ch = getopt(argc, argv, "Cq")) != -1) {
+ switch (ch) {
+ case 'C':
+ case 'q':
+ /* compatibility */
+ break;
+ }
+ }
+
+ return (pw_userlock(arg1, M_UNLOCK));
+}
+
+static struct group *
+group_from_name_or_id(char *name)
+{
+ const char *errstr = NULL;
+ struct group *grp;
+ uintmax_t id;
+
+ if ((grp = GETGRNAM(name)) == NULL) {
+ id = strtounum(name, 0, GID_MAX, &errstr);
+ if (errstr)
+ errx(EX_NOUSER, "group `%s' does not exist", name);
+ grp = GETGRGID(id);
+ if (grp == NULL)
+ errx(EX_NOUSER, "group `%s' does not exist", name);
+ }
+
+ return (grp);
+}
+
+static void
+split_groups(StringList **groups, char *groupsstr)
+{
+ struct group *grp;
+ char *p;
+ char tok[] = ", \t";
+
+ for (p = strtok(groupsstr, tok); p != NULL; p = strtok(NULL, tok)) {
+ grp = group_from_name_or_id(p);
+ if (*groups == NULL)
+ *groups = sl_init();
+ sl_add(*groups, newstr(grp->gr_name));
+ }
+}
+
+static void
+validate_grname(struct userconf *cnf, char *group)
+{
+ struct group *grp;
+
+ if (group == NULL || *group == '\0') {
+ cnf->default_group = "";
+ return;
+ }
+ grp = group_from_name_or_id(group);
+ cnf->default_group = newstr(grp->gr_name);
+}
+
+static mode_t
+validate_mode(char *mode)
+{
+ mode_t m;
+ void *set;
+
+ if ((set = setmode(mode)) == NULL)
+ errx(EX_DATAERR, "invalid directory creation mode '%s'", mode);
+
+ m = getmode(set, _DEF_DIRMODE);
+ free(set);
+ return (m);
+}
+
+static void
+mix_config(struct userconf *cmdcnf, struct userconf *cfg)
+{
+
+ if (cmdcnf->default_password == 0)
+ cmdcnf->default_password = cfg->default_password;
+ if (cmdcnf->reuse_uids == 0)
+ cmdcnf->reuse_uids = cfg->reuse_uids;
+ if (cmdcnf->reuse_gids == 0)
+ cmdcnf->reuse_gids = cfg->reuse_gids;
+ if (cmdcnf->nispasswd == NULL)
+ cmdcnf->nispasswd = cfg->nispasswd;
+ if (cmdcnf->dotdir == NULL)
+ cmdcnf->dotdir = cfg->dotdir;
+ if (cmdcnf->newmail == NULL)
+ cmdcnf->newmail = cfg->newmail;
+ if (cmdcnf->logfile == NULL)
+ cmdcnf->logfile = cfg->logfile;
+ if (cmdcnf->home == NULL)
+ cmdcnf->home = cfg->home;
+ if (cmdcnf->homemode == 0)
+ cmdcnf->homemode = cfg->homemode;
+ if (cmdcnf->shelldir == NULL)
+ cmdcnf->shelldir = cfg->shelldir;
+ if (cmdcnf->shells == NULL)
+ cmdcnf->shells = cfg->shells;
+ if (cmdcnf->shell_default == NULL)
+ cmdcnf->shell_default = cfg->shell_default;
+ if (cmdcnf->default_group == NULL)
+ cmdcnf->default_group = cfg->default_group;
+ if (cmdcnf->groups == NULL)
+ cmdcnf->groups = cfg->groups;
+ if (cmdcnf->default_class == NULL)
+ cmdcnf->default_class = cfg->default_class;
+ if (cmdcnf->min_uid == 0)
+ cmdcnf->min_uid = cfg->min_uid;
+ if (cmdcnf->max_uid == 0)
+ cmdcnf->max_uid = cfg->max_uid;
+ if (cmdcnf->min_gid == 0)
+ cmdcnf->min_gid = cfg->min_gid;
+ if (cmdcnf->max_gid == 0)
+ cmdcnf->max_gid = cfg->max_gid;
+ if (cmdcnf->expire_days == 0)
+ cmdcnf->expire_days = cfg->expire_days;
+ if (cmdcnf->password_days == 0)
+ cmdcnf->password_days = cfg->password_days;
+}
+
+int
+pw_user_add(int argc, char **argv, char *arg1)
+{
+ struct userconf *cnf, *cmdcnf;
+ struct passwd *pwd;
+ struct group *grp;
+ struct stat st;
+ char args[] = "C:qn:u:c:d:e:p:g:G:mM:k:s:oL:i:w:h:H:Db:NPy:Y";
+ char line[_PASSWORD_LEN+1], path[MAXPATHLEN];
+ char *gecos, *homedir, *skel, *walk, *userid, *groupid, *grname;
+ char *default_passwd, *name, *p;
+ const char *cfg;
+ login_cap_t *lc;
+ FILE *pfp, *fp;
+ intmax_t id = -1;
+ time_t now;
+ int rc, ch, fd = -1;
+ size_t i;
+ bool dryrun, nis, pretty, quiet, createhome, precrypted, genconf;
+
+ dryrun = nis = pretty = quiet = createhome = precrypted = false;
+ genconf = false;
+ gecos = homedir = skel = userid = groupid = default_passwd = NULL;
+ grname = name = NULL;
+
+ if ((cmdcnf = calloc(1, sizeof(struct userconf))) == NULL)
+ err(EXIT_FAILURE, "calloc()");
+
+ if (arg1 != NULL) {
+ if (arg1[strspn(arg1, "0123456789")] == '\0')
+ id = pw_checkid(arg1, UID_MAX);
+ else
+ name = arg1;
+ }
+
+ while ((ch = getopt(argc, argv, args)) != -1) {
+ switch (ch) {
+ case 'C':
+ cfg = optarg;
+ break;
+ case 'q':
+ quiet = true;
+ break;
+ case 'n':
+ name = optarg;
+ break;
+ case 'u':
+ userid = optarg;
+ break;
+ case 'c':
+ gecos = pw_checkname(optarg, 1);
+ break;
+ case 'd':
+ homedir = optarg;
+ break;
+ case 'e':
+ now = time(NULL);
+ cmdcnf->expire_days = parse_date(now, optarg);
+ break;
+ case 'p':
+ now = time(NULL);
+ cmdcnf->password_days = parse_date(now, optarg);
+ break;
+ case 'g':
+ validate_grname(cmdcnf, optarg);
+ grname = optarg;
+ break;
+ case 'G':
+ split_groups(&cmdcnf->groups, optarg);
+ break;
+ case 'm':
+ createhome = true;
+ break;
+ case 'M':
+ cmdcnf->homemode = validate_mode(optarg);
+ break;
+ case 'k':
+ walk = skel = optarg;
+ if (*walk == '/')
+ walk++;
+ if (fstatat(conf.rootfd, walk, &st, 0) == -1)
+ errx(EX_OSFILE, "skeleton `%s' does not "
+ "exists", skel);
+ if (!S_ISDIR(st.st_mode))
+ errx(EX_OSFILE, "skeleton `%s' is not a "
+ "directory", skel);
+ cmdcnf->dotdir = skel;
+ break;
+ case 's':
+ cmdcnf->shell_default = optarg;
+ break;
+ case 'o':
+ conf.checkduplicate = false;
+ break;
+ case 'L':
+ cmdcnf->default_class = pw_checkname(optarg, 0);
+ break;
+ case 'i':
+ groupid = optarg;
+ break;
+ case 'w':
+ default_passwd = optarg;
+ break;
+ case 'H':
+ if (fd != -1)
+ errx(EX_USAGE, "'-h' and '-H' are mutually "
+ "exclusive options");
+ fd = pw_checkfd(optarg);
+ precrypted = true;
+ if (fd == '-')
+ errx(EX_USAGE, "-H expects a file descriptor");
+ break;
+ case 'h':
+ if (fd != -1)
+ errx(EX_USAGE, "'-h' and '-H' are mutually "
+ "exclusive options");
+ fd = pw_checkfd(optarg);
+ break;
+ case 'D':
+ genconf = true;
+ break;
+ case 'b':
+ cmdcnf->home = optarg;
+ break;
+ case 'N':
+ dryrun = true;
+ break;
+ case 'P':
+ pretty = true;
+ break;
+ case 'y':
+ cmdcnf->nispasswd = optarg;
+ break;
+ case 'Y':
+ nis = true;
+ break;
+ }
+ }
+
+ if (geteuid() != 0 && ! dryrun)
+ errx(EX_NOPERM, "you must be root");
+
+ if (quiet)
+ freopen(_PATH_DEVNULL, "w", stderr);
+
+ cnf = get_userconfig(cfg);
+
+ mix_config(cmdcnf, cnf);
+ if (default_passwd)
+ cmdcnf->default_password = boolean_val(default_passwd,
+ cnf->default_password);
+ if (genconf) {
+ if (name != NULL)
+ errx(EX_DATAERR, "can't combine `-D' with `-n name'");
+ if (userid != NULL) {
+ if ((p = strtok(userid, ", \t")) != NULL)
+ cmdcnf->min_uid = pw_checkid(p, UID_MAX);
+ if (cmdcnf->min_uid == 0)
+ cmdcnf->min_uid = 1000;
+ if ((p = strtok(NULL, " ,\t")) != NULL)
+ cmdcnf->max_uid = pw_checkid(p, UID_MAX);
+ if (cmdcnf->max_uid == 0)
+ cmdcnf->max_uid = 32000;
+ }
+ if (groupid != NULL) {
+ if ((p = strtok(groupid, ", \t")) != NULL)
+ cmdcnf->min_gid = pw_checkid(p, GID_MAX);
+ if (cmdcnf->min_gid == 0)
+ cmdcnf->min_gid = 1000;
+ if ((p = strtok(NULL, " ,\t")) != NULL)
+ cmdcnf->max_gid = pw_checkid(p, GID_MAX);
+ if (cmdcnf->max_gid == 0)
+ cmdcnf->max_gid = 32000;
+ }
+ if (write_userconfig(cmdcnf, cfg))
+ return (EXIT_SUCCESS);
+ err(EX_IOERR, "config update");
+ }
+
+ if (userid)
+ id = pw_checkid(userid, UID_MAX);
+ if (id < 0 && name == NULL)
+ errx(EX_DATAERR, "user name or id required");
+
+ if (name == NULL)
+ errx(EX_DATAERR, "login name required");
+
+ if (GETPWNAM(name) != NULL)
+ errx(EX_DATAERR, "login name `%s' already exists", name);
+
+ pwd = &fakeuser;
+ pwd->pw_name = name;
+ pwd->pw_class = cmdcnf->default_class ? cmdcnf->default_class : "";
+ pwd->pw_uid = pw_uidpolicy(cmdcnf, id);
+ pwd->pw_gid = pw_gidpolicy(cnf, grname, pwd->pw_name,
+ (gid_t) pwd->pw_uid, dryrun);
+ pwd->pw_change = cmdcnf->password_days;
+ pwd->pw_expire = cmdcnf->expire_days;
+ pwd->pw_dir = pw_homepolicy(cmdcnf, homedir, pwd->pw_name);
+ pwd->pw_shell = pw_shellpolicy(cmdcnf);
+ lc = login_getpwclass(pwd);
+ if (lc == NULL || login_setcryptfmt(lc, "sha512", NULL) == NULL)
+ warn("setting crypt(3) format");
+ login_close(lc);
+ pwd->pw_passwd = pw_password(cmdcnf, pwd->pw_name, dryrun);
+ if (pwd->pw_uid == 0 && strcmp(pwd->pw_name, "root") != 0)
+ warnx("WARNING: new account `%s' has a uid of 0 "
+ "(superuser access!)", pwd->pw_name);
+ if (gecos)
+ pwd->pw_gecos = gecos;
+
+ if (fd != -1)
+ pw_set_passwd(pwd, fd, precrypted, false);
+
+ if (dryrun)
+ return (print_user(pwd, pretty, false));
+
+ if ((rc = addpwent(pwd)) != 0) {
+ if (rc == -1)
+ errx(EX_IOERR, "user '%s' already exists",
+ pwd->pw_name);
+ else if (rc != 0)
+ err(EX_IOERR, "passwd file update");
+ }
+ if (nis && cmdcnf->nispasswd && *cmdcnf->nispasswd == '/') {
+ printf("%s\n", cmdcnf->nispasswd);
+ rc = addnispwent(cmdcnf->nispasswd, pwd);
+ if (rc == -1)
+ warnx("User '%s' already exists in NIS passwd",
+ pwd->pw_name);
+ else if (rc != 0)
+ warn("NIS passwd update");
+ /* NOTE: we treat NIS-only update errors as non-fatal */
+ }
+
+ if (cmdcnf->groups != NULL) {
+ for (i = 0; i < cmdcnf->groups->sl_cur; i++) {
+ grp = GETGRNAM(cmdcnf->groups->sl_str[i]);
+ grp = gr_add(grp, pwd->pw_name);
+ /*
+ * grp can only be NULL in 2 cases:
+ * - the new member is already a member
+ * - a problem with memory occurs
+ * in both cases we want to skip now.
+ */
+ if (grp == NULL)
+ continue;
+ chggrent(grp->gr_name, grp);
+ free(grp);
+ }
+ }
+
+ pwd = GETPWNAM(name);
+ if (pwd == NULL)
+ errx(EX_NOUSER, "user '%s' disappeared during update", name);
+
+ grp = GETGRGID(pwd->pw_gid);
+ pw_log(cnf, M_ADD, W_USER, "%s(%ju):%s(%ju):%s:%s:%s",
+ pwd->pw_name, (uintmax_t)pwd->pw_uid,
+ grp ? grp->gr_name : "unknown",
+ (uintmax_t)(grp ? grp->gr_gid : (uid_t)-1),
+ pwd->pw_gecos, pwd->pw_dir, pwd->pw_shell);
+
+ /*
+ * let's touch and chown the user's mail file. This is not
+ * strictly necessary under BSD with a 0755 maildir but it also
+ * doesn't hurt anything to create the empty mailfile
+ */
+ if (PWALTDIR() != PWF_ALT) {
+ snprintf(path, sizeof(path), "%s/%s", _PATH_MAILDIR,
+ pwd->pw_name);
+ /* Preserve contents & mtime */
+ close(openat(conf.rootfd, path +1, O_RDWR | O_CREAT, 0600));
+ fchownat(conf.rootfd, path + 1, pwd->pw_uid, pwd->pw_gid,
+ AT_SYMLINK_NOFOLLOW);
+ }
+
+ /*
+ * Let's create and populate the user's home directory. Note
+ * that this also `works' for editing users if -m is used, but
+ * existing files will *not* be overwritten.
+ */
+ if (PWALTDIR() != PWF_ALT && createhome && pwd->pw_dir &&
+ *pwd->pw_dir == '/' && pwd->pw_dir[1])
+ create_and_populate_homedir(cmdcnf, pwd, cmdcnf->dotdir,
+ cmdcnf->homemode, false);
+
+ if (!PWALTDIR() && cmdcnf->newmail && *cmdcnf->newmail &&
+ (fp = fopen(cnf->newmail, "r")) != NULL) {
+ if ((pfp = popen(_PATH_SENDMAIL " -t", "w")) == NULL)
+ warn("sendmail");
+ else {
+ fprintf(pfp, "From: root\n" "To: %s\n"
+ "Subject: Welcome!\n\n", pwd->pw_name);
+ while (fgets(line, sizeof(line), fp) != NULL) {
+ /* Do substitutions? */
+ fputs(line, pfp);
+ }
+ pclose(pfp);
+ pw_log(cnf, M_ADD, W_USER, "%s(%ju) new user mail sent",
+ pwd->pw_name, (uintmax_t)pwd->pw_uid);
+ }
+ fclose(fp);
+ }
+
+ if (nis && nis_update() == 0)
+ pw_log(cnf, M_ADD, W_USER, "NIS maps updated");
+
+ return (EXIT_SUCCESS);
+}
+
+int
+pw_user_mod(int argc, char **argv, char *arg1)
+{
+ struct userconf *cnf;
+ struct passwd *pwd;
+ struct group *grp;
+ StringList *groups = NULL;
+ char args[] = "C:qn:u:c:d:e:p:g:G:mM:l:k:s:w:L:h:H:NPYy:";
+ const char *cfg;
+ char *gecos, *homedir, *grname, *name, *newname, *walk, *skel, *shell;
+ char *passwd, *class, *nispasswd;
+ login_cap_t *lc;
+ struct stat st;
+ intmax_t id = -1;
+ int ch, fd = -1;
+ size_t i, j;
+ bool quiet, createhome, pretty, dryrun, nis, edited, docreatehome;
+ bool precrypted;
+ mode_t homemode = 0;
+ time_t expire_days, password_days, now;
+
+ expire_days = password_days = -1;
+ gecos = homedir = grname = name = newname = skel = shell =NULL;
+ passwd = NULL;
+ class = nispasswd = NULL;
+ quiet = createhome = pretty = dryrun = nis = precrypted = false;
+ edited = docreatehome = false;
+
+ if (arg1 != NULL) {
+ if (arg1[strspn(arg1, "0123456789")] == '\0')
+ id = pw_checkid(arg1, UID_MAX);
+ else
+ name = arg1;
+ }
+
+ while ((ch = getopt(argc, argv, args)) != -1) {
+ switch (ch) {
+ case 'C':
+ cfg = optarg;
+ break;
+ case 'q':
+ quiet = true;
+ break;
+ case 'n':
+ name = optarg;
+ break;
+ case 'u':
+ id = pw_checkid(optarg, UID_MAX);
+ break;
+ case 'c':
+ gecos = pw_checkname(optarg, 1);
+ break;
+ case 'd':
+ homedir = optarg;
+ break;
+ case 'e':
+ now = time(NULL);
+ expire_days = parse_date(now, optarg);
+ break;
+ case 'p':
+ now = time(NULL);
+ password_days = parse_date(now, optarg);
+ break;
+ case 'g':
+ group_from_name_or_id(optarg);
+ grname = optarg;
+ break;
+ case 'G':
+ split_groups(&groups, optarg);
+ break;
+ case 'm':
+ createhome = true;
+ break;
+ case 'M':
+ homemode = validate_mode(optarg);
+ break;
+ case 'l':
+ newname = optarg;
+ break;
+ case 'k':
+ walk = skel = optarg;
+ if (*walk == '/')
+ walk++;
+ if (fstatat(conf.rootfd, walk, &st, 0) == -1)
+ errx(EX_OSFILE, "skeleton `%s' does not "
+ "exists", skel);
+ if (!S_ISDIR(st.st_mode))
+ errx(EX_OSFILE, "skeleton `%s' is not a "
+ "directory", skel);
+ break;
+ case 's':
+ shell = optarg;
+ break;
+ case 'w':
+ passwd = optarg;
+ break;
+ case 'L':
+ class = pw_checkname(optarg, 0);
+ break;
+ case 'H':
+ if (fd != -1)
+ errx(EX_USAGE, "'-h' and '-H' are mutually "
+ "exclusive options");
+ fd = pw_checkfd(optarg);
+ precrypted = true;
+ if (fd == '-')
+ errx(EX_USAGE, "-H expects a file descriptor");
+ break;
+ case 'h':
+ if (fd != -1)
+ errx(EX_USAGE, "'-h' and '-H' are mutually "
+ "exclusive options");
+ fd = pw_checkfd(optarg);
+ break;
+ case 'N':
+ dryrun = true;
+ break;
+ case 'P':
+ pretty = true;
+ break;
+ case 'y':
+ nispasswd = optarg;
+ break;
+ case 'Y':
+ nis = true;
+ break;
+ }
+ }
+
+ if (geteuid() != 0 && ! dryrun)
+ errx(EX_NOPERM, "you must be root");
+
+ if (quiet)
+ freopen(_PATH_DEVNULL, "w", stderr);
+
+ cnf = get_userconfig(cfg);
+
+ if (id < 0 && name == NULL)
+ errx(EX_DATAERR, "username or id required");
+
+ pwd = (name != NULL) ? GETPWNAM(pw_checkname(name, 0)) : GETPWUID(id);
+ if (pwd == NULL) {
+ if (name == NULL)
+ errx(EX_NOUSER, "no such uid `%ju'",
+ (uintmax_t) id);
+ errx(EX_NOUSER, "no such user `%s'", name);
+ }
+
+ if (name == NULL)
+ name = pwd->pw_name;
+
+ if (nis && nispasswd == NULL)
+ nispasswd = cnf->nispasswd;
+
+ if (PWF._altdir == PWF_REGULAR &&
+ ((pwd->pw_fields & _PWF_SOURCE) != _PWF_FILES)) {
+ if ((pwd->pw_fields & _PWF_SOURCE) == _PWF_NIS) {
+ if (!nis && nispasswd && *nispasswd != '/')
+ errx(EX_NOUSER, "Cannot modify NIS user `%s'",
+ name);
+ } else {
+ errx(EX_NOUSER, "Cannot modify non local user `%s'",
+ name);
+ }
+ }
+
+ if (newname) {
+ if (strcmp(pwd->pw_name, "root") == 0)
+ errx(EX_DATAERR, "can't rename `root' account");
+ if (strcmp(pwd->pw_name, newname) != 0) {
+ pwd->pw_name = pw_checkname(newname, 0);
+ edited = true;
+ }
+ }
+
+ if (id > 0 && pwd->pw_uid != id) {
+ pwd->pw_uid = id;
+ edited = true;
+ if (pwd->pw_uid != 0 && strcmp(pwd->pw_name, "root") == 0)
+ errx(EX_DATAERR, "can't change uid of `root' account");
+ if (pwd->pw_uid == 0 && strcmp(pwd->pw_name, "root") != 0)
+ warnx("WARNING: account `%s' will have a uid of 0 "
+ "(superuser access!)", pwd->pw_name);
+ }
+
+ if (grname && pwd->pw_uid != 0) {
+ grp = GETGRNAM(grname);
+ if (grp == NULL)
+ grp = GETGRGID(pw_checkid(grname, GID_MAX));
+ if (grp->gr_gid != pwd->pw_gid) {
+ pwd->pw_gid = grp->gr_gid;
+ edited = true;
+ }
+ }
+
+ if (password_days >= 0 && pwd->pw_change != password_days) {
+ pwd->pw_change = password_days;
+ edited = true;
+ }
+
+ if (expire_days >= 0 && pwd->pw_expire != expire_days) {
+ pwd->pw_expire = expire_days;
+ edited = true;
+ }
+
+ if (shell) {
+ shell = shell_path(cnf->shelldir, cnf->shells, shell);
+ if (shell == NULL)
+ shell = "";
+ if (strcmp(shell, pwd->pw_shell) != 0) {
+ pwd->pw_shell = shell;
+ edited = true;
+ }
+ }
+
+ if (class && strcmp(pwd->pw_class, class) != 0) {
+ pwd->pw_class = class;
+ edited = true;
+ }
+
+ if (homedir && strcmp(pwd->pw_dir, homedir) != 0) {
+ pwd->pw_dir = homedir;
+ edited = true;
+ if (fstatat(conf.rootfd, pwd->pw_dir, &st, 0) == -1) {
+ if (!createhome)
+ warnx("WARNING: home `%s' does not exist",
+ pwd->pw_dir);
+ else
+ docreatehome = true;
+ } else if (!S_ISDIR(st.st_mode)) {
+ warnx("WARNING: home `%s' is not a directory",
+ pwd->pw_dir);
+ }
+ }
+
+ if (passwd && conf.fd == -1) {
+ lc = login_getpwclass(pwd);
+ if (lc == NULL || login_setcryptfmt(lc, "sha512", NULL) == NULL)
+ warn("setting crypt(3) format");
+ login_close(lc);
+ cnf->default_password = boolean_val(passwd,
+ cnf->default_password);
+ pwd->pw_passwd = pw_password(cnf, pwd->pw_name, dryrun);
+ edited = true;
+ }
+
+ if (gecos && strcmp(pwd->pw_gecos, gecos) != 0) {
+ pwd->pw_gecos = gecos;
+ edited = true;
+ }
+
+ if (fd != -1)
+ edited = pw_set_passwd(pwd, fd, precrypted, true);
+
+ if (dryrun)
+ return (print_user(pwd, pretty, false));
+
+ if (edited) /* Only updated this if required */
+ perform_chgpwent(name, pwd, nis ? nispasswd : NULL);
+ /* Now perform the needed changes concern groups */
+ if (groups != NULL) {
+ /* Delete User from groups using old name */
+ SETGRENT();
+ while ((grp = GETGRENT()) != NULL) {
+ if (grp->gr_mem == NULL)
+ continue;
+ for (i = 0; grp->gr_mem[i] != NULL; i++) {
+ if (strcmp(grp->gr_mem[i] , name) != 0)
+ continue;
+ for (j = i; grp->gr_mem[j] != NULL ; j++)
+ grp->gr_mem[j] = grp->gr_mem[j+1];
+ chggrent(grp->gr_name, grp);
+ break;
+ }
+ }
+ ENDGRENT();
+ /* Add the user to the needed groups */
+ for (i = 0; i < groups->sl_cur; i++) {
+ grp = GETGRNAM(groups->sl_str[i]);
+ grp = gr_add(grp, pwd->pw_name);
+ if (grp == NULL)
+ continue;
+ chggrent(grp->gr_name, grp);
+ free(grp);
+ }
+ }
+ /* In case of rename we need to walk over the different groups */
+ if (newname) {
+ SETGRENT();
+ while ((grp = GETGRENT()) != NULL) {
+ if (grp->gr_mem == NULL)
+ continue;
+ for (i = 0; grp->gr_mem[i] != NULL; i++) {
+ if (strcmp(grp->gr_mem[i], name) != 0)
+ continue;
+ grp->gr_mem[i] = newname;
+ chggrent(grp->gr_name, grp);
+ break;
+ }
+ }
+ }
+
+ /* go get a current version of pwd */
+ if (newname)
+ name = newname;
+ pwd = GETPWNAM(name);
+ if (pwd == NULL)
+ errx(EX_NOUSER, "user '%s' disappeared during update", name);
+ grp = GETGRGID(pwd->pw_gid);
+ pw_log(cnf, M_UPDATE, W_USER, "%s(%ju):%s(%ju):%s:%s:%s",
+ pwd->pw_name, (uintmax_t)pwd->pw_uid,
+ grp ? grp->gr_name : "unknown",
+ (uintmax_t)(grp ? grp->gr_gid : (uid_t)-1),
+ pwd->pw_gecos, pwd->pw_dir, pwd->pw_shell);
+
+ /*
+ * Let's create and populate the user's home directory. Note
+ * that this also `works' for editing users if -m is used, but
+ * existing files will *not* be overwritten.
+ */
+ if (PWALTDIR() != PWF_ALT && docreatehome && pwd->pw_dir &&
+ *pwd->pw_dir == '/' && pwd->pw_dir[1]) {
+ if (!skel)
+ skel = cnf->dotdir;
+ if (homemode == 0)
+ homemode = cnf->homemode;
+ create_and_populate_homedir(cnf, pwd, skel, homemode, true);
+ }
+
+ if (nis && nis_update() == 0)
+ pw_log(cnf, M_UPDATE, W_USER, "NIS maps updated");
+
+ return (EXIT_SUCCESS);
+}
diff --git a/usr.sbin/pw/pw_utils.c b/usr.sbin/pw/pw_utils.c
new file mode 100644
index 0000000..1a4f812
--- /dev/null
+++ b/usr.sbin/pw/pw_utils.c
@@ -0,0 +1,99 @@
+/*-
+ * Copyright (C) 2015 Baptiste Daroussin <bapt@FreeBSD.org>
+ * 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
+ * in this position and unchanged.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR(S) 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 <sys/cdefs.h>
+__FBSDID("$FreeBSD$");
+
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <err.h>
+#include <inttypes.h>
+#include <sysexits.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "pw.h"
+
+int
+pw_checkfd(char *nptr)
+{
+ const char *errstr;
+ int fd = -1;
+
+ if (strcmp(nptr, "-") == 0)
+ return '-';
+ fd = strtonum(nptr, 0, INT_MAX, &errstr);
+ if (errstr != NULL)
+ errx(EX_USAGE, "Bad file descriptor '%s': %s",
+ nptr, errstr);
+ return (fd);
+}
+
+uintmax_t
+pw_checkid(char *nptr, uintmax_t maxval)
+{
+ const char *errstr = NULL;
+ uintmax_t id;
+
+ id = strtounum(nptr, 0, maxval, &errstr);
+ if (errstr)
+ errx(EX_USAGE, "Bad id '%s': %s", nptr, errstr);
+ return (id);
+}
+
+struct userconf *
+get_userconfig(const char *config)
+{
+ char defaultcfg[MAXPATHLEN];
+
+ if (config != NULL)
+ return (read_userconfig(config));
+ snprintf(defaultcfg, sizeof(defaultcfg), "%s/pw.conf", conf.etcpath);
+ return (read_userconfig(defaultcfg));
+}
+
+int
+nis_update(void) {
+ pid_t pid;
+ int i;
+
+ fflush(NULL);
+ if ((pid = fork()) == -1) {
+ warn("fork()");
+ return (1);
+ }
+ if (pid == 0) {
+ execlp("/usr/bin/make", "make", "-C", "/var/yp/", (char*) NULL);
+ _exit(1);
+ }
+ waitpid(pid, &i, 0);
+ if ((i = WEXITSTATUS(i)) != 0)
+ errx(i, "make exited with status %d", i);
+ return (i);
+}
diff --git a/usr.sbin/pw/pw_vpw.c b/usr.sbin/pw/pw_vpw.c
new file mode 100644
index 0000000..2d1c75b
--- /dev/null
+++ b/usr.sbin/pw/pw_vpw.c
@@ -0,0 +1,205 @@
+/*-
+ * Copyright (C) 1996
+ * David L. Nugent. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT 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 DAVID L. NUGENT 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.
+ *
+ */
+
+#ifndef lint
+static const char rcsid[] =
+ "$FreeBSD$";
+#endif /* not lint */
+
+#include <pwd.h>
+#include <grp.h>
+#include <libutil.h>
+#define _WITH_GETLINE
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <sys/param.h>
+#include <err.h>
+
+#include "pwupd.h"
+
+static FILE * pwd_fp = NULL;
+
+void
+vendpwent(void)
+{
+ if (pwd_fp != NULL) {
+ fclose(pwd_fp);
+ pwd_fp = NULL;
+ }
+}
+
+void
+vsetpwent(void)
+{
+ vendpwent();
+}
+
+static struct passwd *
+vnextpwent(char const *nam, uid_t uid, int doclose)
+{
+ struct passwd *pw;
+ char *line;
+ size_t linecap;
+ ssize_t linelen;
+
+ pw = NULL;
+ line = NULL;
+ linecap = 0;
+
+ if (pwd_fp != NULL || (pwd_fp = fopen(getpwpath(_MASTERPASSWD), "r")) != NULL) {
+ while ((linelen = getline(&line, &linecap, pwd_fp)) > 0) {
+ /* Skip comments and empty lines */
+ if (*line == '\n' || *line == '#')
+ continue;
+ /* trim latest \n */
+ if (line[linelen - 1 ] == '\n')
+ line[linelen - 1] = '\0';
+ pw = pw_scan(line, PWSCAN_MASTER);
+ if (pw == NULL)
+ errx(EXIT_FAILURE, "Invalid user entry in '%s':"
+ " '%s'", getpwpath(_MASTERPASSWD), line);
+ if (uid != (uid_t)-1) {
+ if (uid == pw->pw_uid)
+ break;
+ } else if (nam != NULL) {
+ if (strcmp(nam, pw->pw_name) == 0)
+ break;
+ } else
+ break;
+ free(pw);
+ pw = NULL;
+ }
+ if (doclose)
+ vendpwent();
+ }
+ free(line);
+
+ return (pw);
+}
+
+struct passwd *
+vgetpwent(void)
+{
+ return vnextpwent(NULL, -1, 0);
+}
+
+struct passwd *
+vgetpwuid(uid_t uid)
+{
+ return vnextpwent(NULL, uid, 1);
+}
+
+struct passwd *
+vgetpwnam(const char * nam)
+{
+ return vnextpwent(nam, -1, 1);
+}
+
+
+static FILE * grp_fp = NULL;
+
+void
+vendgrent(void)
+{
+ if (grp_fp != NULL) {
+ fclose(grp_fp);
+ grp_fp = NULL;
+ }
+}
+
+RET_SETGRENT
+vsetgrent(void)
+{
+ vendgrent();
+#if defined(__FreeBSD__)
+ return 0;
+#endif
+}
+
+static struct group *
+vnextgrent(char const *nam, gid_t gid, int doclose)
+{
+ struct group *gr;
+ char *line;
+ size_t linecap;
+ ssize_t linelen;
+
+ gr = NULL;
+ line = NULL;
+ linecap = 0;
+
+ if (grp_fp != NULL || (grp_fp = fopen(getgrpath(_GROUP), "r")) != NULL) {
+ while ((linelen = getline(&line, &linecap, grp_fp)) > 0) {
+ /* Skip comments and empty lines */
+ if (*line == '\n' || *line == '#')
+ continue;
+ /* trim latest \n */
+ if (line[linelen - 1 ] == '\n')
+ line[linelen - 1] = '\0';
+ gr = gr_scan(line);
+ if (gr == NULL)
+ errx(EXIT_FAILURE, "Invalid group entry in '%s':"
+ " '%s'", getgrpath(_GROUP), line);
+ if (gid != (gid_t)-1) {
+ if (gid == gr->gr_gid)
+ break;
+ } else if (nam != NULL) {
+ if (strcmp(nam, gr->gr_name) == 0)
+ break;
+ } else
+ break;
+ free(gr);
+ gr = NULL;
+ }
+ if (doclose)
+ vendgrent();
+ }
+ free(line);
+
+ return (gr);
+}
+
+struct group *
+vgetgrent(void)
+{
+ return vnextgrent(NULL, -1, 0);
+}
+
+
+struct group *
+vgetgrgid(gid_t gid)
+{
+ return vnextgrent(NULL, gid, 1);
+}
+
+struct group *
+vgetgrnam(const char * nam)
+{
+ return vnextgrent(nam, -1, 1);
+}
+
diff --git a/usr.sbin/pw/pwupd.c b/usr.sbin/pw/pwupd.c
new file mode 100644
index 0000000..ee23952
--- /dev/null
+++ b/usr.sbin/pw/pwupd.c
@@ -0,0 +1,149 @@
+/*-
+ * Copyright (C) 1996
+ * David L. Nugent. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT 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 DAVID L. NUGENT 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.
+ */
+
+#ifndef lint
+static const char rcsid[] =
+ "$FreeBSD$";
+#endif /* not lint */
+
+#include <sys/wait.h>
+
+#include <err.h>
+#include <errno.h>
+#include <pwd.h>
+#include <libutil.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "pwupd.h"
+
+char *
+getpwpath(char const * file)
+{
+ static char pathbuf[MAXPATHLEN];
+
+ snprintf(pathbuf, sizeof pathbuf, "%s/%s", conf.etcpath, file);
+
+ return (pathbuf);
+}
+
+static int
+pwdb_check(void)
+{
+ int i = 0;
+ pid_t pid;
+ char *args[10];
+
+ args[i++] = _PATH_PWD_MKDB;
+ args[i++] = "-C";
+
+ if (strcmp(conf.etcpath, _PATH_PWD) != 0) {
+ args[i++] = "-d";
+ args[i++] = conf.etcpath;
+ }
+ args[i++] = getpwpath(_MASTERPASSWD);
+ args[i] = NULL;
+
+ if ((pid = fork()) == -1) /* Error (errno set) */
+ i = errno;
+ else if (pid == 0) { /* Child */
+ execv(args[0], args);
+ _exit(1);
+ } else { /* Parent */
+ waitpid(pid, &i, 0);
+ if (WEXITSTATUS(i))
+ i = EIO;
+ }
+
+ return (i);
+}
+
+static int
+pw_update(struct passwd * pwd, char const * user)
+{
+ struct passwd *pw = NULL;
+ struct passwd *old_pw = NULL;
+ int rc, pfd, tfd;
+
+ if ((rc = pwdb_check()) != 0)
+ return (rc);
+
+ if (pwd != NULL)
+ pw = pw_dup(pwd);
+
+ if (user != NULL)
+ old_pw = GETPWNAM(user);
+
+ if (pw_init(conf.etcpath, NULL))
+ err(1, "pw_init()");
+ if ((pfd = pw_lock()) == -1) {
+ pw_fini();
+ err(1, "pw_lock()");
+ }
+ if ((tfd = pw_tmp(-1)) == -1) {
+ pw_fini();
+ err(1, "pw_tmp()");
+ }
+ if (pw_copy(pfd, tfd, pw, old_pw) == -1) {
+ pw_fini();
+ err(1, "pw_copy()");
+ }
+ /*
+ * in case of deletion of a user, the whole database
+ * needs to be regenerated
+ */
+ if (pw_mkdb(pw != NULL ? pw->pw_name : NULL) == -1) {
+ pw_fini();
+ err(1, "pw_mkdb()");
+ }
+ free(pw);
+ pw_fini();
+
+ return (0);
+}
+
+int
+addpwent(struct passwd * pwd)
+{
+
+ return (pw_update(pwd, NULL));
+}
+
+int
+chgpwent(char const * login, struct passwd * pwd)
+{
+
+ return (pw_update(pwd, login));
+}
+
+int
+delpwent(struct passwd * pwd)
+{
+
+ return (pw_update(NULL, pwd->pw_name));
+}
diff --git a/usr.sbin/pw/pwupd.h b/usr.sbin/pw/pwupd.h
new file mode 100644
index 0000000..7fecffb
--- /dev/null
+++ b/usr.sbin/pw/pwupd.h
@@ -0,0 +1,152 @@
+/*-
+ * Copyright (C) 1996
+ * David L. Nugent. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT 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 DAVID L. NUGENT 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.
+ *
+ * $FreeBSD$
+ */
+
+#ifndef _PWUPD_H_
+#define _PWUPD_H_
+
+#include <sys/cdefs.h>
+#include <sys/param.h>
+#include <sys/types.h>
+
+#include <pwd.h>
+#include <grp.h>
+#include <stdbool.h>
+#include <stringlist.h>
+
+#if defined(__FreeBSD__)
+#define RET_SETGRENT int
+#else
+#define RET_SETGRENT void
+#endif
+
+struct pwf {
+ int _altdir;
+ void (*_setpwent)(void);
+ void (*_endpwent)(void);
+ struct passwd * (*_getpwent)(void);
+ struct passwd * (*_getpwuid)(uid_t uid);
+ struct passwd * (*_getpwnam)(const char * nam);
+ RET_SETGRENT (*_setgrent)(void);
+ void (*_endgrent)(void);
+ struct group * (*_getgrent)(void);
+ struct group * (*_getgrgid)(gid_t gid);
+ struct group * (*_getgrnam)(const char * nam);
+};
+
+struct userconf {
+ int default_password; /* Default password for new users? */
+ int reuse_uids; /* Reuse uids? */
+ int reuse_gids; /* Reuse gids? */
+ char *nispasswd; /* Path to NIS version of the passwd file */
+ char *dotdir; /* Where to obtain skeleton files */
+ char *newmail; /* Mail to send to new accounts */
+ char *logfile; /* Where to log changes */
+ char *home; /* Where to create home directory */
+ mode_t homemode; /* Home directory permissions */
+ char *shelldir; /* Where shells are located */
+ char **shells; /* List of shells */
+ char *shell_default; /* Default shell */
+ char *default_group; /* Default group number */
+ StringList *groups; /* Default (additional) groups */
+ char *default_class; /* Default user class */
+ uid_t min_uid, max_uid; /* Allowed range of uids */
+ gid_t min_gid, max_gid; /* Allowed range of gids */
+ time_t expire_days; /* Days to expiry */
+ time_t password_days; /* Days to password expiry */
+};
+
+struct pwconf {
+ char rootdir[MAXPATHLEN];
+ char etcpath[MAXPATHLEN];
+ int fd;
+ int rootfd;
+ bool checkduplicate;
+};
+
+extern struct pwf PWF;
+extern struct pwf VPWF;
+extern struct pwconf conf;
+
+#define SETPWENT() PWF._setpwent()
+#define ENDPWENT() PWF._endpwent()
+#define GETPWENT() PWF._getpwent()
+#define GETPWUID(uid) PWF._getpwuid(uid)
+#define GETPWNAM(nam) PWF._getpwnam(nam)
+
+#define SETGRENT() PWF._setgrent()
+#define ENDGRENT() PWF._endgrent()
+#define GETGRENT() PWF._getgrent()
+#define GETGRGID(gid) PWF._getgrgid(gid)
+#define GETGRNAM(nam) PWF._getgrnam(nam)
+
+#define PWF_REGULAR 0
+#define PWF_ALT 1
+#define PWF_ROOTDIR 2
+
+#define PWALTDIR() PWF._altdir
+#ifndef _PATH_PWD
+#define _PATH_PWD "/etc"
+#endif
+#ifndef _GROUP
+#define _GROUP "group"
+#endif
+#ifndef _MASTERPASSWD
+#define _MASTERPASSWD "master.passwd"
+#endif
+
+__BEGIN_DECLS
+int addpwent(struct passwd * pwd);
+int delpwent(struct passwd * pwd);
+int chgpwent(char const * login, struct passwd * pwd);
+
+char * getpwpath(char const * file);
+
+int addgrent(struct group * grp);
+int delgrent(struct group * grp);
+int chggrent(char const * name, struct group * grp);
+
+char * getgrpath(const char *file);
+
+void vsetpwent(void);
+void vendpwent(void);
+struct passwd * vgetpwent(void);
+struct passwd * vgetpwuid(uid_t uid);
+struct passwd * vgetpwnam(const char * nam);
+
+struct group * vgetgrent(void);
+struct group * vgetgrgid(gid_t gid);
+struct group * vgetgrnam(const char * nam);
+RET_SETGRENT vsetgrent(void);
+void vendgrent(void);
+
+void copymkdir(int rootfd, char const * dir, int skelfd, mode_t mode, uid_t uid,
+ gid_t gid, int flags);
+void rm_r(int rootfd, char const * dir, uid_t uid);
+__END_DECLS
+
+#endif /* !_PWUPD_H */
diff --git a/usr.sbin/pw/rm_r.c b/usr.sbin/pw/rm_r.c
new file mode 100644
index 0000000..172c7b0
--- /dev/null
+++ b/usr.sbin/pw/rm_r.c
@@ -0,0 +1,70 @@
+/*-
+ * Copyright (C) 1996
+ * David L. Nugent. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT 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 DAVID L. NUGENT 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.
+ */
+
+#ifndef lint
+static const char rcsid[] =
+ "$FreeBSD$";
+#endif /* not lint */
+
+#include <sys/stat.h>
+
+#include <dirent.h>
+#include <fcntl.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "pwupd.h"
+
+void
+rm_r(int rootfd, const char *path, uid_t uid)
+{
+ int dirfd;
+ DIR *d;
+ struct dirent *e;
+ struct stat st;
+
+ if (*path == '/')
+ path++;
+
+ dirfd = openat(rootfd, path, O_DIRECTORY);
+
+ d = fdopendir(dirfd);
+ while ((e = readdir(d)) != NULL) {
+ if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0)
+ continue;
+
+ if (fstatat(dirfd, e->d_name, &st, AT_SYMLINK_NOFOLLOW) != 0)
+ continue;
+ if (S_ISDIR(st.st_mode))
+ rm_r(dirfd, e->d_name, uid);
+ else if (S_ISLNK(st.st_mode) || st.st_uid == uid)
+ unlinkat(dirfd, e->d_name, 0);
+ }
+ closedir(d);
+ if (fstatat(rootfd, path, &st, AT_SYMLINK_NOFOLLOW) != 0)
+ return;
+ unlinkat(rootfd, path, S_ISDIR(st.st_mode) ? AT_REMOVEDIR : 0);
+}
diff --git a/usr.sbin/pw/strtounum.c b/usr.sbin/pw/strtounum.c
new file mode 100644
index 0000000..b2fefeb
--- /dev/null
+++ b/usr.sbin/pw/strtounum.c
@@ -0,0 +1,72 @@
+/*-
+ * Copyright (C) 2015 Baptiste Daroussin <bapt@FreeBSD.org>
+ * 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
+ * in this position and unchanged.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR(S) 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 <sys/cdefs.h>
+__FBSDID("$FreeBSD$");
+
+#include <errno.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdlib.h>
+
+#include "pw.h"
+
+uintmax_t
+strtounum(const char * __restrict np, uintmax_t minval, uintmax_t maxval,
+ const char ** __restrict errpp)
+{
+ char *endp;
+ uintmax_t ret;
+
+ *errpp = NULL;
+ if (minval > maxval) {
+ errno = EINVAL;
+ if (errpp != NULL)
+ *errpp = "invalid";
+ return (0);
+ }
+ errno = 0;
+ ret = strtoumax(np, &endp, 10);
+ if (endp == np || *endp != '\0') {
+ errno = EINVAL;
+ if (errpp != NULL)
+ *errpp = "invalid";
+ return (0);
+ }
+ if (ret < minval) {
+ errno = ERANGE;
+ if (errpp != NULL)
+ *errpp = "too small";
+ return (0);
+ }
+ if (errno == ERANGE || ret > maxval) {
+ errno = ERANGE;
+ if (errpp != NULL)
+ *errpp = "too large";
+ return (0);
+ }
+ return (ret);
+}
diff --git a/usr.sbin/pw/tests/Makefile b/usr.sbin/pw/tests/Makefile
new file mode 100644
index 0000000..27f0ac4
--- /dev/null
+++ b/usr.sbin/pw/tests/Makefile
@@ -0,0 +1,22 @@
+# $FreeBSD$
+
+ATF_TESTS_SH= pw_etcdir \
+ pw_lock \
+ pw_config \
+ pw_groupadd \
+ pw_groupdel \
+ pw_groupmod \
+ pw_useradd \
+ pw_userdel \
+ pw_usermod \
+ pw_usernext
+
+.for tp in ${ATF_TESTS_SH}
+TEST_METADATA.${tp}+= required_user="root"
+.endfor
+
+FILES= group helper_functions.shin master.passwd pw.conf \
+ pw-modified.conf
+FILESDIR= ${TESTSDIR}
+
+.include <bsd.test.mk>
diff --git a/usr.sbin/pw/tests/group b/usr.sbin/pw/tests/group
new file mode 100644
index 0000000..620c588
--- /dev/null
+++ b/usr.sbin/pw/tests/group
@@ -0,0 +1,3 @@
+# $FreeBSD$
+#
+wheel:*:0:root
diff --git a/usr.sbin/pw/tests/helper_functions.shin b/usr.sbin/pw/tests/helper_functions.shin
new file mode 100755
index 0000000..1ee731a
--- /dev/null
+++ b/usr.sbin/pw/tests/helper_functions.shin
@@ -0,0 +1,32 @@
+# $FreeBSD$
+
+# The pw command
+PW="pw -V ${HOME}"
+RPW="pw -R ${HOME}"
+
+# Workdir to run tests in
+TESTDIR=$(atf_get_srcdir)
+
+# Populate the files pw needs to use into $HOME
+populate_etc_skel() {
+ cp ${TESTDIR}/master.passwd ${HOME} || \
+ atf_fail "Populating master.passwd in ${HOME}"
+ cp ${TESTDIR}/group ${HOME} || atf_fail "Populating group in ${HOME}"
+
+ # Generate the passwd file
+ pwd_mkdb -p -d ${HOME} ${HOME}/master.passwd || \
+ atf_fail "generate passwd from master.passwd"
+}
+
+# Populate the files pw needs to use into $HOME/etc
+populate_root_etc_skel() {
+ mkdir ${HOME}/etc
+ cp ${TESTDIR}/master.passwd ${HOME}/etc || \
+ atf_fail "Populating master.passwd in ${HOME}/etc"
+ cp ${TESTDIR}/group ${HOME}/etc || \
+ atf_fail "Populating group in ${HOME}/etc"
+
+ # Generate the passwd file
+ pwd_mkdb -p -d ${HOME}/etc ${HOME}//etc/master.passwd || \
+ atf_fail "generate passwd from master.passwd"
+}
diff --git a/usr.sbin/pw/tests/master.passwd b/usr.sbin/pw/tests/master.passwd
new file mode 100644
index 0000000..f7dc837
--- /dev/null
+++ b/usr.sbin/pw/tests/master.passwd
@@ -0,0 +1,4 @@
+# $FreeBSD$
+#
+root:*:0:0::0:0:Charlie &:/root:/bin/csh
+toor:*:0:0::0:0:Bourne-again Superuser:/root:
diff --git a/usr.sbin/pw/tests/pw-modified.conf b/usr.sbin/pw/tests/pw-modified.conf
new file mode 100644
index 0000000..84f44e7
--- /dev/null
+++ b/usr.sbin/pw/tests/pw-modified.conf
@@ -0,0 +1,62 @@
+#
+# pw.conf - user/group configuration defaults
+#
+
+# Password for new users? no=nologin yes=loginid none=blank random=random
+defaultpasswd = "no"
+
+# Reuse gaps in uid sequence? (yes or no)
+reuseuids = "no"
+
+# Reuse gaps in gid sequence? (yes or no)
+reusegids = "no"
+
+# Path to the NIS passwd file (blank or 'no' for none)
+nispasswd =
+
+# Obtain default dotfiles from this directory
+skeleton = "/usr/share/skel"
+
+# Mail this file to new user (/etc/newuser.msg or no)
+newmail = "no"
+
+# Log add/change/remove information in this file
+logfile = "/var/log/userlog"
+
+# Root directory in which $HOME directory is created
+home = "/home"
+
+# Mode for the new $HOME directory, will be modified by umask
+homemode = 0777
+
+# Colon separated list of directories containing valid shells
+shellpath = "/bin"
+
+# Comma separated list of available shells (without paths)
+shells = "sh","csh","tcsh"
+
+# Default shell (without path)
+defaultshell = "sh"
+
+# Default group (leave blank for new group per user)
+defaultgroup = ""
+
+# Extra groups for new users
+extragroups =
+
+# Default login class for new users
+defaultclass = ""
+
+# Range of valid default user ids
+minuid = 2000
+maxuid = 5000
+
+# Range of valid default group ids
+mingid = 2100
+maxgid = 6000
+
+# Days after which account expires (0=disabled)
+expire_days = 0
+
+# Days after which password expires (0=disabled)
+password_days = 0
diff --git a/usr.sbin/pw/tests/pw.conf b/usr.sbin/pw/tests/pw.conf
new file mode 100644
index 0000000..4e493f6
--- /dev/null
+++ b/usr.sbin/pw/tests/pw.conf
@@ -0,0 +1,62 @@
+#
+# pw.conf - user/group configuration defaults
+#
+
+# Password for new users? no=nologin yes=loginid none=blank random=random
+defaultpasswd = "no"
+
+# Reuse gaps in uid sequence? (yes or no)
+reuseuids = "no"
+
+# Reuse gaps in gid sequence? (yes or no)
+reusegids = "no"
+
+# Path to the NIS passwd file (blank or 'no' for none)
+nispasswd =
+
+# Obtain default dotfiles from this directory
+skeleton = "/usr/share/skel"
+
+# Mail this file to new user (/etc/newuser.msg or no)
+newmail = "no"
+
+# Log add/change/remove information in this file
+logfile = "/var/log/userlog"
+
+# Root directory in which $HOME directory is created
+home = "/home"
+
+# Mode for the new $HOME directory, will be modified by umask
+homemode = 0777
+
+# Colon separated list of directories containing valid shells
+shellpath = "/bin"
+
+# Comma separated list of available shells (without paths)
+shells = "sh","csh","tcsh"
+
+# Default shell (without path)
+defaultshell = "sh"
+
+# Default group (leave blank for new group per user)
+defaultgroup = ""
+
+# Extra groups for new users
+extragroups =
+
+# Default login class for new users
+defaultclass = ""
+
+# Range of valid default user ids
+minuid = 1000
+maxuid = 32000
+
+# Range of valid default group ids
+mingid = 1000
+maxgid = 32000
+
+# Days after which account expires (0=disabled)
+expire_days = 0
+
+# Days after which password expires (0=disabled)
+password_days = 0
diff --git a/usr.sbin/pw/tests/pw_config.sh b/usr.sbin/pw/tests/pw_config.sh
new file mode 100755
index 0000000..fb6489a
--- /dev/null
+++ b/usr.sbin/pw/tests/pw_config.sh
@@ -0,0 +1,26 @@
+# $FreeBSD$
+
+# Import helper functions
+. $(atf_get_srcdir)/helper_functions.shin
+
+atf_test_case generate_config
+generate_config_body() {
+ atf_check -s exit:0 \
+ ${PW} useradd -D -C ${HOME}/foo.conf
+ atf_check -o file:$(atf_get_srcdir)/pw.conf \
+ cat ${HOME}/foo.conf
+}
+
+atf_test_case modify_config_uid_gid_boundaries
+modify_config_uid_gid_boundaries_body() {
+ atf_check -s exit:0 \
+ ${PW} useradd -D -C ${HOME}/foo.conf \
+ -u 2000,5000 -i 2100,6000
+ atf_check -o file:$(atf_get_srcdir)/pw-modified.conf \
+ cat ${HOME}/foo.conf
+}
+
+atf_init_test_cases() {
+ atf_add_test_case generate_config
+ atf_add_test_case modify_config_uid_gid_boundaries
+}
diff --git a/usr.sbin/pw/tests/pw_etcdir.sh b/usr.sbin/pw/tests/pw_etcdir.sh
new file mode 100755
index 0000000..b237789
--- /dev/null
+++ b/usr.sbin/pw/tests/pw_etcdir.sh
@@ -0,0 +1,18 @@
+# $FreeBSD$
+
+# When the '-V directory' option is provided, the directory must exist
+atf_test_case etcdir_must_exist
+etcdir_must_exist_head() {
+ atf_set "descr" "When the '-V directory' option is provided, the directory must exist"
+}
+
+etcdir_must_exist_body() {
+ local fakedir="/this_directory_does_not_exist"
+ atf_check -e inline:"pw: no such directory \`$fakedir'\n" \
+ -s exit:72 -x pw -V ${fakedir} usershow root
+}
+
+atf_init_test_cases() {
+ atf_add_test_case etcdir_must_exist
+}
+
diff --git a/usr.sbin/pw/tests/pw_groupadd.sh b/usr.sbin/pw/tests/pw_groupadd.sh
new file mode 100755
index 0000000..5fa7bef
--- /dev/null
+++ b/usr.sbin/pw/tests/pw_groupadd.sh
@@ -0,0 +1,26 @@
+# $FreeBSD$
+
+# Import helper functions
+. $(atf_get_srcdir)/helper_functions.shin
+
+atf_test_case group_add_gid_too_large
+group_add_gid_too_large_body() {
+ populate_etc_skel
+ atf_check -s exit:64 -e inline:"pw: Bad id '9999999999999': too large\n" \
+ ${PW} groupadd -n test1 -g 9999999999999
+}
+
+atf_test_case group_add_already_exists
+group_add_already_exists_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} groupadd foo
+ atf_check -s exit:65 \
+ -e inline:"pw: group name \`foo' already exists\n" \
+ ${PW} groupadd foo
+}
+
+atf_init_test_cases() {
+ atf_add_test_case group_add_gid_too_large
+ atf_add_test_case group_add_already_exists
+}
diff --git a/usr.sbin/pw/tests/pw_groupdel.sh b/usr.sbin/pw/tests/pw_groupdel.sh
new file mode 100755
index 0000000..88cc0e0
--- /dev/null
+++ b/usr.sbin/pw/tests/pw_groupdel.sh
@@ -0,0 +1,24 @@
+# $FreeBSD$
+
+# Import helper functions
+. $(atf_get_srcdir)/helper_functions.shin
+
+
+# Test to make sure we do not accidentially delete wheel when trying to delete
+# an unknown group
+atf_test_case group_do_not_delete_wheel_if_group_unknown
+group_do_not_delete_wheel_if_group_unknown_head() {
+ atf_set "descr" "Make sure we do not consider gid 0 an unknown group"
+}
+group_do_not_delete_wheel_if_group_unknown_body() {
+ populate_etc_skel
+ atf_check -s exit:0 -o inline:"wheel:*:0:root\n" -x ${PW} groupshow wheel
+ atf_check -e inline:"pw: Bad id 'I_do_not_exist': invalid\n" -s exit:64 -x \
+ ${PW} groupdel -g I_do_not_exist
+ atf_check -s exit:0 -o inline:"wheel:*:0:root\n" -x ${PW} groupshow wheel
+}
+
+
+atf_init_test_cases() {
+ atf_add_test_case group_do_not_delete_wheel_if_group_unknown
+}
diff --git a/usr.sbin/pw/tests/pw_groupmod.sh b/usr.sbin/pw/tests/pw_groupmod.sh
new file mode 100755
index 0000000..5806925
--- /dev/null
+++ b/usr.sbin/pw/tests/pw_groupmod.sh
@@ -0,0 +1,118 @@
+# $FreeBSD$
+
+# Import helper functions
+. $(atf_get_srcdir)/helper_functions.shin
+
+
+# Test adding & removing a user from a group
+atf_test_case groupmod_user
+groupmod_user_body() {
+ populate_etc_skel
+ atf_check -s exit:0 ${PW} addgroup test
+ atf_check -s exit:0 ${PW} groupmod test -m root
+ atf_check -s exit:0 -o match:"^test:\*:1001:root$" \
+ grep "^test:\*:.*:root$" $HOME/group
+ atf_check -s exit:0 ${PW} groupmod test -d root
+ atf_check -s exit:0 -o match:"^test:\*:1001:$" \
+ grep "^test:\*:.*:$" $HOME/group
+}
+
+
+# Test adding and removing a user that does not exist
+atf_test_case groupmod_invalid_user
+groupmod_invalid_user_body() {
+ populate_etc_skel
+ atf_check -s exit:0 ${PW} addgroup test
+ atf_check -s exit:67 -e match:"does not exist" ${PW} groupmod test -m foo
+ atf_check -s exit:0 ${PW} groupmod test -d foo
+}
+
+atf_test_case groupmod_bug_193704
+groupmod_bug_193704_head() {
+ atf_set "descr" "Regression test for the #193704 bug"
+}
+groupmod_bug_193704_body() {
+ populate_etc_skel
+ atf_check -s exit:0 -x ${PW} groupadd test
+ atf_check -s exit:0 -x ${PW} groupmod test -l newgroupname
+ atf_check -s exit:65 -e match:"^pw: unknown group" -x ${PW} groupshow test
+}
+
+atf_test_case usermod_bug_185666
+usermod_bug_185666_head() {
+ atf_set "descr" "Regression test for the #185666 bug"
+}
+
+usermod_bug_185666_body() {
+ populate_etc_skel
+ atf_check -s exit:0 -x ${PW} useradd testuser
+ atf_check -s exit:0 -x ${PW} groupadd testgroup
+ atf_check -s exit:0 -x ${PW} groupadd testgroup2
+ atf_check -s exit:0 -x ${PW} usermod testuser -G testgroup
+ atf_check -o inline:"testuser:*:1001:\n" -x ${PW} groupshow testuser
+ atf_check -o inline:"testgroup:*:1002:testuser\n" -x ${PW} groupshow testgroup
+ atf_check -o inline:"testgroup2:*:1003:\n" -x ${PW} groupshow testgroup2
+ atf_check -s exit:0 -x ${PW} usermod testuser -G testgroup2
+ atf_check -o inline:"testuser:*:1001:\n" -x ${PW} groupshow testuser
+ atf_check -o inline:"testgroup:*:1002:\n" -x ${PW} groupshow testgroup
+ atf_check -o inline:"testgroup2:*:1003:testuser\n" -x ${PW} groupshow testgroup2
+}
+
+atf_test_case do_not_duplicate_group_on_gid_change
+do_not_duplicate_group_on_gid_change_head() {
+ atf_set "descr" "Do not duplicate group on gid change"
+}
+
+do_not_duplicate_group_on_gid_change_body() {
+ populate_etc_skel
+ atf_check -s exit:0 -x ${PW} groupadd testgroup
+ atf_check -s exit:0 -x ${PW} groupmod testgroup -g 12345
+ # use grep to see if the entry has not be duplicated
+ atf_check -o inline:"testgroup:*:12345:\n" -s exit:0 -x grep "^testgroup" ${HOME}/group
+}
+
+atf_test_case groupmod_rename
+groupmod_rename_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} groupadd foo
+ atf_check -s exit:0 ${PW} groupmod foo -l bar
+ atf_check -s exit:0 -o match:"^bar:.*" \
+ grep "^bar:.*" ${HOME}/group
+}
+
+atf_test_case groupmod_members
+groupmod_members_body() {
+ populate_etc_skel
+
+ for i in user1 user2 user3 user4; do
+ atf_check -s exit:0 ${PW} useradd $i
+ done
+
+ atf_check -s exit:0 ${PW} groupadd foo -M "user1, user2"
+ atf_check -o inline:"foo:*:1005:user1,user2\n" -s exit:0 \
+ ${PW} groupshow foo
+ atf_check -s exit:0 ${PW} groupmod foo -m "user3, user4"
+ atf_check -o inline:"foo:*:1005:user1,user2,user3,user4\n" -s exit:0 \
+ ${PW} groupshow foo
+ atf_check -s exit:0 ${PW} groupmod foo -M "user1, user4"
+ atf_check -o inline:"foo:*:1005:user1,user4\n" -s exit:0 \
+ ${PW} groupshow foo
+ # what about duplicates
+ atf_check -s exit:0 ${PW} groupmod foo -m "user1, user2, user3, user4"
+ atf_check -o inline:"foo:*:1005:user1,user4,user2,user3\n" -s exit:0 \
+ ${PW} groupshow foo
+ atf_check -s exit:0 ${PW} groupmod foo -d "user1, user3"
+ atf_check -o inline:"foo:*:1005:user4,user2\n" -s exit:0 \
+ ${PW} groupshow foo
+}
+
+atf_init_test_cases() {
+ atf_add_test_case groupmod_user
+ atf_add_test_case groupmod_invalid_user
+ atf_add_test_case groupmod_bug_193704
+ atf_add_test_case usermod_bug_185666
+ atf_add_test_case do_not_duplicate_group_on_gid_change
+ atf_add_test_case groupmod_rename
+ atf_add_test_case groupmod_members
+}
diff --git a/usr.sbin/pw/tests/pw_lock.sh b/usr.sbin/pw/tests/pw_lock.sh
new file mode 100755
index 0000000..5ec1b09
--- /dev/null
+++ b/usr.sbin/pw/tests/pw_lock.sh
@@ -0,0 +1,42 @@
+# $FreeBSD$
+
+# Import helper functions
+. $(atf_get_srcdir)/helper_functions.shin
+
+# Test locking and unlocking a user account
+atf_test_case user_locking cleanup
+user_locking_body() {
+ populate_etc_skel
+ ${PW} useradd test || atf_fail "Creating test user"
+ ${PW} lock test || atf_fail "Locking the user"
+ atf_check -s exit:0 -o match:"^test:\*LOCKED\*\*:1001:" \
+ grep "^test:\*LOCKED\*\*:1001:" $HOME/master.passwd
+ ${PW} unlock test || atf_fail "Locking the user"
+ atf_check -s exit:0 -o match:"^test:\*:1001:" \
+ grep "^test:\*:1001:" $HOME/master.passwd
+}
+
+atf_test_case numeric_locking cleanup
+numeric_locking_body() {
+ populate_etc_skel
+ ${PW} useradd test || atf_fail "Creating test user"
+ ${PW} lock 1001 || atf_fail "Locking the user"
+ atf_check -s exit:0 -o match:"^test:\*LOCKED\*\*:1001:" \
+ grep "^test:\*LOCKED\*\*:1001:" $HOME/master.passwd
+ ${PW} unlock 1001 || atf_fail "Unlocking the user"
+ atf_check -s exit:0 -o match:"^test:\*:1001:" \
+ grep "^test:\*:1001:" $HOME/master.passwd
+ # Now numeric names
+ ${PW} useradd -n 1001 || atf_fail "Creating test user"
+ ${PW} lock 1001 || atf_fail "Locking the user"
+ atf_check -s exit:0 -o match:"^1001:\*LOCKED\*\*:1002:" \
+ grep "^1001:\*LOCKED\*\*:1002:" $HOME/master.passwd
+ ${PW} unlock 1001 || atf_fail "Unlocking the user"
+ atf_check -s exit:0 -o match:"^1001:\*:1002:" \
+ grep "^1001:\*:1002:" $HOME/master.passwd
+}
+
+atf_init_test_cases() {
+ atf_add_test_case user_locking
+ atf_add_test_case numeric_locking
+}
diff --git a/usr.sbin/pw/tests/pw_useradd.sh b/usr.sbin/pw/tests/pw_useradd.sh
new file mode 100755
index 0000000..cb62944
--- /dev/null
+++ b/usr.sbin/pw/tests/pw_useradd.sh
@@ -0,0 +1,385 @@
+# $FreeBSD$
+
+# Import helper functions
+. $(atf_get_srcdir)/helper_functions.shin
+
+# Test add user
+atf_test_case user_add
+user_add_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} useradd test
+ atf_check -s exit:0 -o match:"^test:.*" \
+ grep "^test:.*" $HOME/master.passwd
+}
+
+# Test add user with option -N
+atf_test_case user_add_noupdate
+user_add_noupdate_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 -o match:"^test:.*" ${PW} useradd test -N
+ atf_check -s exit:1 -o empty grep "^test:.*" $HOME/master.passwd
+}
+
+# Test add user with comments
+atf_test_case user_add_comments
+user_add_comments_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} useradd test -c "Test User,work,123,456"
+ atf_check -s exit:0 -o match:"^test:.*:Test User,work,123,456:" \
+ grep "^test:.*:Test User,work,123,456:" $HOME/master.passwd
+}
+
+# Test add user with comments and option -N
+atf_test_case user_add_comments_noupdate
+user_add_comments_noupdate_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 -o match:"^test:.*:Test User,work,123,456:" \
+ ${PW} useradd test -c "Test User,work,123,456" -N
+ atf_check -s exit:1 -o empty grep "^test:.*" $HOME/master.passwd
+}
+
+# Test add user with invalid comments
+atf_test_case user_add_comments_invalid
+user_add_comments_invalid_body() {
+ populate_etc_skel
+
+ atf_check -s exit:65 -e match:"invalid character" \
+ ${PW} useradd test -c "Test User,work,123:456,456"
+ atf_check -s exit:1 -o empty \
+ grep "^test:.*:Test User,work,123:456,456:" $HOME/master.passwd
+}
+
+# Test add user with invalid comments and option -N
+atf_test_case user_add_comments_invalid_noupdate
+user_add_comments_invalid_noupdate_body() {
+ populate_etc_skel
+
+ atf_check -s exit:65 -e match:"invalid character" \
+ ${PW} useradd test -c "Test User,work,123:456,456" -N
+ atf_check -s exit:1 -o empty grep "^test:.*" $HOME/master.passwd
+}
+
+# Test add user with alternate homedir
+atf_test_case user_add_homedir
+user_add_homedir_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} useradd test -d /foo/bar
+ atf_check -s exit:0 -o match:"^test:\*:.*::0:0:User &:/foo/bar:.*" \
+ ${PW} usershow test
+}
+
+# Test add user with account expiration as an epoch date
+atf_test_case user_add_account_expiration_epoch
+user_add_account_expiration_epoch_body() {
+ populate_etc_skel
+
+ DATE=`date -j -v+1d "+%s"`
+ atf_check -s exit:0 ${PW} useradd test -e ${DATE}
+ atf_check -s exit:0 -o match:"^test:\*:.*::0:${DATE}:.*" \
+ ${PW} usershow test
+}
+
+# Test add user with account expiration as a DD-MM-YYYY date
+atf_test_case user_add_account_expiration_date_numeric
+user_add_account_expiration_date_numeric_body() {
+ populate_etc_skel
+
+ DATE=`date -j -v+1d "+%d-%m-%Y"`
+ EPOCH=`date -j -f "%d-%m-%Y %H:%M:%S" "${DATE} 00:00:00" "+%s"`
+ atf_check -s exit:0 ${PW} useradd test -e ${DATE}
+ atf_check -s exit:0 -o match:"^test:\*:.*::0:${EPOCH}:User &:.*" \
+ ${PW} usershow test
+}
+
+# Test add user with account expiration as a DD-MM-YYYY date
+atf_test_case user_add_account_expiration_date_month
+user_add_account_expiration_date_month_body() {
+ populate_etc_skel
+
+ DATE=`date -j -v+1d "+%d-%b-%Y"`
+ EPOCH=`date -j -f "%d-%b-%Y %H:%M:%S" "${DATE} 00:00:00" "+%s"`
+ atf_check -s exit:0 ${PW} useradd test -e ${DATE}
+ atf_check -s exit:0 -o match:"^test:\*:.*::0:${EPOCH}:User &:.*" \
+ ${PW} usershow test
+}
+
+# Test add user with account expiration as a relative date
+atf_test_case user_add_account_expiration_date_relative
+user_add_account_expiration_date_relative_body() {
+ populate_etc_skel
+
+ EPOCH=`date -j -v+13m "+%s"`
+ BUF=`expr $EPOCH + 5`
+ atf_check -s exit:0 ${PW} useradd test -e +13o
+ TIME=`${PW} usershow test | awk -F ':' '{print $7}'`
+ [ ! -z $TIME -a $TIME -ge $EPOCH -a $TIME -lt $BUF ] || \
+ atf_fail "Expiration time($TIME) was not within $EPOCH - $BUF seconds."
+}
+
+# Test add user with password expiration as an epoch date
+atf_test_case user_add_password_expiration_epoch
+user_add_password_expiration_epoch_body() {
+ populate_etc_skel
+
+ DATE=`date -j -v+1d "+%s"`
+ atf_check -s exit:0 ${PW} useradd test -p ${DATE}
+ atf_check -s exit:0 -o match:"^test:\*:.*::${DATE}:0:.*" \
+ ${PW} usershow test
+}
+
+# Test add user with password expiration as a DD-MM-YYYY date
+atf_test_case user_add_password_expiration_date_numeric
+user_add_password_expiration_date_numeric_body() {
+ populate_etc_skel
+
+ DATE=`date -j -v+1d "+%d-%m-%Y"`
+ EPOCH=`date -j -f "%d-%m-%Y %H:%M:%S" "${DATE} 00:00:00" "+%s"`
+ atf_check -s exit:0 ${PW} useradd test -p ${DATE}
+ atf_check -s exit:0 -o match:"^test:\*:.*::${EPOCH}:0:User &:.*" \
+ ${PW} usershow test
+}
+
+# Test add user with password expiration as a DD-MMM-YYYY date
+atf_test_case user_add_password_expiration_date_month
+user_add_password_expiration_date_month_body() {
+ populate_etc_skel
+
+ DATE=`date -j -v+1d "+%d-%b-%Y"`
+ EPOCH=`date -j -f "%d-%b-%Y %H:%M:%S" "${DATE} 00:00:00" "+%s"`
+ atf_check -s exit:0 ${PW} useradd test -p ${DATE}
+ atf_check -s exit:0 -o match:"^test:\*:.*::${EPOCH}:0:User &:.*" \
+ ${PW} usershow test
+}
+
+# Test add user with password expiration as a relative date
+atf_test_case user_add_password_expiration_date_relative
+user_add_password_expiration_date_relative_body() {
+ populate_etc_skel
+
+ EPOCH=`date -j -v+13m "+%s"`
+ BUF=`expr $EPOCH + 5`
+ atf_check -s exit:0 ${PW} useradd test -p +13o
+ TIME=`${PW} usershow test | awk -F ':' '{print $6}'`
+ [ ! -z $TIME -a $TIME -ge $EPOCH -a $TIME -lt $BUF ] || \
+ atf_fail "Expiration time($TIME) was not within $EPOCH - $BUF seconds."
+}
+
+atf_test_case user_add_name_too_long
+user_add_name_too_long_body() {
+ populate_etc_skel
+ atf_check -e match:"too long" -s exit:64 \
+ ${PW} useradd name_very_vert_very_very_very_long
+}
+
+atf_test_case user_add_expiration
+user_add_expiration_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 \
+ ${PW} useradd foo -e 20-03-2037
+ atf_check -o inline:"foo:*:1001:1001::0:2121120000:User &:/home/foo:/bin/sh\n" \
+ -s exit:0 grep "^foo" ${HOME}/master.passwd
+ atf_check -s exit:0 ${PW} userdel foo
+ atf_check -s exit:0 \
+ ${PW} useradd foo -e 20-03-37
+ atf_check -o inline:"foo:*:1001:1001::0:2121120000:User &:/home/foo:/bin/sh\n" \
+ -s exit:0 grep "^foo" ${HOME}/master.passwd
+ atf_check -s exit:0 ${PW} userdel foo
+ atf_check -s exit:0 \
+ ${PW} useradd foo -e 20-Mar-2037
+ atf_check -o inline:"foo:*:1001:1001::0:2121120000:User &:/home/foo:/bin/sh\n" \
+ -s exit:0 grep "^foo" ${HOME}/master.passwd
+ atf_check -s exit:0 ${PW} userdel foo
+ atf_check -e inline:"pw: Invalid date\n" -s exit:1 \
+ ${PW} useradd foo -e 20-Foo-2037
+ atf_check -e inline:"pw: Invalid date\n" -s exit:1 \
+ ${PW} useradd foo -e 20-13-2037
+ atf_check -s exit:0 ${PW} useradd foo -e "12:00 20-03-2037"
+ atf_check -s exit:0 ${PW} userdel foo
+ atf_check -e inline:"pw: Invalid date\n" -s exit:1 \
+ ${PW} useradd foo -e "12 20-03-2037"
+ atf_check -s exit:0 ${PW} useradd foo -e "20-03-2037 12:00"
+ atf_check -s exit:0 ${PW} userdel foo
+}
+
+atf_test_case user_add_invalid_user_entry
+user_add_invalid_user_entry_body() {
+ touch ${HOME}/master.passwd
+ touch ${HOME}/group
+
+ pwd_mkdb -p -d ${HOME} ${HOME}/master.passwd || \
+ atf_fail "generate passwd from master.passwd"
+ atf_check -s exit:0 ${PW} useradd foo
+ echo "foo1:*:1002" >> ${HOME}/master.passwd
+ atf_check -s exit:1 -e match:"Invalid user entry" ${PW} useradd foo2
+}
+
+atf_test_case user_add_invalid_group_entry
+user_add_invalid_group_entry_body() {
+ touch ${HOME}/master.passwd
+ touch ${HOME}/group
+
+ pwd_mkdb -p -d ${HOME} ${HOME}/master.passwd || \
+ atf_fail "generate passwd from master.passwd"
+ atf_check -s exit:0 ${PW} useradd foo
+ echo 'foo1:*:1002' >> group
+ atf_check -s exit:1 -e match:"Invalid group entry" ${PW} useradd foo2
+}
+
+atf_test_case user_add_password_from_h
+user_add_password_from_h_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} useradd test -h 0 <<-EOF
+ $(echo test)
+ EOF
+}
+
+atf_test_case user_add_R
+user_add_R_body() {
+ populate_root_etc_skel
+
+ atf_check -s exit:0 ${RPW} useradd foo
+ atf_check -s exit:0 ${RPW} useradd bar -m
+ test -d ${HOME}/home || atf_fail "Home parent directory not created"
+ test -d ${HOME}/home/bar || atf_fail "Directory not created"
+ atf_check -s exit:0 ${RPW} userdel bar
+ test -d ${HOME}/home/bar || atf_fail "Directory removed"
+ atf_check -s exit:0 ${RPW} useradd bar
+ atf_check -s exit:0 ${RPW} userdel bar -r
+ [ ! -d ${HOME}/home/bar ] || atf_fail "Directory not removed"
+}
+
+atf_test_case user_add_R_symlink
+user_add_R_symlink_body() {
+ populate_root_etc_skel
+
+ mkdir ${HOME}/usr
+ atf_check -s exit:0 ${RPW} useradd foo -m
+ test -d ${HOME}/usr/home || atf_fail "Home parent directory not created"
+ test -h ${HOME}/home || atf_fail "/home directory is not a symlink"
+ atf_check -s exit:0 -o inline:"usr/home\n" readlink ${HOME}/home
+}
+
+atf_test_case user_add_skel
+user_add_skel_body() {
+ populate_root_etc_skel
+
+ mkdir ${HOME}/skel
+ echo "a" > ${HOME}/skel/.a
+ echo "b" > ${HOME}/skel/b
+ mkdir ${HOME}/skel/c
+ mkdir ${HOME}/skel/c/d
+ mkdir ${HOME}/skel/dot.plop
+ echo "c" > ${HOME}/skel/c/d/dot.c
+ mkdir ${HOME}/home
+ ln -sf /nonexistent ${HOME}/skel/c/foo
+ atf_check -s exit:0 ${RPW} useradd foo -k /skel -m
+ test -d ${HOME}/home/foo || atf_fail "Directory not created"
+ test -f ${HOME}/home/foo/.a || atf_fail "File not created"
+ atf_check -o file:${HOME}/skel/.a -s exit:0 cat ${HOME}/home/foo/.a
+ atf_check -o file:${HOME}/skel/b -s exit:0 cat ${HOME}/home/foo/b
+ test -d ${HOME}/home/foo/c || atf_fail "Dotted directory in skel not copied"
+ test -d ${HOME}/home/foo/.plop || atf_fail "Directory in skell not created"
+ atf_check -o inline:"/nonexistent\n" -s ignore readlink -f ${HOME}/home/foo/c/foo
+ atf_check -o file:${HOME}/skel/c/d/dot.c -s exit:0 cat ${HOME}/home/foo/c/d/.c
+}
+
+atf_test_case user_add_uid0
+user_add_uid0_body() {
+ populate_etc_skel
+ atf_check -e inline:"pw: WARNING: new account \`foo' has a uid of 0 (superuser access!)\n" \
+ -s exit:0 ${PW} useradd foo -u 0 -g 0 -d /root -s /bin/sh -c "Bourne-again Superuser" -o
+ atf_check \
+ -o inline:"foo:*:0:0::0:0:Bourne-again Superuser:/root:/bin/sh\n" \
+ -s exit:0 ${PW} usershow foo
+}
+
+atf_test_case user_add_uid_too_large
+user_add_uid_too_large_body() {
+ populate_etc_skel
+ atf_check -s exit:64 -e inline:"pw: Bad id '9999999999999': too large\n" \
+ ${PW} useradd -n test1 -u 9999999999999
+}
+
+atf_test_case user_add_bad_shell
+user_add_bad_shell_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} useradd foo -s sh
+ atf_check -s exit:78 -e ignore ${PW} useradd bar -s badshell
+}
+
+atf_test_case user_add_already_exists
+user_add_already_exists_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} useradd foo
+ atf_check -s exit:65 \
+ -e inline:"pw: login name \`foo' already exists\n" \
+ ${PW} useradd foo
+}
+
+atf_test_case user_add_w_yes
+user_add_w_yes_body() {
+ populate_etc_skel
+ atf_check -s exit:0 ${PW} useradd foo -w yes
+ atf_check -s exit:0 \
+ -o match:'^foo:\$.*' \
+ grep "^foo" ${HOME}/master.passwd
+ atf_check -s exit:0 ${PW} usermod foo -w yes
+ atf_check -s exit:0 \
+ -o match:'^foo:\$.*' \
+ grep "^foo" ${HOME}/master.passwd
+}
+
+atf_test_case user_add_with_pw_conf
+user_add_with_pw_conf_body()
+{
+ populate_etc_skel
+ atf_check -s exit:0 \
+ ${PW} useradd -D -C ${HOME}/pw.conf \
+ -u 2000,32767 -i 2000,32767
+ atf_check -s exit:0 \
+ -o inline:"minuid = 2000\nmaxuid = 32767\nmingid = 2000\nmaxgid = 32767\n" \
+ grep "^m.*id =" ${HOME}/pw.conf
+ atf_check -s exit:0 \
+ ${PW} useradd foo -C ${HOME}/pw.conf
+}
+
+atf_init_test_cases() {
+ atf_add_test_case user_add
+ atf_add_test_case user_add_noupdate
+ atf_add_test_case user_add_comments
+ atf_add_test_case user_add_comments_noupdate
+ atf_add_test_case user_add_comments_invalid
+ atf_add_test_case user_add_comments_invalid_noupdate
+ atf_add_test_case user_add_homedir
+ atf_add_test_case user_add_account_expiration_epoch
+ atf_add_test_case user_add_account_expiration_date_numeric
+ atf_add_test_case user_add_account_expiration_date_month
+ atf_add_test_case user_add_account_expiration_date_relative
+ atf_add_test_case user_add_password_expiration_epoch
+ atf_add_test_case user_add_password_expiration_date_numeric
+ atf_add_test_case user_add_password_expiration_date_month
+ atf_add_test_case user_add_password_expiration_date_relative
+ atf_add_test_case user_add_name_too_long
+ atf_add_test_case user_add_expiration
+ atf_add_test_case user_add_invalid_user_entry
+ atf_add_test_case user_add_invalid_group_entry
+ atf_add_test_case user_add_password_from_h
+ atf_add_test_case user_add_R
+ atf_add_test_case user_add_R_symlink
+ atf_add_test_case user_add_skel
+ atf_add_test_case user_add_uid0
+ atf_add_test_case user_add_uid_too_large
+ atf_add_test_case user_add_bad_shell
+ atf_add_test_case user_add_already_exists
+ atf_add_test_case user_add_w_yes
+ atf_add_test_case user_add_with_pw_conf
+}
diff --git a/usr.sbin/pw/tests/pw_userdel.sh b/usr.sbin/pw/tests/pw_userdel.sh
new file mode 100755
index 0000000..f608029
--- /dev/null
+++ b/usr.sbin/pw/tests/pw_userdel.sh
@@ -0,0 +1,67 @@
+# $FreeBSD$
+
+# Import helper functions
+. $(atf_get_srcdir)/helper_functions.shin
+
+
+# Test that a user can be deleted when another user is part of this
+# user's default group and does not go into an infinate loop.
+# PR: 191427
+atf_test_case rmuser_seperate_group cleanup
+rmuser_seperate_group_head() {
+ atf_set "timeout" "30"
+}
+rmuser_seperate_group_body() {
+ populate_etc_skel
+ ${PW} useradd test || atf_fail "Creating test user"
+ ${PW} groupmod test -M 'test,root' || \
+ atf_fail "Modifying the group"
+ ${PW} userdel test || atf_fail "Delete the test user"
+}
+
+
+atf_test_case user_do_not_try_to_delete_root_if_user_unknown
+user_do_not_try_to_delete_root_if_user_unknown_head() {
+ atf_set "descr" \
+ "Make sure not to try to remove root if deleting an unknown user"
+}
+user_do_not_try_to_delete_root_if_user_unknown_body() {
+ populate_etc_skel
+ atf_check -e inline:"pw: Bad id 'plop': invalid\n" -s exit:64 -x \
+ ${PW} userdel -u plop
+}
+
+atf_test_case delete_files
+delete_files_body() {
+ populate_root_etc_skel
+
+ mkdir -p ${HOME}/skel
+ touch ${HOME}/skel/a
+ mkdir -p ${HOME}/home
+ mkdir -p ${HOME}/var/mail
+ echo "foo wedontcare" > ${HOME}/etc/opiekeys
+ atf_check -s exit:0 ${RPW} useradd foo -k /skel -m
+ test -d ${HOME}/home || atf_fail "Fail to create home directory"
+ test -f ${HOME}/var/mail/foo || atf_fail "Mail file not created"
+ atf_check -s exit:0 ${RPW} userdel foo -r
+ atf_check -s exit:0 -o inline:"#oo wedontcare\n" cat ${HOME}/etc/opiekeys
+ if test -f ${HOME}/var/mail/foo; then
+ atf_fail "Mail file not removed"
+ fi
+}
+
+atf_test_case delete_numeric_name
+delete_numeric_name_body() {
+ populate_etc_skel
+
+ atf_check ${PW} useradd -n foo -u 4001
+ atf_check -e inline:"pw: no such user \`4001'\n" -s exit:67 \
+ ${PW} userdel -n 4001
+}
+
+atf_init_test_cases() {
+ atf_add_test_case rmuser_seperate_group
+ atf_add_test_case user_do_not_try_to_delete_root_if_user_unknown
+ atf_add_test_case delete_files
+ atf_add_test_case delete_numeric_name
+}
diff --git a/usr.sbin/pw/tests/pw_usermod.sh b/usr.sbin/pw/tests/pw_usermod.sh
new file mode 100755
index 0000000..236fd27
--- /dev/null
+++ b/usr.sbin/pw/tests/pw_usermod.sh
@@ -0,0 +1,222 @@
+# $FreeBSD$
+
+# Import helper functions
+. $(atf_get_srcdir)/helper_functions.shin
+
+# Test modifying a user
+atf_test_case user_mod
+user_mod_body() {
+ populate_etc_skel
+
+ atf_check -s exit:67 -e match:"no such user" ${PW} usermod test
+ atf_check -s exit:0 ${PW} useradd test
+ atf_check -s exit:0 ${PW} usermod test
+ atf_check -s exit:0 -o match:"^test:.*" \
+ grep "^test:.*" $HOME/master.passwd
+}
+
+# Test modifying a user with option -N
+atf_test_case user_mod_noupdate
+user_mod_noupdate_body() {
+ populate_etc_skel
+
+ atf_check -s exit:67 -e match:"no such user" ${PW} usermod test -N
+ atf_check -s exit:0 ${PW} useradd test
+ atf_check -s exit:0 -o match:"^test:.*" ${PW} usermod test -N
+ atf_check -s exit:0 -o match:"^test:.*" \
+ grep "^test:.*" $HOME/master.passwd
+}
+
+# Test modifying a user with comments
+atf_test_case user_mod_comments
+user_mod_comments_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} useradd test -c "Test User,home,123,456"
+ atf_check -s exit:0 ${PW} usermod test -c "Test User,work,123,456"
+ atf_check -s exit:0 -o match:"^test:.*:Test User,work,123,456:" \
+ grep "^test:.*:Test User,work,123,456:" $HOME/master.passwd
+}
+
+# Test modifying a user with comments with option -N
+atf_test_case user_mod_comments_noupdate
+user_mod_comments_noupdate_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} useradd test -c "Test User,home,123,456"
+ atf_check -s exit:0 -o match:"^test:.*:Test User,work,123,456:" \
+ ${PW} usermod test -c "Test User,work,123,456" -N
+ atf_check -s exit:0 -o match:"^test:.*:Test User,home,123,456:" \
+ grep "^test:.*:Test User,home,123,456:" $HOME/master.passwd
+}
+
+# Test modifying a user with invalid comments
+atf_test_case user_mod_comments_invalid
+user_mod_comments_invalid_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} useradd test
+ atf_check -s exit:65 -e match:"invalid character" \
+ ${PW} usermod test -c "Test User,work,123:456,456"
+ atf_check -s exit:1 -o empty \
+ grep "^test:.*:Test User,work,123:456,456:" $HOME/master.passwd
+ atf_check -s exit:0 -o match:"^test:\*" \
+ grep "^test:\*" $HOME/master.passwd
+}
+
+# Test modifying a user with invalid comments with option -N
+atf_test_case user_mod_comments_invalid_noupdate
+user_mod_comments_invalid_noupdate_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} useradd test
+ atf_check -s exit:65 -e match:"invalid character" \
+ ${PW} usermod test -c "Test User,work,123:456,456" -N
+ atf_check -s exit:1 -o empty \
+ grep "^test:.*:Test User,work,123:456,456:" $HOME/master.passwd
+ atf_check -s exit:0 -o match:"^test:\*" \
+ grep "^test:\*" $HOME/master.passwd
+}
+
+# Test modifying a user name with -l
+atf_test_case user_mod_name
+user_mod_name_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} useradd foo
+ atf_check -s exit:0 ${PW} usermod foo -l "bar"
+ atf_check -s exit:0 -o match:"^bar:.*" \
+ grep "^bar:.*" $HOME/master.passwd
+}
+
+# Test modifying a user name with -l with option -N
+atf_test_case user_mod_name_noupdate
+user_mod_name_noupdate_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} useradd foo
+ atf_check -s exit:0 -o match:"^bar:.*" ${PW} usermod foo -l "bar" -N
+ atf_check -s exit:0 -o match:"^foo:.*" \
+ grep "^foo:.*" $HOME/master.passwd
+}
+
+atf_test_case user_mod_rename_multigroups
+user_mod_rename_multigroups_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} groupadd test1
+ atf_check -s exit:0 ${PW} groupadd test2
+ atf_check -s exit:0 ${PW} useradd foo -G test1,test2
+ atf_check -o match:"foo" -s exit:0 ${PW} groupshow test1
+ atf_check -o match:"foo" -s exit:0 ${PW} groupshow test2
+ atf_check -s exit:0 ${PW} usermod foo -l bar
+ atf_check -o match:"bar" -s exit:0 ${PW} groupshow test1
+ atf_check -o match:"bar" -s exit:0 ${PW} groupshow test2
+}
+
+atf_test_case user_mod_nogroups
+user_mod_nogroups_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} groupadd test1
+ atf_check -s exit:0 ${PW} groupadd test2
+ atf_check -s exit:0 ${PW} groupadd test3
+ atf_check -s exit:0 ${PW} groupadd test4
+ atf_check -s exit:0 ${PW} useradd foo -G test1,test2
+ atf_check -o match:"foo" -s exit:0 ${PW} groupshow test1
+ atf_check -o match:"foo" -s exit:0 ${PW} groupshow test2
+ atf_check -s exit:0 ${PW} usermod foo -G test3,test4
+ atf_check -s exit:0 -o inline:"test3\ntest4\n" \
+ awk -F\: '$4 == "foo" { print $1 }' ${HOME}/group
+}
+
+atf_test_case user_mod_rename
+user_mod_rename_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} useradd foo
+ atf_check -s exit:0 ${PW} usermod foo -l bar
+ atf_check -s exit:0 -o match:"^bar:.*" \
+ grep "^bar:.*" ${HOME}/master.passwd
+}
+
+atf_test_case user_mod_rename_too_long
+user_mod_rename_too_long_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} useradd foo
+ atf_check -s exit:64 -e match:"too long" ${PW} usermod foo \
+ -l name_very_very_very_very_very_long
+}
+
+atf_test_case user_mod_h
+user_mod_h_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} useradd foo
+ atf_check -s exit:0 ${PW} usermod foo -h 0 <<- EOF
+ $(echo a)
+ EOF
+ atf_check -s exit:0 -o not-match:"^foo:\*:.*" \
+ grep "^foo" ${HOME}/master.passwd
+ atf_check -s exit:0 ${PW} usermod foo -h - <<- EOF
+ $(echo b)
+ EOF
+ atf_check -s exit:0 -o match:"^foo:\*:.*" \
+ grep "^foo" ${HOME}/master.passwd
+ atf_check -e inline:"pw: Bad file descriptor 'a': invalid\n" \
+ -s exit:64 ${PW} usermod foo -h a <<- EOF
+ $(echo a)
+ EOF
+}
+
+atf_test_case user_mod_H
+user_mod_H_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} useradd foo
+ atf_check -s exit:0 ${PW} usermod foo -H 0 <<- EOF
+ $(echo a)
+ EOF
+ atf_check -s exit:0 -o match:"^foo:a:.*" \
+ grep "^foo" ${HOME}/master.passwd
+ atf_check -s exit:64 -e inline:"pw: -H expects a file descriptor\n" \
+ ${PW} usermod foo -H -
+}
+
+atf_test_case user_mod_renamehome
+user_mod_renamehome_body() {
+ populate_root_etc_skel
+
+ mkdir -p ${HOME}/home
+ atf_check -s exit:0 ${RPW} useradd foo -m
+ test -d ${HOME}/home/foo || atf_fail "Directory not created"
+ atf_check -s exit:0 ${RPW} usermod foo -l bar -d /home/bar -m
+ test -d ${HOME}/home/bar || atf_fail "Directory not created"
+}
+
+atf_test_case user_mod_uid
+user_mod_uid_body() {
+ populate_etc_skel
+
+ atf_check -s exit:0 ${PW} useradd foo
+ atf_check -s exit:0 ${PW} usermod foo -u 5000
+}
+
+atf_init_test_cases() {
+ atf_add_test_case user_mod
+ atf_add_test_case user_mod_noupdate
+ atf_add_test_case user_mod_comments
+ atf_add_test_case user_mod_comments_noupdate
+ atf_add_test_case user_mod_comments_invalid
+ atf_add_test_case user_mod_comments_invalid_noupdate
+ atf_add_test_case user_mod_nogroups
+ atf_add_test_case user_mod_rename
+ atf_add_test_case user_mod_name_noupdate
+ atf_add_test_case user_mod_rename_too_long
+ atf_add_test_case user_mod_rename_multigroups
+ atf_add_test_case user_mod_h
+ atf_add_test_case user_mod_H
+ atf_add_test_case user_mod_renamehome
+ atf_add_test_case user_mod_uid
+}
diff --git a/usr.sbin/pw/tests/pw_usernext.sh b/usr.sbin/pw/tests/pw_usernext.sh
new file mode 100755
index 0000000..89f938e
--- /dev/null
+++ b/usr.sbin/pw/tests/pw_usernext.sh
@@ -0,0 +1,45 @@
+# $FreeBSD$
+
+# Import helper functions
+. $(atf_get_srcdir)/helper_functions.shin
+
+# Test usernext after adding a random number of new users.
+atf_test_case usernext
+usernext_body() {
+ populate_etc_skel
+
+ CURRENT=`${PW} usernext | sed -e 's/:.*//'`
+ RANDOM=`jot -r 1 1 150`
+ MAX=`expr ${CURRENT} + ${RANDOM}`
+ while [ "${CURRENT}" -lt "${MAX}" ]
+ do
+ atf_check -s exit:0 ${PW} useradd test${CURRENT}
+ CURRENT=`expr ${CURRENT} + 1`
+ done
+ atf_check -s exit:0 -o match:"${CURRENT}:${CURRENT}" \
+ ${PW} usernext
+}
+
+# Test usernext when multiple users are added to the same group so
+# that group id doesn't increment at the same pace as new users.
+atf_test_case usernext_assigned_group
+usernext_assigned_group_body() {
+ populate_etc_skel
+
+ CURRENT=`${PW} usernext | sed -e 's/:.*//'`
+ CURRENTGID=`${PW} groupnext`
+ RANDOM=`jot -r 1 1 150`
+ MAX=`expr ${CURRENT} + ${RANDOM}`
+ while [ "${CURRENT}" -lt "${MAX}" ]
+ do
+ atf_check -s exit:0 ${PW} useradd -n test${CURRENT} -g 0
+ CURRENT=`expr ${CURRENT} + 1`
+ done
+ atf_check -s exit:0 -o match:"${CURRENT}:${CURRENTGID}" \
+ ${PW} usernext
+}
+
+atf_init_test_cases() {
+ atf_add_test_case usernext
+ atf_add_test_case usernext_assigned_group
+}
OpenPOWER on IntegriCloud