diff options
Diffstat (limited to 'usr.sbin/pw')
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 +} |