summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSean Bruno <sbruno@FreeBSD.org>2012-01-04 02:01:27 +0000
committerSean Bruno <sbruno@FreeBSD.org>2012-01-04 02:01:27 +0000
commitde100da053802312c0bbcd80656737e7ad38e106 (patch)
tree388e2c45b6c6040ac8232006bb2e30552e9055f1
parentb38996d419508c7a361e6056304aed0894b793ca (diff)
parentcda8e1027a7b18b2894b7422ef611093e5e2a3da (diff)
downloadpw-darwin-de100da053802312c0bbcd80656737e7ad38e106.tar.gz
pw-darwin-de100da053802312c0bbcd80656737e7ad38e106.tar.zst
pw-darwin-de100da053802312c0bbcd80656737e7ad38e106.zip
IFC to head to catch up the bhyve branch
Approved by: grehan@
-rw-r--r--adduser/adduser.sh1050
-rw-r--r--libutil/gr_util.c546
-rw-r--r--libutil/libutil.h8
-rw-r--r--libutil/pw_util.c645
-rw-r--r--pw/cpdir.c131
-rw-r--r--pw/pw.81008
-rw-r--r--pw/pw_user.c2
7 files changed, 3388 insertions, 2 deletions
diff --git a/adduser/adduser.sh b/adduser/adduser.sh
new file mode 100644
index 0000000..f645f59
--- /dev/null
+++ b/adduser/adduser.sh
@@ -0,0 +1,1050 @@
+#!/bin/sh
+#
+# Copyright (c) 2002-2004 Michael Telahun Makonnen. 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 THE AUTHOR ``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 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.
+#
+# Email: Mike Makonnen <mtm@FreeBSD.Org>
+#
+# $FreeBSD$
+#
+
+# err msg
+# Display $msg on stderr, unless we're being quiet.
+#
+err() {
+ if [ -z "$quietflag" ]; then
+ echo 1>&2 ${THISCMD}: ERROR: $*
+ fi
+}
+
+# info msg
+# Display $msg on stdout, unless we're being quiet.
+#
+info() {
+ if [ -z "$quietflag" ]; then
+ echo ${THISCMD}: INFO: $*
+ fi
+}
+
+# get_nextuid
+# Output the value of $_uid if it is available for use. If it
+# is not, output the value of the next higher uid that is available.
+# If a uid is not specified, output the first available uid, as indicated
+# by pw(8).
+#
+get_nextuid () {
+ _uid=$1
+ _nextuid=
+
+ if [ -z "$_uid" ]; then
+ _nextuid="`${PWCMD} usernext | cut -f1 -d:`"
+ else
+ while : ; do
+ ${PWCMD} usershow $_uid > /dev/null 2>&1
+ if [ ! "$?" -eq 0 ]; then
+ _nextuid=$_uid
+ break
+ fi
+ _uid=$(($_uid + 1))
+ done
+ fi
+ echo $_nextuid
+}
+
+# show_usage
+# Display usage information for this utility.
+#
+show_usage() {
+ echo "usage: ${THISCMD} [options]"
+ echo " options may include:"
+ echo " -C save to the configuration file only"
+ echo " -D do not attempt to create the home directory"
+ echo " -E disable this account after creation"
+ echo " -G additional groups to add accounts to"
+ echo " -L login class of the user"
+ echo " -M file permission for home directory"
+ echo " -N do not read configuration file"
+ echo " -S a nonexistent shell is not an error"
+ echo " -d home directory"
+ echo " -f file from which input will be received"
+ echo " -g default login group"
+ echo " -h display this usage message"
+ echo " -k path to skeleton home directory"
+ echo " -m user welcome message file"
+ echo " -q absolute minimal user feedback"
+ echo " -s shell"
+ echo " -u uid to start at"
+ echo " -w password type: no, none, yes or random"
+}
+
+# valid_shells
+# Outputs a list of valid shells from /etc/shells. Only the
+# basename of the shell is output.
+#
+valid_shells() {
+ _prefix=
+ cat ${ETCSHELLS} |
+ while read _path _junk ; do
+ case $_path in
+ \#*|'')
+ ;;
+ *)
+ echo -n "${_prefix}`basename $_path`"
+ _prefix=' '
+ ;;
+ esac
+ done
+
+ # /usr/sbin/nologin is a special case
+ [ -x "${NOLOGIN_PATH}" ] && echo -n " ${NOLOGIN}"
+}
+
+# fullpath_from_shell shell
+# Given $shell, which is either the full path to a shell or
+# the basename component of a valid shell, get the
+# full path to the shell from the /etc/shells file.
+#
+fullpath_from_shell() {
+ _shell=$1
+ [ -z "$_shell" ] && return 1
+
+ # /usr/sbin/nologin is a special case; it needs to be handled
+ # before the cat | while loop, since a 'return' from within
+ # a subshell will not terminate the function's execution, and
+ # the path to the nologin shell might be printed out twice.
+ #
+ if [ "$_shell" = "${NOLOGIN}" -o \
+ "$_shell" = "${NOLOGIN_PATH}" ]; then
+ echo ${NOLOGIN_PATH}
+ return 0;
+ fi
+
+ cat ${ETCSHELLS} |
+ while read _path _junk ; do
+ case "$_path" in
+ \#*|'')
+ ;;
+ *)
+ if [ "$_path" = "$_shell" -o \
+ "`basename $_path`" = "$_shell" ]; then
+ echo $_path
+ return 0
+ fi
+ ;;
+ esac
+ done
+
+ return 1
+}
+
+# shell_exists shell
+# If the given shell is listed in ${ETCSHELLS} or it is
+# the nologin shell this function will return 0.
+# Otherwise, it will return 1. If shell is valid but
+# the path is invalid or it is not executable it
+# will emit an informational message saying so.
+#
+shell_exists() {
+ _sh="$1"
+ _shellchk="${GREPCMD} '^$_sh$' ${ETCSHELLS} > /dev/null 2>&1"
+
+ if ! eval $_shellchk; then
+ # The nologin shell is not listed in /etc/shells.
+ if [ "$_sh" != "${NOLOGIN_PATH}" ]; then
+ err "Invalid shell ($_sh) for user $username."
+ return 1
+ fi
+ fi
+ ! [ -x "$_sh" ] &&
+ info "The shell ($_sh) does not exist or is not executable."
+
+ return 0
+}
+
+# save_config
+# Save some variables to a configuration file.
+# Note: not all script variables are saved, only those that
+# it makes sense to save.
+#
+save_config() {
+ echo "# Configuration file for adduser(8)." > ${ADDUSERCONF}
+ echo "# NOTE: only *some* variables are saved." >> ${ADDUSERCONF}
+ echo "# Last Modified on `${DATECMD}`." >> ${ADDUSERCONF}
+ echo '' >> ${ADDUSERCONF}
+ echo "defaultHomePerm=$uhomeperm" >> ${ADDUSERCONF}
+ echo "defaultLgroup=$ulogingroup" >> ${ADDUSERCONF}
+ echo "defaultclass=$uclass" >> ${ADDUSERCONF}
+ echo "defaultgroups=$ugroups" >> ${ADDUSERCONF}
+ echo "passwdtype=$passwdtype" >> ${ADDUSERCONF}
+ echo "homeprefix=$homeprefix" >> ${ADDUSERCONF}
+ echo "defaultshell=$ushell" >> ${ADDUSERCONF}
+ echo "udotdir=$udotdir" >> ${ADDUSERCONF}
+ echo "msgfile=$msgfile" >> ${ADDUSERCONF}
+ echo "disableflag=$disableflag" >> ${ADDUSERCONF}
+ echo "uidstart=$uidstart" >> ${ADDUSERCONF}
+}
+
+# add_user
+# Add a user to the user database. If the user chose to send a welcome
+# message or lock the account, do so.
+#
+add_user() {
+
+ # Is this a configuration run? If so, don't modify user database.
+ #
+ if [ -n "$configflag" ]; then
+ save_config
+ return
+ fi
+
+ _uid=
+ _name=
+ _comment=
+ _gecos=
+ _home=
+ _group=
+ _grouplist=
+ _shell=
+ _class=
+ _dotdir=
+ _expire=
+ _pwexpire=
+ _passwd=
+ _upasswd=
+ _passwdmethod=
+
+ _name="-n '$username'"
+ [ -n "$uuid" ] && _uid='-u "$uuid"'
+ [ -n "$ulogingroup" ] && _group='-g "$ulogingroup"'
+ [ -n "$ugroups" ] && _grouplist='-G "$ugroups"'
+ [ -n "$ushell" ] && _shell='-s "$ushell"'
+ [ -n "$uclass" ] && _class='-L "$uclass"'
+ [ -n "$ugecos" ] && _comment='-c "$ugecos"'
+ [ -n "$udotdir" ] && _dotdir='-k "$udotdir"'
+ [ -n "$uexpire" ] && _expire='-e "$uexpire"'
+ [ -n "$upwexpire" ] && _pwexpire='-p "$upwexpire"'
+ if [ -z "$Dflag" -a -n "$uhome" ]; then
+ # The /nonexistent home directory is special. It
+ # means the user has no home directory.
+ if [ "$uhome" = "$NOHOME" ]; then
+ _home='-d "$uhome"'
+ else
+ # Use home directory permissions if specified
+ if [ -n "$uhomeperm" ]; then
+ _home='-m -d "$uhome" -M "$uhomeperm"'
+ else
+ _home='-m -d "$uhome"'
+ fi
+ fi
+ elif [ -n "$Dflag" -a -n "$uhome" ]; then
+ _home='-d "$uhome"'
+ fi
+ case $passwdtype in
+ no)
+ _passwdmethod="-w no"
+ _passwd="-h -"
+ ;;
+ yes)
+ # Note on processing the password: The outer double quotes
+ # make literal everything except ` and \ and $.
+ # The outer single quotes make literal ` and $.
+ # We can ensure the \ isn't treated specially by specifying
+ # the -r switch to the read command used to obtain the input.
+ #
+ _passwdmethod="-w yes"
+ _passwd="-h 0"
+ _upasswd='echo "$upass" |'
+ ;;
+ none)
+ _passwdmethod="-w none"
+ ;;
+ random)
+ _passwdmethod="-w random"
+ ;;
+ esac
+
+ _pwcmd="$_upasswd ${PWCMD} useradd $_uid $_name $_group $_grouplist $_comment"
+ _pwcmd="$_pwcmd $_shell $_class $_home $_dotdir $_passwdmethod $_passwd"
+ _pwcmd="$_pwcmd $_expire $_pwexpire"
+
+ if ! _output=`eval $_pwcmd` ; then
+ err "There was an error adding user ($username)."
+ return 1
+ else
+ info "Successfully added ($username) to the user database."
+ if [ "random" = "$passwdtype" ]; then
+ randompass="$_output"
+ info "Password for ($username) is: $randompass"
+ fi
+ fi
+
+ if [ -n "$disableflag" ]; then
+ if ${PWCMD} lock $username ; then
+ info "Account ($username) is locked."
+ else
+ info "Account ($username) could NOT be locked."
+ fi
+ fi
+
+ _line=
+ _owner=
+ _perms=
+ if [ -n "$msgflag" ]; then
+ [ -r "$msgfile" ] && {
+ # We're evaluating the contents of an external file.
+ # Let's not open ourselves up for attack. _perms will
+ # be empty if it's writeable only by the owner. _owner
+ # will *NOT* be empty if the file is owned by root.
+ #
+ _dir="`dirname $msgfile`"
+ _file="`basename $msgfile`"
+ _perms=`/usr/bin/find $_dir -name $_file -perm +07022 -prune`
+ _owner=`/usr/bin/find $_dir -name $_file -user 0 -prune`
+ if [ -z "$_owner" -o -n "$_perms" ]; then
+ err "The message file ($msgfile) may be writeable only by root."
+ return 1
+ fi
+ cat "$msgfile" |
+ while read _line ; do
+ eval echo "$_line"
+ done | ${MAILCMD} -s"Welcome" ${username}
+ info "Sent welcome message to ($username)."
+ }
+ fi
+}
+
+# get_user
+# Reads username of the account from standard input or from a global
+# variable containing an account line from a file. The username is
+# required. If this is an interactive session it will prompt in
+# a loop until a username is entered. If it is batch processing from
+# a file it will output an error message and return to the caller.
+#
+get_user() {
+ _input=
+
+ # No need to take down user names if this is a configuration saving run.
+ [ -n "$configflag" ] && return
+
+ while : ; do
+ if [ -z "$fflag" ]; then
+ echo -n "Username: "
+ read _input
+ else
+ _input="`echo "$fileline" | cut -f1 -d:`"
+ fi
+
+ # There *must* be a username, and it must not exist. If
+ # this is an interactive session give the user an
+ # opportunity to retry.
+ #
+ if [ -z "$_input" ]; then
+ err "You must enter a username!"
+ [ -z "$fflag" ] && continue
+ fi
+ ${PWCMD} usershow $_input > /dev/null 2>&1
+ if [ "$?" -eq 0 ]; then
+ err "User exists!"
+ [ -z "$fflag" ] && continue
+ fi
+ break
+ done
+ username="$_input"
+}
+
+# get_gecos
+# Reads extra information about the user. Can be used both in interactive
+# and batch (from file) mode.
+#
+get_gecos() {
+ _input=
+
+ # No need to take down additional user information for a configuration run.
+ [ -n "$configflag" ] && return
+
+ if [ -z "$fflag" ]; then
+ echo -n "Full name: "
+ read _input
+ else
+ _input="`echo "$fileline" | cut -f7 -d:`"
+ fi
+ ugecos="$_input"
+}
+
+# get_shell
+# Get the account's shell. Works in interactive and batch mode. It
+# accepts either the base name of the shell or the full path.
+# If an invalid shell is entered it will simply use the default shell.
+#
+get_shell() {
+ _input=
+ _fullpath=
+ ushell="$defaultshell"
+
+ # Make sure the current value of the shell is a valid one
+ if [ -z "$Sflag" ]; then
+ if ! shell_exists $ushell ; then
+ info "Using default shell ${defaultshell}."
+ ushell="$defaultshell"
+ fi
+ fi
+
+ if [ -z "$fflag" ]; then
+ echo -n "Shell ($shells) [`basename $ushell`]: "
+ read _input
+ else
+ _input="`echo "$fileline" | cut -f9 -d:`"
+ fi
+ if [ -n "$_input" ]; then
+ if [ -n "$Sflag" ]; then
+ ushell="$_input"
+ else
+ _fullpath=`fullpath_from_shell $_input`
+ if [ -n "$_fullpath" ]; then
+ ushell="$_fullpath"
+ else
+ err "Invalid shell ($_input) for user $username."
+ info "Using default shell ${defaultshell}."
+ ushell="$defaultshell"
+ fi
+ fi
+ fi
+}
+
+# get_homedir
+# Reads the account's home directory. Used both with interactive input
+# and batch input.
+#
+get_homedir() {
+ _input=
+ if [ -z "$fflag" ]; then
+ echo -n "Home directory [${homeprefix}/${username}]: "
+ read _input
+ else
+ _input="`echo "$fileline" | cut -f8 -d:`"
+ fi
+
+ if [ -n "$_input" ]; then
+ uhome="$_input"
+ # if this is a configuration run, then user input is the home
+ # directory prefix. Otherwise it is understood to
+ # be $prefix/$user
+ #
+ [ -z "$configflag" ] && homeprefix="`dirname $uhome`" || homeprefix="$uhome"
+ else
+ uhome="${homeprefix}/${username}"
+ fi
+}
+
+# get_homeperm
+# Reads the account's home directory permissions.
+#
+get_homeperm() {
+ uhomeperm=$defaultHomePerm
+ _input=
+ _prompt=
+
+ if [ -n "$uhomeperm" ]; then
+ _prompt="Home directory permissions [${uhomeperm}]: "
+ else
+ _prompt="Home directory permissions (Leave empty for default): "
+ fi
+ if [ -z "$fflag" ]; then
+ echo -n "$_prompt"
+ read _input
+ fi
+
+ if [ -n "$_input" ]; then
+ uhomeperm="$_input"
+ fi
+}
+
+# get_uid
+# Reads a numeric userid in an interactive or batch session. Automatically
+# allocates one if it is not specified.
+#
+get_uid() {
+ uuid=${uidstart}
+ _input=
+ _prompt=
+
+ if [ -n "$uuid" ]; then
+ _prompt="Uid [$uuid]: "
+ else
+ _prompt="Uid (Leave empty for default): "
+ fi
+ if [ -z "$fflag" ]; then
+ echo -n "$_prompt"
+ read _input
+ else
+ _input="`echo "$fileline" | cut -f2 -d:`"
+ fi
+
+ [ -n "$_input" ] && uuid=$_input
+ uuid=`get_nextuid $uuid`
+ uidstart=$uuid
+}
+
+# get_class
+# Reads login class of account. Can be used in interactive or batch mode.
+#
+get_class() {
+ uclass="$defaultclass"
+ _input=
+ _class=${uclass:-"default"}
+
+ if [ -z "$fflag" ]; then
+ echo -n "Login class [$_class]: "
+ read _input
+ else
+ _input="`echo "$fileline" | cut -f4 -d:`"
+ fi
+
+ [ -n "$_input" ] && uclass="$_input"
+}
+
+# get_logingroup
+# Reads user's login group. Can be used in both interactive and batch
+# modes. The specified value can be a group name or its numeric id.
+# This routine leaves the field blank if nothing is provided and
+# a default login group has not been set. The pw(8) command
+# will then provide a login group with the same name as the username.
+#
+get_logingroup() {
+ ulogingroup="$defaultLgroup"
+ _input=
+
+ if [ -z "$fflag" ]; then
+ echo -n "Login group [${ulogingroup:-$username}]: "
+ read _input
+ else
+ _input="`echo "$fileline" | cut -f3 -d:`"
+ fi
+
+ # Pw(8) will use the username as login group if it's left empty
+ [ -n "$_input" ] && ulogingroup="$_input"
+}
+
+# get_groups
+# Read additional groups for the user. It can be used in both interactive
+# and batch modes.
+#
+get_groups() {
+ ugroups="$defaultgroups"
+ _input=
+ _group=${ulogingroup:-"${username}"}
+
+ if [ -z "$configflag" ]; then
+ [ -z "$fflag" ] && echo -n "Login group is $_group. Invite $username"
+ [ -z "$fflag" ] && echo -n " into other groups? [$ugroups]: "
+ else
+ [ -z "$fflag" ] && echo -n "Enter additional groups [$ugroups]: "
+ fi
+ read _input
+
+ [ -n "$_input" ] && ugroups="$_input"
+}
+
+# get_expire_dates
+# Read expiry information for the account and also for the password. This
+# routine is used only from batch processing mode.
+#
+get_expire_dates() {
+ upwexpire="`echo "$fileline" | cut -f5 -d:`"
+ uexpire="`echo "$fileline" | cut -f6 -d:`"
+}
+
+# get_password
+# Read the password in batch processing mode. The password field matters
+# only when the password type is "yes" or "random". If the field is empty and the
+# password type is "yes", then it assumes the account has an empty passsword
+# and changes the password type accordingly. If the password type is "random"
+# and the password field is NOT empty, then it assumes the account will NOT
+# have a random password and set passwdtype to "yes."
+#
+get_password() {
+ # We may temporarily change a password type. Make sure it's changed
+ # back to whatever it was before we process the next account.
+ #
+ [ -n "$savedpwtype" ] && {
+ passwdtype=$savedpwtype
+ savedpwtype=
+ }
+
+ # There may be a ':' in the password
+ upass=${fileline#*:*:*:*:*:*:*:*:*:}
+
+ if [ -z "$upass" ]; then
+ case $passwdtype in
+ yes)
+ # if it's empty, assume an empty password
+ passwdtype=none
+ savedpwtype=yes
+ ;;
+ esac
+ else
+ case $passwdtype in
+ random)
+ passwdtype=yes
+ savedpwtype=random
+ ;;
+ esac
+ fi
+}
+
+# input_from_file
+# Reads a line of account information from standard input and
+# adds it to the user database.
+#
+input_from_file() {
+ _field=
+
+ while read -r fileline ; do
+ case "$fileline" in
+ \#*|'')
+ ;;
+ *)
+ get_user || continue
+ get_gecos
+ get_uid
+ get_logingroup
+ get_class
+ get_shell
+ get_homedir
+ get_homeperm
+ get_password
+ get_expire_dates
+ ugroups="$defaultgroups"
+
+ add_user
+ ;;
+ esac
+ done
+}
+
+# input_interactive
+# Prompts for user information interactively, and commits to
+# the user database.
+#
+input_interactive() {
+ _disable=
+ _pass=
+ _passconfirm=
+ _random="no"
+ _emptypass="no"
+ _usepass="yes"
+ _logingroup_ok="no"
+ _groups_ok="no"
+ case $passwdtype in
+ none)
+ _emptypass="yes"
+ _usepass="yes"
+ ;;
+ no)
+ _usepass="no"
+ ;;
+ random)
+ _random="yes"
+ ;;
+ esac
+
+ get_user
+ get_gecos
+ get_uid
+
+ # The case where group = user is handled elsewhere, so
+ # validate any other groups the user is invited to.
+ until [ "$_logingroup_ok" = yes ]; do
+ get_logingroup
+ _logingroup_ok=yes
+ if [ -n "$ulogingroup" -a "$username" != "$ulogingroup" ]; then
+ if ! ${PWCMD} show group $ulogingroup > /dev/null 2>&1; then
+ echo "Group $ulogingroup does not exist!"
+ _logingroup_ok=no
+ fi
+ fi
+ done
+ until [ "$_groups_ok" = yes ]; do
+ get_groups
+ _groups_ok=yes
+ for i in $ugroups; do
+ if [ "$username" != "$i" ]; then
+ if ! ${PWCMD} show group $i > /dev/null 2>&1; then
+ echo "Group $i does not exist!"
+ _groups_ok=no
+ fi
+ fi
+ done
+ done
+
+ get_class
+ get_shell
+ get_homedir
+ get_homeperm
+
+ while : ; do
+ echo -n "Use password-based authentication? [$_usepass]: "
+ read _input
+ [ -z "$_input" ] && _input=$_usepass
+ case $_input in
+ [Nn][Oo]|[Nn])
+ passwdtype="no"
+ ;;
+ [Yy][Ee][Ss]|[Yy][Ee]|[Yy])
+ while : ; do
+ echo -n "Use an empty password? (yes/no) [$_emptypass]: "
+ read _input
+ [ -n "$_input" ] && _emptypass=$_input
+ case $_emptypass in
+ [Nn][Oo]|[Nn])
+ echo -n "Use a random password? (yes/no) [$_random]: "
+ read _input
+ [ -n "$_input" ] && _random="$_input"
+ case $_random in
+ [Yy][Ee][Ss]|[Yy][Ee]|[Yy])
+ passwdtype="random"
+ break
+ ;;
+ esac
+ passwdtype="yes"
+ [ -n "$configflag" ] && break
+ trap 'stty echo; exit' 0 1 2 3 15
+ stty -echo
+ echo -n "Enter password: "
+ read -r upass
+ echo''
+ echo -n "Enter password again: "
+ read -r _passconfirm
+ echo ''
+ stty echo
+ # if user entered a blank password
+ # explicitly ask again.
+ [ -z "$upass" -a -z "$_passconfirm" ] \
+ && continue
+ ;;
+ [Yy][Ee][Ss]|[Yy][Ee]|[Yy])
+ passwdtype="none"
+ break;
+ ;;
+ *)
+ # invalid answer; repeat the loop
+ continue
+ ;;
+ esac
+ if [ "$upass" != "$_passconfirm" ]; then
+ echo "Passwords did not match!"
+ continue
+ fi
+ break
+ done
+ ;;
+ *)
+ # invalid answer; repeat loop
+ continue
+ ;;
+ esac
+ break;
+ done
+ _disable=${disableflag:-"no"}
+ while : ; do
+ echo -n "Lock out the account after creation? [$_disable]: "
+ read _input
+ [ -z "$_input" ] && _input=$_disable
+ case $_input in
+ [Nn][Oo]|[Nn])
+ disableflag=
+ ;;
+ [Yy][Ee][Ss]|[Yy][Ee]|[Yy])
+ disableflag=yes
+ ;;
+ *)
+ # invalid answer; repeat loop
+ continue
+ ;;
+ esac
+ break
+ done
+
+ # Display the information we have so far and prompt to
+ # commit it.
+ #
+ _disable=${disableflag:-"no"}
+ [ -z "$configflag" ] && printf "%-10s : %s\n" Username $username
+ case $passwdtype in
+ yes)
+ _pass='*****'
+ ;;
+ no)
+ _pass='<disabled>'
+ ;;
+ none)
+ _pass='<blank>'
+ ;;
+ random)
+ _pass='<random>'
+ ;;
+ esac
+ [ -z "$configflag" ] && printf "%-10s : %s\n" "Password" "$_pass"
+ [ -n "$configflag" ] && printf "%-10s : %s\n" "Pass Type" "$passwdtype"
+ [ -z "$configflag" ] && printf "%-10s : %s\n" "Full Name" "$ugecos"
+ [ -z "$configflag" ] && printf "%-10s : %s\n" "Uid" "$uuid"
+ printf "%-10s : %s\n" "Class" "$uclass"
+ printf "%-10s : %s %s\n" "Groups" "${ulogingroup:-$username}" "$ugroups"
+ printf "%-10s : %s\n" "Home" "$uhome"
+ printf "%-10s : %s\n" "Home Mode" "$uhomeperm"
+ printf "%-10s : %s\n" "Shell" "$ushell"
+ printf "%-10s : %s\n" "Locked" "$_disable"
+ while : ; do
+ echo -n "OK? (yes/no): "
+ read _input
+ case $_input in
+ [Nn][Oo]|[Nn])
+ return 1
+ ;;
+ [Yy][Ee][Ss]|[Yy][Ee]|[Yy])
+ add_user
+ ;;
+ *)
+ continue
+ ;;
+ esac
+ break
+ done
+ return 0
+}
+
+#### END SUBROUTINE DEFINITION ####
+
+THISCMD=`/usr/bin/basename $0`
+DEFAULTSHELL=/bin/sh
+ADDUSERCONF="${ADDUSERCONF:-/etc/adduser.conf}"
+PWCMD="${PWCMD:-/usr/sbin/pw}"
+MAILCMD="${MAILCMD:-mail}"
+ETCSHELLS="${ETCSHELLS:-/etc/shells}"
+NOHOME="/nonexistent"
+NOLOGIN="nologin"
+NOLOGIN_PATH="/usr/sbin/nologin"
+GREPCMD="/usr/bin/grep"
+DATECMD="/bin/date"
+
+# Set default values
+#
+username=
+uuid=
+uidstart=
+ugecos=
+ulogingroup=
+uclass=
+uhome=
+uhomeperm=
+upass=
+ushell=
+udotdir=/usr/share/skel
+ugroups=
+uexpire=
+upwexpire=
+shells="`valid_shells`"
+passwdtype="yes"
+msgfile=/etc/adduser.msg
+msgflag=
+quietflag=
+configflag=
+fflag=
+infile=
+disableflag=
+Dflag=
+Sflag=
+readconfig="yes"
+homeprefix="/home"
+randompass=
+fileline=
+savedpwtype=
+defaultclass=
+defaultLgroup=
+defaultgroups=
+defaultshell="${DEFAULTSHELL}"
+defaultHomePerm=
+
+# Make sure the user running this program is root. This isn't a security
+# measure as much as it is a useful method of reminding the user to
+# 'su -' before he/she wastes time entering data that won't be saved.
+#
+procowner=${procowner:-`/usr/bin/id -u`}
+if [ "$procowner" != "0" ]; then
+ err 'you must be the super-user (uid 0) to use this utility.'
+ exit 1
+fi
+
+# Override from our conf file
+# Quickly go through the commandline line to see if we should read
+# from our configuration file. The actual parsing of the commandline
+# arguments happens after we read in our configuration file (commandline
+# should override configuration file).
+#
+for _i in $* ; do
+ if [ "$_i" = "-N" ]; then
+ readconfig=
+ break;
+ fi
+done
+if [ -n "$readconfig" ]; then
+ # On a long-lived system, the first time this script is run it
+ # will barf upon reading the configuration file for its perl predecessor.
+ if ( . ${ADDUSERCONF} > /dev/null 2>&1 ); then
+ [ -r ${ADDUSERCONF} ] && . ${ADDUSERCONF} > /dev/null 2>&1
+ fi
+fi
+
+# Process command-line options
+#
+for _switch ; do
+ case $_switch in
+ -L)
+ defaultclass="$2"
+ shift; shift
+ ;;
+ -C)
+ configflag=yes
+ shift
+ ;;
+ -D)
+ Dflag=yes
+ shift
+ ;;
+ -E)
+ disableflag=yes
+ shift
+ ;;
+ -k)
+ udotdir="$2"
+ shift; shift
+ ;;
+ -f)
+ [ "$2" != "-" ] && infile="$2"
+ fflag=yes
+ shift; shift
+ ;;
+ -g)
+ defaultLgroup="$2"
+ shift; shift
+ ;;
+ -G)
+ defaultgroups="$2"
+ shift; shift
+ ;;
+ -h)
+ show_usage
+ exit 0
+ ;;
+ -d)
+ homeprefix="$2"
+ shift; shift
+ ;;
+ -m)
+ case "$2" in
+ [Nn][Oo])
+ msgflag=
+ ;;
+ *)
+ msgflag=yes
+ msgfile="$2"
+ ;;
+ esac
+ shift; shift
+ ;;
+ -M)
+ defaultHomePerm=$2
+ shift; shift
+ ;;
+ -N)
+ readconfig=
+ shift
+ ;;
+ -w)
+ case "$2" in
+ no|none|random|yes)
+ passwdtype=$2
+ ;;
+ *)
+ show_usage
+ exit 1
+ ;;
+ esac
+ shift; shift
+ ;;
+ -q)
+ quietflag=yes
+ shift
+ ;;
+ -s)
+ defaultshell="`fullpath_from_shell $2`"
+ shift; shift
+ ;;
+ -S)
+ Sflag=yes
+ shift
+ ;;
+ -u)
+ uidstart=$2
+ shift; shift
+ ;;
+ esac
+done
+
+# If the -f switch was used, get input from a file. Otherwise,
+# this is an interactive session.
+#
+if [ -n "$fflag" ]; then
+ if [ -z "$infile" ]; then
+ input_from_file
+ elif [ -n "$infile" ]; then
+ if [ -r "$infile" ]; then
+ input_from_file < $infile
+ else
+ err "File ($infile) is unreadable or does not exist."
+ fi
+ fi
+else
+ input_interactive
+ while : ; do
+ if [ -z "$configflag" ]; then
+ echo -n "Add another user? (yes/no): "
+ else
+ echo -n "Re-edit the default configuration? (yes/no): "
+ fi
+ read _input
+ case $_input in
+ [Yy][Ee][Ss]|[Yy][Ee]|[Yy])
+ uidstart=`get_nextuid $uidstart`
+ input_interactive
+ continue
+ ;;
+ [Nn][Oo]|[Nn])
+ echo "Goodbye!"
+ ;;
+ *)
+ continue
+ ;;
+ esac
+ break
+ done
+fi
diff --git a/libutil/gr_util.c b/libutil/gr_util.c
new file mode 100644
index 0000000..0173595
--- /dev/null
+++ b/libutil/gr_util.c
@@ -0,0 +1,546 @@
+/*-
+ * Copyright (c) 2008 Sean C. Farley <scf@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,
+ * without modification, immediately at the beginning of the file.
+ * 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 ``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 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/param.h>
+#include <sys/errno.h>
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <fcntl.h>
+#include <grp.h>
+#include <inttypes.h>
+#include <libutil.h>
+#include <paths.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+struct group_storage {
+ struct group gr;
+ char *members[];
+};
+
+static int lockfd = -1;
+static char group_dir[PATH_MAX];
+static char group_file[PATH_MAX];
+static char tempname[PATH_MAX];
+static int initialized;
+
+static const char group_line_format[] = "%s:%s:%ju:";
+
+/*
+ * Initialize statics
+ */
+int
+gr_init(const char *dir, const char *group)
+{
+ if (dir == NULL) {
+ strcpy(group_dir, _PATH_ETC);
+ } else {
+ if (strlen(dir) >= sizeof(group_dir)) {
+ errno = ENAMETOOLONG;
+ return (-1);
+ }
+ strcpy(group_dir, dir);
+ }
+
+ if (group == NULL) {
+ if (dir == NULL) {
+ strcpy(group_file, _PATH_GROUP);
+ } else if (snprintf(group_file, sizeof(group_file), "%s/group",
+ group_dir) > (int)sizeof(group_file)) {
+ errno = ENAMETOOLONG;
+ return (-1);
+ }
+ } else {
+ if (strlen(group) >= sizeof(group_file)) {
+ errno = ENAMETOOLONG;
+ return (-1);
+ }
+ strcpy(group_file, group);
+ }
+ initialized = 1;
+ return (0);
+}
+
+/*
+ * Lock the group file
+ */
+int
+gr_lock(void)
+{
+ if (*group_file == '\0')
+ return (-1);
+
+ for (;;) {
+ struct stat st;
+
+ lockfd = open(group_file, O_RDONLY, 0);
+ if (lockfd < 0 || fcntl(lockfd, F_SETFD, 1) == -1)
+ err(1, "%s", group_file);
+ if (flock(lockfd, LOCK_EX|LOCK_NB) == -1) {
+ if (errno == EWOULDBLOCK) {
+ errx(1, "the group file is busy");
+ } else {
+ err(1, "could not lock the group file: ");
+ }
+ }
+ if (fstat(lockfd, &st) == -1)
+ err(1, "fstat() failed: ");
+ if (st.st_nlink != 0)
+ break;
+ close(lockfd);
+ lockfd = -1;
+ }
+ return (lockfd);
+}
+
+/*
+ * Create and open a presmuably safe temp file for editing group data
+ */
+int
+gr_tmp(int mfd)
+{
+ char buf[8192];
+ ssize_t nr;
+ const char *p;
+ int tfd;
+
+ if (*group_file == '\0')
+ return (-1);
+ if ((p = strrchr(group_file, '/')))
+ ++p;
+ else
+ p = group_file;
+ if (snprintf(tempname, sizeof(tempname), "%.*sgroup.XXXXXX",
+ (int)(p - group_file), group_file) >= (int)sizeof(tempname)) {
+ errno = ENAMETOOLONG;
+ return (-1);
+ }
+ if ((tfd = mkstemp(tempname)) == -1)
+ return (-1);
+ if (mfd != -1) {
+ while ((nr = read(mfd, buf, sizeof(buf))) > 0)
+ if (write(tfd, buf, (size_t)nr) != nr)
+ break;
+ if (nr != 0) {
+ unlink(tempname);
+ *tempname = '\0';
+ close(tfd);
+ return (-1);
+ }
+ }
+ return (tfd);
+}
+
+/*
+ * Copy the group file from one descriptor to another, replacing, deleting
+ * or adding a single record on the way.
+ */
+int
+gr_copy(int ffd, int tfd, const struct group *gr, struct group *old_gr)
+{
+ char buf[8192], *end, *line, *p, *q, *r, t;
+ struct group *fgr;
+ const struct group *sgr;
+ size_t len;
+ int eof, readlen;
+
+ sgr = gr;
+ if (gr == NULL) {
+ line = NULL;
+ if (old_gr == NULL)
+ return (-1);
+ sgr = old_gr;
+ } else if ((line = gr_make(gr)) == NULL)
+ return (-1);
+
+ eof = 0;
+ len = 0;
+ p = q = end = buf;
+ for (;;) {
+ /* find the end of the current line */
+ for (p = q; q < end && *q != '\0'; ++q)
+ if (*q == '\n')
+ break;
+
+ /* if we don't have a complete line, fill up the buffer */
+ if (q >= end) {
+ if (eof)
+ break;
+ if ((size_t)(q - p) >= sizeof(buf)) {
+ warnx("group line too long");
+ errno = EINVAL; /* hack */
+ goto err;
+ }
+ if (p < end) {
+ q = memmove(buf, p, end -p);
+ end -= p - buf;
+ } else {
+ p = q = end = buf;
+ }
+ readlen = read(ffd, end, sizeof(buf) - (end -buf));
+ if (readlen == -1)
+ goto err;
+ else
+ len = (size_t)readlen;
+ if (len == 0 && p == buf)
+ break;
+ end += len;
+ len = end - buf;
+ if (len < (ssize_t)sizeof(buf)) {
+ eof = 1;
+ if (len > 0 && buf[len -1] != '\n')
+ ++len, *end++ = '\n';
+ }
+ continue;
+ }
+
+ /* is it a blank line or a comment? */
+ for (r = p; r < q && isspace(*r); ++r)
+ /* nothing */;
+ if (r == q || *r == '#') {
+ /* yep */
+ if (write(tfd, p, q -p + 1) != q - p + 1)
+ goto err;
+ ++q;
+ continue;
+ }
+
+ /* is it the one we're looking for? */
+
+ t = *q;
+ *q = '\0';
+
+ fgr = gr_scan(r);
+
+ /* fgr is either a struct group for the current line,
+ * or NULL if the line is malformed.
+ */
+
+ *q = t;
+ if (fgr == NULL || fgr->gr_gid != sgr->gr_gid) {
+ /* nope */
+ if (fgr != NULL)
+ free(fgr);
+ if (write(tfd, p, q - p + 1) != q - p + 1)
+ goto err;
+ ++q;
+ continue;
+ }
+ if (old_gr && !gr_equal(fgr, old_gr)) {
+ warnx("entry inconsistent");
+ free(fgr);
+ errno = EINVAL; /* hack */
+ goto err;
+ }
+ free(fgr);
+
+ /* it is, replace or remove it */
+ if (line != NULL) {
+ len = strlen(line);
+ if (write(tfd, line, len) != (int) len)
+ goto err;
+ } else {
+ /* when removed, avoid the \n */
+ q++;
+ }
+ /* we're done, just copy the rest over */
+ for (;;) {
+ if (write(tfd, q, end - q) != end - q)
+ goto err;
+ q = buf;
+ readlen = read(ffd, buf, sizeof(buf));
+ if (readlen == 0)
+ break;
+ else
+ len = (size_t)readlen;
+ if (readlen == -1)
+ goto err;
+ end = buf + len;
+ }
+ goto done;
+ }
+
+ /* if we got here, we didn't find the old entry */
+ if (line == NULL) {
+ errno = ENOENT;
+ goto err;
+ }
+ len = strlen(line);
+ if ((size_t)write(tfd, line, len) != len ||
+ write(tfd, "\n", 1) != 1)
+ goto err;
+ done:
+ if (line != NULL)
+ free(line);
+ return (0);
+ err:
+ if (line != NULL)
+ free(line);
+ return (-1);
+}
+
+/*
+ * Regenerate the group file
+ */
+int
+gr_mkdb(void)
+{
+ return (rename(tempname, group_file));
+}
+
+/*
+ * Clean up. Preserver errno for the caller's convenience.
+ */
+void
+gr_fini(void)
+{
+ int serrno;
+
+ if (!initialized)
+ return;
+ initialized = 0;
+ serrno = errno;
+ if (*tempname != '\0') {
+ unlink(tempname);
+ *tempname = '\0';
+ }
+ if (lockfd != -1)
+ close(lockfd);
+ errno = serrno;
+}
+
+/*
+ * Compares two struct group's.
+ */
+int
+gr_equal(const struct group *gr1, const struct group *gr2)
+{
+ int gr1_ndx;
+ int gr2_ndx;
+ bool found;
+
+ /* Check that the non-member information is the same. */
+ if (gr1->gr_name == NULL || gr2->gr_name == NULL) {
+ if (gr1->gr_name != gr2->gr_name)
+ return (false);
+ } else if (strcmp(gr1->gr_name, gr2->gr_name) != 0)
+ return (false);
+ if (gr1->gr_passwd == NULL || gr2->gr_passwd == NULL) {
+ if (gr1->gr_passwd != gr2->gr_passwd)
+ return (false);
+ } else if (strcmp(gr1->gr_passwd, gr2->gr_passwd) != 0)
+ return (false);
+ if (gr1->gr_gid != gr2->gr_gid)
+ return (false);
+
+ /* Check all members in both groups. */
+ if (gr1->gr_mem == NULL || gr2->gr_mem == NULL) {
+ if (gr1->gr_mem != gr2->gr_mem)
+ return (false);
+ } else {
+ for (found = false, gr1_ndx = 0; gr1->gr_mem[gr1_ndx] != NULL;
+ gr1_ndx++) {
+ for (gr2_ndx = 0; gr2->gr_mem[gr2_ndx] != NULL;
+ gr2_ndx++)
+ if (strcmp(gr1->gr_mem[gr1_ndx],
+ gr2->gr_mem[gr2_ndx]) == 0) {
+ found = true;
+ break;
+ }
+ if (!found)
+ return (false);
+ }
+
+ /* Check that group2 does not have more members than group1. */
+ if (gr2->gr_mem[gr1_ndx] != NULL)
+ return (false);
+ }
+
+ return (true);
+}
+
+/*
+ * Make a group line out of a struct group.
+ */
+char *
+gr_make(const struct group *gr)
+{
+ char *line;
+ size_t line_size;
+ int ndx;
+
+ /* Calculate the length of the group line. */
+ line_size = snprintf(NULL, 0, group_line_format, gr->gr_name,
+ gr->gr_passwd, (uintmax_t)gr->gr_gid) + 1;
+ if (gr->gr_mem != NULL) {
+ for (ndx = 0; gr->gr_mem[ndx] != NULL; ndx++)
+ line_size += strlen(gr->gr_mem[ndx]) + 1;
+ if (ndx > 0)
+ line_size--;
+ }
+
+ /* Create the group line and fill it. */
+ if ((line = malloc(line_size)) == NULL)
+ return (NULL);
+ snprintf(line, line_size, group_line_format, gr->gr_name, gr->gr_passwd,
+ (uintmax_t)gr->gr_gid);
+ if (gr->gr_mem != NULL)
+ for (ndx = 0; gr->gr_mem[ndx] != NULL; ndx++) {
+ strcat(line, gr->gr_mem[ndx]);
+ if (gr->gr_mem[ndx + 1] != NULL)
+ strcat(line, ",");
+ }
+
+ return (line);
+}
+
+/*
+ * Duplicate a struct group.
+ */
+struct group *
+gr_dup(const struct group *gr)
+{
+ char *dst;
+ size_t len;
+ struct group_storage *gs;
+ int ndx;
+ int num_mem;
+
+ /* Calculate size of the group. */
+ len = sizeof(*gs);
+ if (gr->gr_name != NULL)
+ len += strlen(gr->gr_name) + 1;
+ if (gr->gr_passwd != NULL)
+ len += strlen(gr->gr_passwd) + 1;
+ if (gr->gr_mem != NULL) {
+ for (num_mem = 0; gr->gr_mem[num_mem] != NULL; num_mem++)
+ len += strlen(gr->gr_mem[num_mem]) + 1;
+ len += (num_mem + 1) * sizeof(*gr->gr_mem);
+ } else
+ num_mem = -1;
+
+ /* Create new group and copy old group into it. */
+ if ((gs = calloc(1, len)) == NULL)
+ return (NULL);
+ dst = (char *)&gs->members[num_mem + 1];
+ if (gr->gr_name != NULL) {
+ gs->gr.gr_name = dst;
+ dst = stpcpy(gs->gr.gr_name, gr->gr_name) + 1;
+ }
+ if (gr->gr_passwd != NULL) {
+ gs->gr.gr_passwd = dst;
+ dst = stpcpy(gs->gr.gr_passwd, gr->gr_passwd) + 1;
+ }
+ gs->gr.gr_gid = gr->gr_gid;
+ if (gr->gr_mem != NULL) {
+ gs->gr.gr_mem = gs->members;
+ for (ndx = 0; ndx < num_mem; ndx++) {
+ gs->gr.gr_mem[ndx] = dst;
+ dst = stpcpy(gs->gr.gr_mem[ndx], gr->gr_mem[ndx]) + 1;
+ }
+ gs->gr.gr_mem[ndx] = NULL;
+ }
+
+ return (&gs->gr);
+}
+
+/*
+ * Scan a line and place it into a group structure.
+ */
+static bool
+__gr_scan(char *line, struct group *gr)
+{
+ char *loc;
+ int ndx;
+
+ /* Assign non-member information to structure. */
+ gr->gr_name = line;
+ if ((loc = strchr(line, ':')) == NULL)
+ return (false);
+ *loc = '\0';
+ gr->gr_passwd = loc + 1;
+ if (*gr->gr_passwd == ':')
+ *gr->gr_passwd = '\0';
+ else {
+ if ((loc = strchr(loc + 1, ':')) == NULL)
+ return (false);
+ *loc = '\0';
+ }
+ if (sscanf(loc + 1, "%u", &gr->gr_gid) != 1)
+ return (false);
+
+ /* Assign member information to structure. */
+ if ((loc = strchr(loc + 1, ':')) == NULL)
+ return (false);
+ line = loc + 1;
+ gr->gr_mem = NULL;
+ ndx = 0;
+ do {
+ gr->gr_mem = reallocf(gr->gr_mem, sizeof(*gr->gr_mem) *
+ (ndx + 1));
+ if (gr->gr_mem == NULL)
+ return (false);
+
+ /* Skip locations without members (i.e., empty string). */
+ do {
+ gr->gr_mem[ndx] = strsep(&line, ",");
+ } while (gr->gr_mem[ndx] != NULL && *gr->gr_mem[ndx] == '\0');
+ } while (gr->gr_mem[ndx++] != NULL);
+
+ return (true);
+}
+
+/*
+ * Create a struct group from a line.
+ */
+struct group *
+gr_scan(const char *line)
+{
+ struct group gr;
+ char *line_copy;
+ struct group *new_gr;
+
+ if ((line_copy = strdup(line)) == NULL)
+ return (NULL);
+ if (!__gr_scan(line_copy, &gr)) {
+ free(line_copy);
+ return (NULL);
+ }
+ new_gr = gr_dup(&gr);
+ free(line_copy);
+ if (gr.gr_mem != NULL)
+ free(gr.gr_mem);
+
+ return (new_gr);
+}
diff --git a/libutil/libutil.h b/libutil/libutil.h
index fc6d3bb..dea14cf 100644
--- a/libutil/libutil.h
+++ b/libutil/libutil.h
@@ -152,9 +152,15 @@ int pw_tmp(int _mfd);
#endif
#ifdef _GRP_H_
+int gr_copy(int __ffd, int _tfd, const struct group *_gr, struct group *_old_gr);
+struct group *gr_dup(const struct group *gr);
int gr_equal(const struct group *gr1, const struct group *gr2);
+void gr_fini(void);
+int gr_init(const char *_dir, const char *_master);
+int gr_lock(void);
char *gr_make(const struct group *gr);
-struct group *gr_dup(const struct group *gr);
+int gr_mkdb(void);
+int gr_tmp(int _mdf);
struct group *gr_scan(const char *line);
#endif
diff --git a/libutil/pw_util.c b/libutil/pw_util.c
new file mode 100644
index 0000000..1068eff
--- /dev/null
+++ b/libutil/pw_util.c
@@ -0,0 +1,645 @@
+/*-
+ * Copyright (c) 1990, 1993, 1994
+ * The Regents of the University of California. All rights reserved.
+ * Copyright (c) 2002 Networks Associates Technology, Inc.
+ * All rights reserved.
+ *
+ * Portions of this software were developed for the FreeBSD Project by
+ * ThinkSec AS and NAI Labs, the Security Research Division of Network
+ * Associates, Inc. under DARPA/SPAWAR contract N66001-01-C-8035
+ * ("CBOSS"), as part of the DARPA CHATS research program.
+ *
+ * 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.
+ * 4. Neither the name of the University nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS 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 THE REGENTS 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
+#if 0
+static const char sccsid[] = "@(#)pw_util.c 8.3 (Berkeley) 4/2/94";
+#endif
+static const char rcsid[] =
+ "$FreeBSD$";
+#endif /* not lint */
+
+/*
+ * This file is used by all the "password" programs; vipw(8), chpass(1),
+ * and passwd(1).
+ */
+
+#include <sys/param.h>
+#include <sys/errno.h>
+#include <sys/time.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <libgen.h>
+#include <paths.h>
+#include <pwd.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "libutil.h"
+
+static pid_t editpid = -1;
+static int lockfd = -1;
+static char masterpasswd[PATH_MAX];
+static char passwd_dir[PATH_MAX];
+static char tempname[PATH_MAX];
+static int initialized;
+
+#if 0
+void
+pw_cont(int sig)
+{
+
+ if (editpid != -1)
+ kill(editpid, sig);
+}
+#endif
+
+/*
+ * Initialize statics and set limits, signals & umask to try to avoid
+ * interruptions, crashes etc. that might expose passord data.
+ */
+int
+pw_init(const char *dir, const char *master)
+{
+#if 0
+ struct rlimit rlim;
+#endif
+
+ if (dir == NULL) {
+ strcpy(passwd_dir, _PATH_ETC);
+ } else {
+ if (strlen(dir) >= sizeof(passwd_dir)) {
+ errno = ENAMETOOLONG;
+ return (-1);
+ }
+ strcpy(passwd_dir, dir);
+ }
+
+ if (master == NULL) {
+ if (dir == NULL) {
+ strcpy(masterpasswd, _PATH_MASTERPASSWD);
+ } else if (snprintf(masterpasswd, sizeof(masterpasswd), "%s/%s",
+ passwd_dir, _MASTERPASSWD) > (int)sizeof(masterpasswd)) {
+ errno = ENAMETOOLONG;
+ return (-1);
+ }
+ } else {
+ if (strlen(master) >= sizeof(masterpasswd)) {
+ errno = ENAMETOOLONG;
+ return (-1);
+ }
+ strcpy(masterpasswd, master);
+ }
+
+ /*
+ * The code that follows is extremely disruptive to the calling
+ * process, and is therefore disabled until someone can conceive
+ * of a realistic scenario where it would fend off a compromise.
+ * Race conditions concerning the temporary files can be guarded
+ * against in other ways than masking signals (by checking stat(2)
+ * results after creation).
+ */
+#if 0
+ /* Unlimited resource limits. */
+ rlim.rlim_cur = rlim.rlim_max = RLIM_INFINITY;
+ (void)setrlimit(RLIMIT_CPU, &rlim);
+ (void)setrlimit(RLIMIT_FSIZE, &rlim);
+ (void)setrlimit(RLIMIT_STACK, &rlim);
+ (void)setrlimit(RLIMIT_DATA, &rlim);
+ (void)setrlimit(RLIMIT_RSS, &rlim);
+
+ /* Don't drop core (not really necessary, but GP's). */
+ rlim.rlim_cur = rlim.rlim_max = 0;
+ (void)setrlimit(RLIMIT_CORE, &rlim);
+
+ /* Turn off signals. */
+ (void)signal(SIGALRM, SIG_IGN);
+ (void)signal(SIGHUP, SIG_IGN);
+ (void)signal(SIGINT, SIG_IGN);
+ (void)signal(SIGPIPE, SIG_IGN);
+ (void)signal(SIGQUIT, SIG_IGN);
+ (void)signal(SIGTERM, SIG_IGN);
+ (void)signal(SIGCONT, pw_cont);
+
+ /* Create with exact permissions. */
+ (void)umask(0);
+#endif
+ initialized = 1;
+ return (0);
+}
+
+/*
+ * Lock the master password file.
+ */
+int
+pw_lock(void)
+{
+
+ if (*masterpasswd == '\0')
+ return (-1);
+
+ /*
+ * If the master password file doesn't exist, the system is hosed.
+ * Might as well try to build one. Set the close-on-exec bit so
+ * that users can't get at the encrypted passwords while editing.
+ * Open should allow flock'ing the file; see 4.4BSD. XXX
+ */
+ for (;;) {
+ struct stat st;
+
+ lockfd = open(masterpasswd, O_RDONLY, 0);
+ if (lockfd < 0 || fcntl(lockfd, F_SETFD, 1) == -1)
+ err(1, "%s", masterpasswd);
+ /* XXX vulnerable to race conditions */
+ if (flock(lockfd, LOCK_EX|LOCK_NB) == -1) {
+ if (errno == EWOULDBLOCK) {
+ errx(1, "the password db file is busy");
+ } else {
+ err(1, "could not lock the passwd file: ");
+ }
+ }
+
+ /*
+ * If the password file was replaced while we were trying to
+ * get the lock, our hardlink count will be 0 and we have to
+ * close and retry.
+ */
+ if (fstat(lockfd, &st) == -1)
+ err(1, "fstat() failed: ");
+ if (st.st_nlink != 0)
+ break;
+ close(lockfd);
+ lockfd = -1;
+ }
+ return (lockfd);
+}
+
+/*
+ * Create and open a presumably safe temp file for editing the password
+ * data, and copy the master password file into it.
+ */
+int
+pw_tmp(int mfd)
+{
+ char buf[8192];
+ ssize_t nr;
+ const char *p;
+ int tfd;
+
+ if (*masterpasswd == '\0')
+ return (-1);
+ if ((p = strrchr(masterpasswd, '/')))
+ ++p;
+ else
+ p = masterpasswd;
+ if (snprintf(tempname, sizeof(tempname), "%.*spw.XXXXXX",
+ (int)(p - masterpasswd), masterpasswd) >= (int)sizeof(tempname)) {
+ errno = ENAMETOOLONG;
+ return (-1);
+ }
+ if ((tfd = mkstemp(tempname)) == -1)
+ return (-1);
+ if (mfd != -1) {
+ while ((nr = read(mfd, buf, sizeof(buf))) > 0)
+ if (write(tfd, buf, (size_t)nr) != nr)
+ break;
+ if (nr != 0) {
+ unlink(tempname);
+ *tempname = '\0';
+ close(tfd);
+ return (-1);
+ }
+ }
+ return (tfd);
+}
+
+/*
+ * Regenerate the password database.
+ */
+int
+pw_mkdb(const char *user)
+{
+ int pstat;
+ pid_t pid;
+
+ (void)fflush(stderr);
+ switch ((pid = fork())) {
+ case -1:
+ return (-1);
+ case 0:
+ /* child */
+ if (user == NULL)
+ execl(_PATH_PWD_MKDB, "pwd_mkdb", "-p",
+ "-d", passwd_dir, tempname, (char *)NULL);
+ else
+ execl(_PATH_PWD_MKDB, "pwd_mkdb", "-p",
+ "-d", passwd_dir, "-u", user, tempname,
+ (char *)NULL);
+ _exit(1);
+ /* NOTREACHED */
+ default:
+ /* parent */
+ break;
+ }
+ if (waitpid(pid, &pstat, 0) == -1)
+ return (-1);
+ if (WIFEXITED(pstat) && WEXITSTATUS(pstat) == 0)
+ return (0);
+ errno = 0;
+ return (-1);
+}
+
+/*
+ * Edit the temp file. Return -1 on error, >0 if the file was modified, 0
+ * if it was not.
+ */
+int
+pw_edit(int notsetuid)
+{
+ struct sigaction sa, sa_int, sa_quit;
+ sigset_t oldsigset, nsigset;
+ struct stat st1, st2;
+ const char *editor;
+ int pstat;
+
+ if ((editor = getenv("EDITOR")) == NULL)
+ editor = _PATH_VI;
+ if (stat(tempname, &st1) == -1)
+ return (-1);
+ sa.sa_handler = SIG_IGN;
+ sigemptyset(&sa.sa_mask);
+ sa.sa_flags = 0;
+ sigaction(SIGINT, &sa, &sa_int);
+ sigaction(SIGQUIT, &sa, &sa_quit);
+ sigemptyset(&nsigset);
+ sigaddset(&nsigset, SIGCHLD);
+ sigprocmask(SIG_BLOCK, &nsigset, &oldsigset);
+ switch ((editpid = fork())) {
+ case -1:
+ return (-1);
+ case 0:
+ sigaction(SIGINT, &sa_int, NULL);
+ sigaction(SIGQUIT, &sa_quit, NULL);
+ sigprocmask(SIG_SETMASK, &oldsigset, NULL);
+ if (notsetuid) {
+ (void)setgid(getgid());
+ (void)setuid(getuid());
+ }
+ errno = 0;
+ execlp(editor, basename(editor), tempname, (char *)NULL);
+ _exit(errno);
+ default:
+ /* parent */
+ break;
+ }
+ for (;;) {
+ if (waitpid(editpid, &pstat, WUNTRACED) == -1) {
+ if (errno == EINTR)
+ continue;
+ unlink(tempname);
+ editpid = -1;
+ break;
+ } else if (WIFSTOPPED(pstat)) {
+ raise(WSTOPSIG(pstat));
+ } else if (WIFEXITED(pstat) && WEXITSTATUS(pstat) == 0) {
+ editpid = -1;
+ break;
+ } else {
+ unlink(tempname);
+ editpid = -1;
+ break;
+ }
+ }
+ sigaction(SIGINT, &sa_int, NULL);
+ sigaction(SIGQUIT, &sa_quit, NULL);
+ sigprocmask(SIG_SETMASK, &oldsigset, NULL);
+ if (stat(tempname, &st2) == -1)
+ return (-1);
+ return (st1.st_mtime != st2.st_mtime);
+}
+
+/*
+ * Clean up. Preserve errno for the caller's convenience.
+ */
+void
+pw_fini(void)
+{
+ int serrno, status;
+
+ if (!initialized)
+ return;
+ initialized = 0;
+ serrno = errno;
+ if (editpid != -1) {
+ kill(editpid, SIGTERM);
+ kill(editpid, SIGCONT);
+ waitpid(editpid, &status, 0);
+ editpid = -1;
+ }
+ if (*tempname != '\0') {
+ unlink(tempname);
+ *tempname = '\0';
+ }
+ if (lockfd != -1)
+ close(lockfd);
+ errno = serrno;
+}
+
+/*
+ * Compares two struct pwds.
+ */
+int
+pw_equal(const struct passwd *pw1, const struct passwd *pw2)
+{
+ return (strcmp(pw1->pw_name, pw2->pw_name) == 0 &&
+ pw1->pw_uid == pw2->pw_uid &&
+ pw1->pw_gid == pw2->pw_gid &&
+ strcmp(pw1->pw_class, pw2->pw_class) == 0 &&
+ pw1->pw_change == pw2->pw_change &&
+ pw1->pw_expire == pw2->pw_expire &&
+ strcmp(pw1->pw_gecos, pw2->pw_gecos) == 0 &&
+ strcmp(pw1->pw_dir, pw2->pw_dir) == 0 &&
+ strcmp(pw1->pw_shell, pw2->pw_shell) == 0);
+}
+
+/*
+ * Make a passwd line out of a struct passwd.
+ */
+char *
+pw_make(const struct passwd *pw)
+{
+ char *line;
+
+ asprintf(&line, "%s:%s:%ju:%ju:%s:%ju:%ju:%s:%s:%s", pw->pw_name,
+ pw->pw_passwd, (uintmax_t)pw->pw_uid, (uintmax_t)pw->pw_gid,
+ pw->pw_class, (uintmax_t)pw->pw_change, (uintmax_t)pw->pw_expire,
+ pw->pw_gecos, pw->pw_dir, pw->pw_shell);
+ return line;
+}
+
+/*
+ * Copy password file from one descriptor to another, replacing, deleting
+ * or adding a single record on the way.
+ */
+int
+pw_copy(int ffd, int tfd, const struct passwd *pw, struct passwd *old_pw)
+{
+ char buf[8192], *end, *line, *p, *q, *r, t;
+ struct passwd *fpw;
+ const struct passwd *spw;
+ size_t len;
+ int eof, readlen;
+
+ spw = pw;
+ if (pw == NULL) {
+ line = NULL;
+ if (old_pw == NULL)
+ return (-1);
+ spw = old_pw;
+ } else if ((line = pw_make(pw)) == NULL)
+ return (-1);
+
+ eof = 0;
+ len = 0;
+ p = q = end = buf;
+ for (;;) {
+ /* find the end of the current line */
+ for (p = q; q < end && *q != '\0'; ++q)
+ if (*q == '\n')
+ break;
+
+ /* if we don't have a complete line, fill up the buffer */
+ if (q >= end) {
+ if (eof)
+ break;
+ if ((size_t)(q - p) >= sizeof(buf)) {
+ warnx("passwd line too long");
+ errno = EINVAL; /* hack */
+ goto err;
+ }
+ if (p < end) {
+ q = memmove(buf, p, end - p);
+ end -= p - buf;
+ } else {
+ p = q = end = buf;
+ }
+ readlen = read(ffd, end, sizeof(buf) - (end - buf));
+ if (readlen == -1)
+ goto err;
+ else
+ len = (size_t)readlen;
+ if (len == 0 && p == buf)
+ break;
+ end += len;
+ len = end - buf;
+ if (len < (ssize_t)sizeof(buf)) {
+ eof = 1;
+ if (len > 0 && buf[len - 1] != '\n')
+ ++len, *end++ = '\n';
+ }
+ continue;
+ }
+
+ /* is it a blank line or a comment? */
+ for (r = p; r < q && isspace(*r); ++r)
+ /* nothing */ ;
+ if (r == q || *r == '#') {
+ /* yep */
+ if (write(tfd, p, q - p + 1) != q - p + 1)
+ goto err;
+ ++q;
+ continue;
+ }
+
+ /* is it the one we're looking for? */
+
+ t = *q;
+ *q = '\0';
+
+ fpw = pw_scan(r, PWSCAN_MASTER);
+
+ /*
+ * fpw is either the struct passwd for the current line,
+ * or NULL if the line is malformed.
+ */
+
+ *q = t;
+ if (fpw == NULL || fpw->pw_uid != spw->pw_uid) {
+ /* nope */
+ if (fpw != NULL)
+ free(fpw);
+ if (write(tfd, p, q - p + 1) != q - p + 1)
+ goto err;
+ ++q;
+ continue;
+ }
+ if (old_pw && !pw_equal(fpw, old_pw)) {
+ warnx("entry inconsistent");
+ free(fpw);
+ errno = EINVAL; /* hack */
+ goto err;
+ }
+ free(fpw);
+
+ /* it is, replace or remove it */
+ if (line != NULL) {
+ len = strlen(line);
+ if (write(tfd, line, len) != (int)len)
+ goto err;
+ } else {
+ /* when removed, avoid the \n */
+ q++;
+ }
+ /* we're done, just copy the rest over */
+ for (;;) {
+ if (write(tfd, q, end - q) != end - q)
+ goto err;
+ q = buf;
+ readlen = read(ffd, buf, sizeof(buf));
+ if (readlen == 0)
+ break;
+ else
+ len = (size_t)readlen;
+ if (readlen == -1)
+ goto err;
+ end = buf + len;
+ }
+ goto done;
+ }
+
+ /* if we got here, we didn't find the old entry */
+ if (line == NULL) {
+ errno = ENOENT;
+ goto err;
+ }
+ len = strlen(line);
+ if ((size_t)write(tfd, line, len) != len ||
+ write(tfd, "\n", 1) != 1)
+ goto err;
+ done:
+ if (line != NULL)
+ free(line);
+ return (0);
+ err:
+ if (line != NULL)
+ free(line);
+ return (-1);
+}
+
+/*
+ * Return the current value of tempname.
+ */
+const char *
+pw_tempname(void)
+{
+
+ return (tempname);
+}
+
+/*
+ * Duplicate a struct passwd.
+ */
+struct passwd *
+pw_dup(const struct passwd *pw)
+{
+ char *dst;
+ struct passwd *npw;
+ ssize_t len;
+
+ len = sizeof(*npw);
+ if (pw->pw_name != NULL)
+ len += strlen(pw->pw_name) + 1;
+ if (pw->pw_passwd != NULL)
+ len += strlen(pw->pw_passwd) + 1;
+ if (pw->pw_class != NULL)
+ len += strlen(pw->pw_class) + 1;
+ if (pw->pw_gecos != NULL)
+ len += strlen(pw->pw_gecos) + 1;
+ if (pw->pw_dir != NULL)
+ len += strlen(pw->pw_dir) + 1;
+ if (pw->pw_shell != NULL)
+ len += strlen(pw->pw_shell) + 1;
+ if ((npw = malloc((size_t)len)) == NULL)
+ return (NULL);
+ memcpy(npw, pw, sizeof(*npw));
+ dst = (char *)npw + sizeof(*npw);
+ if (pw->pw_name != NULL) {
+ npw->pw_name = dst;
+ dst = stpcpy(npw->pw_name, pw->pw_name) + 1;
+ }
+ if (pw->pw_passwd != NULL) {
+ npw->pw_passwd = dst;
+ dst = stpcpy(npw->pw_passwd, pw->pw_passwd) + 1;
+ }
+ if (pw->pw_class != NULL) {
+ npw->pw_class = dst;
+ dst = stpcpy(npw->pw_class, pw->pw_class) + 1;
+ }
+ if (pw->pw_gecos != NULL) {
+ npw->pw_gecos = dst;
+ dst = stpcpy(npw->pw_gecos, pw->pw_gecos) + 1;
+ }
+ if (pw->pw_dir != NULL) {
+ npw->pw_dir = dst;
+ dst = stpcpy(npw->pw_dir, pw->pw_dir) + 1;
+ }
+ if (pw->pw_shell != NULL) {
+ npw->pw_shell = dst;
+ dst = stpcpy(npw->pw_shell, pw->pw_shell) + 1;
+ }
+ return (npw);
+}
+
+#include "pw_scan.h"
+
+/*
+ * Wrapper around an internal libc function
+ */
+struct passwd *
+pw_scan(const char *line, int flags)
+{
+ struct passwd pw, *ret;
+ char *bp;
+
+ if ((bp = strdup(line)) == NULL)
+ return (NULL);
+ if (!__pw_scan(bp, &pw, flags)) {
+ free(bp);
+ return (NULL);
+ }
+ ret = pw_dup(&pw);
+ free(bp);
+ return (ret);
+}
diff --git a/pw/cpdir.c b/pw/cpdir.c
new file mode 100644
index 0000000..c5534e5
--- /dev/null
+++ b/pw/cpdir.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 <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/param.h>
+#include <dirent.h>
+
+#include "pw.h"
+#include "pwupd.h"
+
+void
+copymkdir(char const * dir, char const * skel, mode_t mode, uid_t uid, gid_t gid)
+{
+ char src[MAXPATHLEN];
+ char dst[MAXPATHLEN];
+ char lnk[MAXPATHLEN];
+ int len;
+
+ if (mkdir(dir, mode) != 0 && errno != EEXIST) {
+ warn("mkdir(%s)", dir);
+ } else {
+ int infd, outfd;
+ struct stat st;
+
+ static char counter = 0;
+ static char *copybuf = NULL;
+
+ ++counter;
+ chown(dir, uid, gid);
+ if (skel != NULL && *skel != '\0') {
+ DIR *d = opendir(skel);
+
+ if (d != NULL) {
+ struct dirent *e;
+
+ while ((e = readdir(d)) != NULL) {
+ char *p = e->d_name;
+
+ if (snprintf(src, sizeof(src), "%s/%s", skel, p) >= (int)sizeof(src))
+ warn("warning: pathname too long '%s/%s' (skel not copied)", skel, p);
+ else if (lstat(src, &st) == 0) {
+ if (strncmp(p, "dot.", 4) == 0) /* Conversion */
+ p += 3;
+ if (snprintf(dst, sizeof(dst), "%s/%s", dir, p) >= (int)sizeof(dst))
+ warn("warning: path too long '%s/%s' (skel file skipped)", dir, p);
+ else {
+ if (S_ISDIR(st.st_mode)) { /* Recurse for this */
+ if (strcmp(e->d_name, ".") != 0 && strcmp(e->d_name, "..") != 0)
+ copymkdir(dst, src, st.st_mode & _DEF_DIRMODE, uid, gid);
+ chflags(dst, st.st_flags); /* propagate flags */
+ } else if (S_ISLNK(st.st_mode) && (len = readlink(src, lnk, sizeof(lnk))) != -1) {
+ lnk[len] = '\0';
+ symlink(lnk, dst);
+ lchown(dst, uid, gid);
+ /*
+ * Note: don't propagate special attributes
+ * but do propagate file flags
+ */
+ } else if (S_ISREG(st.st_mode) && (outfd = open(dst, O_RDWR | O_CREAT | O_EXCL, st.st_mode)) != -1) {
+ if ((infd = open(src, O_RDONLY)) == -1) {
+ close(outfd);
+ remove(dst);
+ } else {
+ int b;
+
+ /*
+ * Allocate our copy buffer if we need to
+ */
+ if (copybuf == NULL)
+ copybuf = malloc(4096);
+ while ((b = read(infd, copybuf, 4096)) > 0)
+ write(outfd, copybuf, b);
+ close(infd);
+ /*
+ * Propagate special filesystem flags
+ */
+ fchown(outfd, uid, gid);
+ fchflags(outfd, st.st_flags);
+ close(outfd);
+ chown(dst, uid, gid);
+ }
+ }
+ }
+ }
+ }
+ closedir(d);
+ }
+ }
+ if (--counter == 0 && copybuf != NULL) {
+ free(copybuf);
+ copybuf = NULL;
+ }
+ }
+}
+
diff --git a/pw/pw.8 b/pw/pw.8
new file mode 100644
index 0000000..8b21107
--- /dev/null
+++ b/pw/pw.8
@@ -0,0 +1,1008 @@
+.\" 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 December 21, 2011
+.Dt PW 8
+.Os
+.Sh NAME
+.Nm pw
+.Nd create, remove, modify & display system users and groups
+.Sh SYNOPSIS
+.Nm
+.Op Fl V Ar etcdir
+.Ar useradd
+.Op name|uid
+.Op Fl C Ar config
+.Op Fl q
+.Op Fl n Ar name
+.Op Fl u Ar uid
+.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 V Ar etcdir
+.Ar useradd
+.Op name|uid
+.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 V Ar etcdir
+.Ar userdel
+.Op name|uid
+.Op Fl n Ar name
+.Op Fl u Ar uid
+.Op Fl r
+.Op Fl Y
+.Nm
+.Op Fl V Ar etcdir
+.Ar usermod
+.Op name|uid
+.Op Fl C Ar config
+.Op Fl q
+.Op Fl n Ar name
+.Op Fl u Ar uid
+.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 name
+.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 V Ar etcdir
+.Ar usershow
+.Op name|uid
+.Op Fl n Ar name
+.Op Fl u Ar uid
+.Op Fl F
+.Op Fl P
+.Op Fl 7
+.Op Fl a
+.Nm
+.Op Fl V Ar etcdir
+.Ar usernext
+.Op Fl C Ar config
+.Op Fl q
+.Nm
+.Op Fl V Ar etcdir
+.Ar groupadd
+.Op group|gid
+.Op Fl C Ar config
+.Op Fl q
+.Op Fl n Ar group
+.Op Fl g Ar gid
+.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 V Ar etcdir
+.Ar groupdel
+.Op group|gid
+.Op Fl n Ar name
+.Op Fl g Ar gid
+.Op Fl Y
+.Nm
+.Op Fl V Ar etcdir
+.Ar groupmod
+.Op group|gid
+.Op Fl C Ar config
+.Op Fl q
+.Op Fl n Ar name
+.Op Fl g Ar gid
+.Op Fl l Ar name
+.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 V Ar etcdir
+.Ar groupshow
+.Op group|gid
+.Op Fl n Ar name
+.Op Fl g Ar gid
+.Op Fl F
+.Op Fl P
+.Op Fl a
+.Nm
+.Op Fl V Ar etcdir
+.Ar groupnext
+.Op Fl C Ar config
+.Op Fl q
+.Nm
+.Op Fl V Ar etcdir
+.Ar lock
+.Op name|uid
+.Op Fl C Ar config
+.Op Fl q
+.Nm
+.Op Fl V Ar etcdir
+.Ar unlock
+.Op name|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, you may optionally specify the user or group name or numeric
+id 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 V Ar etcdir
+This flag sets an alternate location for the password, group and configuration files,
+and may 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 (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 may 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 Fl n Ar name
+Specify the user/account name.
+.It Fl u Ar uid
+Specify the user/account numeric id.
+.Pp
+Usually, you only need to provide one or the other of these options, as the account
+name will imply the uid, or vice versa.
+However, there are times when you need to provide both.
+For example, when changing the uid of an existing user with
+.Ar usermod ,
+or overriding the default uid when creating a new account.
+If you wish
+.Nm
+to automatically allocate the uid to a new user with
+.Ar useradd ,
+then you should
+.Em not
+use the
+.Fl u
+option.
+You may also provide either the account or userid 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, you need to quote the comment itself with 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, you will only use this 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.
+Unlike use without
+.Fl D ,
+the argument must be numeric, which specifies the number of days after creation when
+the account is to expire.
+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, and you should 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
+These options 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 (used by
+some system daemons).
+.It Fl w Ar method
+The
+.Fl w
+option sets 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 where you issue
+users with passwords to access their accounts rather than having the user nominate
+their own (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 only three valid 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 name
+This option allows changing of an existing account name to
+.Ql \&name .
+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 Fl n Ar name
+Specify the group name.
+.It Fl g Ar gid
+Specify the group numeric id.
+.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 name
+This option allows changing of an existing group name to
+.Ql \&name .
+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/master.passwd.new
+Temporary copy of the master password file
+.It Pa /etc/passwd.new
+Temporary copy of the Version 7 password file
+.It Pa /etc/group.new
+Temporary copy of the group file
+.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/pw/pw_user.c b/pw/pw_user.c
index db33746..0001a41 100644
--- a/pw/pw_user.c
+++ b/pw/pw_user.c
@@ -1208,7 +1208,7 @@ pw_checkname(u_char *name, int gecos)
if (reject) {
snprintf(showch, sizeof(showch), (*ch >= ' ' && *ch < 127)
? "`%c'" : "0x%02x", *ch);
- errx(EX_DATAERR, "invalid character %s at position %d in %s",
+ errx(EX_DATAERR, "invalid character %s at position %td in %s",
showch, (ch - name), showtype);
}
if (!gecos && (ch - name) > LOGNAMESIZE)