All of lore.kernel.org
 help / color / mirror / Atom feed
* [Buildroot] [pull request v5] Pull request for branch yem-package-create-user
@ 2013-02-05 14:54 Yann E. MORIN
  2013-02-05 14:54 ` [Buildroot] [PATCH 1/2] packages: add ability for packages to create users Yann E. MORIN
  2013-02-05 14:54 ` [Buildroot] [PATCH 2/2] package/tvheadend: use a non-root user to run the daemon Yann E. MORIN
  0 siblings, 2 replies; 27+ messages in thread
From: Yann E. MORIN @ 2013-02-05 14:54 UTC (permalink / raw)
  To: buildroot

Hello All!

Here is a series that allows packages to create users.

Packages that install daemons may well want to run those daemons as
non-root users to avoid security issues. Currently, there are two users
of choice to run 'generic' daemons: root or daemon (although there are
a few dedicated users to run a few services: mail, sshd, ftp...).

This series builds upon both the package infrastrucutre to define the
user(s) a package may want to create, and the filesystem infrastructure
to actually generate these users, and chown their ${HOME}s.

Documentation is updated accordingly.

As a proof of concept, the package tvheadend has been updated to use
a dedicated user to run its daemon as (call me stubborn! ;-] ).


Changes v4 -> v5:
  - rebased ontop master after Developer's Day comments and upstreaming

Changes v3 -> v4:
  - use the configured password encryption scheme
  - some tweaks and typo-fixes to the documentation

Changes v2 -> v3:
  - clarify password prefixes (Samuel)
  - move makeuser syntax doc to its own file (Samuel)
  - use awk instead of sed to parse /etc/passwd et al. (Cam, Thomas)
  - sanitise use of grep (Cam)
  - enhancements and fixes to makuser syntax doc (Cam)

Changes v1 -> v2:
  - drop the gshadow patch (Thomas, Peter)
  - tvheadend user is now part of the video secondary group


The following changes since commit b93bc6ebdcbae89547dc89dbce4701ea8037e02b:

  samba: security bump to version 3.6.12 (2013-02-05 12:23:01 +0100)

are available in the git repository at:
  git://gitorious.org/buildroot/buildroot.git yem-package-create-user

Yann E. MORIN (2):
      packages: add ability for packages to create users
      package/tvheadend: use a non-root user to run the daemon

 docs/manual/adding-packages-generic.txt |   16 ++-
 docs/manual/appendix.txt                |    1 +
 docs/manual/makeusers-syntax.txt        |   87 +++++++
 fs/common.mk                            |    3 +
 package/pkg-generic.mk                  |    1 +
 package/tvheadend/etc.default.tvheadend |    5 +-
 package/tvheadend/tvheadend.mk          |   10 +-
 support/scripts/mkusers                 |  371 +++++++++++++++++++++++++++++++
 8 files changed, 487 insertions(+), 7 deletions(-)
 create mode 100644 docs/manual/makeusers-syntax.txt
 create mode 100755 support/scripts/mkusers

Regards,
Yann E. MORIN

-- 
.-----------------.--------------------.------------------.--------------------.
|  Yann E. MORIN  | Real-Time Embedded | /"\ ASCII RIBBON | Erics' conspiracy: |
| +33 662 376 056 | Software  Designer | \ / CAMPAIGN     |  ___               |
| +33 223 225 172 `------------.-------:  X  AGAINST      |  \e/  There is no  |
| http://ymorin.is-a-geek.org/ | _/*\_ | / \ HTML MAIL    |   v   conspiracy.  |
'------------------------------^-------^------------------^--------------------'

^ permalink raw reply	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-02-05 14:54 [Buildroot] [pull request v5] Pull request for branch yem-package-create-user Yann E. MORIN
@ 2013-02-05 14:54 ` Yann E. MORIN
  2013-02-06  0:12   ` Arnout Vandecappelle
  2013-02-05 14:54 ` [Buildroot] [PATCH 2/2] package/tvheadend: use a non-root user to run the daemon Yann E. MORIN
  1 sibling, 1 reply; 27+ messages in thread
From: Yann E. MORIN @ 2013-02-05 14:54 UTC (permalink / raw)
  To: buildroot

Packages that install daemons may need those daemons to run as a non-root,
or an otherwise non-system (eg. 'daemon'), user.

Add infrastructure for packages to create users, by declaring the FOO_USERS
variable that contain a makedev-syntax-like description of the user(s) to
add.

Signed-off-by: "Yann E. MORIN" <yann.morin.1998@free.fr>
Cc: Samuel Martin <s.martin49@gmail.com>
Cc: Cam Hutchison <camh@xdna.net>
Cc: Thomas Petazzoni <thomas.petazzoni@free-electrons.com>
---
 docs/manual/adding-packages-generic.txt |   16 ++-
 docs/manual/appendix.txt                |    1 +
 docs/manual/makeusers-syntax.txt        |   87 +++++++
 fs/common.mk                            |    3 +
 package/pkg-generic.mk                  |    1 +
 support/scripts/mkusers                 |  371 +++++++++++++++++++++++++++++++
 6 files changed, 477 insertions(+), 2 deletions(-)
 create mode 100644 docs/manual/makeusers-syntax.txt
 create mode 100755 support/scripts/mkusers

diff --git a/docs/manual/adding-packages-generic.txt b/docs/manual/adding-packages-generic.txt
index 41a94d7..aa938b6 100644
--- a/docs/manual/adding-packages-generic.txt
+++ b/docs/manual/adding-packages-generic.txt
@@ -51,7 +51,11 @@ system is based on hand-written Makefiles or shell scripts.
 35:	/bin/foo  f  4755  0  0	 -  -  -  -  -
 36: endef
 37:
-38: $(eval $(generic-package))
+38: define LIBFOO_USERS
+39:	foo -1 libfoo -1 * - - - LibFoo daemon
+40: endef
+41:
+42: $(eval $(generic-package))
 --------------------------------
 
 The Makefile begins on line 6 to 10 with metadata information: the
@@ -134,7 +138,10 @@ On line 29..31, we define a device-node file used by this package
 On line 33..35, we define the permissions to set to specific files
 installed by this package (+LIBFOO_PERMISSIONS+).
 
-Finally, on line 37, we call the +generic-package+ function, which
+On lines 38..40, we define a user that is used by this package (eg.
+to run a daemon as non-root) (+LIBFOO_USERS+).
+
+Finally, on line 42, we call the +generic-package+ function, which
 generates, according to the variables defined previously, all the
 Makefile code necessary to make your package working.
 
@@ -299,6 +306,11 @@ information is (assuming the package name is +libfoo+) :
   You can find some documentation for this syntax in the xref:makedev-syntax[].
   This variable is optional.
 
+* +LIBFOO_USERS+ lists the users to create for this package, if it installs
+  a program you want to run as a specific user (eg. as a daemon, or as a
+  cron-job). The syntax is similar in spirit to the makedevs one, and is
+  described in the xref:makeuser-syntax[]. This variable is optional.
+
 * +LIBFOO_LICENSE+ defines the license (or licenses) under which the package
   is released.
   This name will appear in the manifest file produced by +make legal-info+.
diff --git a/docs/manual/appendix.txt b/docs/manual/appendix.txt
index 6f1e9f3..63b172b 100644
--- a/docs/manual/appendix.txt
+++ b/docs/manual/appendix.txt
@@ -4,6 +4,7 @@ Appendix
 ========
 
 include::makedev-syntax.txt[]
+include::makeusers-syntax.txt[]
 
 [[package-list]]
 Available packages
diff --git a/docs/manual/makeusers-syntax.txt b/docs/manual/makeusers-syntax.txt
new file mode 100644
index 0000000..2199654
--- /dev/null
+++ b/docs/manual/makeusers-syntax.txt
@@ -0,0 +1,87 @@
+// -*- mode:doc -*- ;
+
+[[makeuser-syntax]]
+Makeuser syntax documentation
+-----------------------------
+
+The syntax to create users is inspired by the makedev syntax, above, but
+is specific to Buildroot.
+
+The syntax for adding a user is a space-separated list of fields, one
+user per line; the fields are:
+
+|=================================================================
+|username |uid |group |gid |password |home |shell |groups |comment
+|=================================================================
+
+Where:
+
+- +username+ is the desired user name (aka login name) for the user.
+  It can not be +root+, and must be unique.
+- +uid+ is the desired UID for the user. It must be unique, and not
+  +0+. If set to +-1+, then a unique UID will be computed by Buildroot
+  in the range [1000...1999]
+- +group+ is the desired name for the user's main group. It can not
+  be +root+. If the group does not exist, it will be created.
+- +gid+ is the desired GID for the user's main group. It must be unique,
+  and not +0+. If set to +-1+, and the group does not already exist, then
+  a unique GID will be computed by Buildroot in the range [1000..1999]
+- +password+ is the crypt(3)-encoded password. If prefixed with +!+,
+  then login is disabled. If prefixed with +=+, then it is interpreted
+  as clear-text, and will be crypt-encoded (using MD5). If prefixed with
+  +!=+, then the password will be crypt-encoded (using MD5) and login
+  will be disabled. If set to +*+, then login is not allowed.
+- +home+ is the desired home directory for the user. If set to '-', no
+  home directory will be created, and the user's home will be +/+.
+  Explicitly setting +home+ to +/+ is not allowed.
+- +shell+ is the desired shell for the user. If set to +-+, then
+  +/bin/false+ is set as the user's shell.
+- +groups+ is the comma-separated list of additional groups the user
+  should be part of. If set to +-+, then the user will be a member of
+  no additional group. Missing groups will be created with an arbitrary
+  +gid+.
+- +comment+ (aka https://en.wikipedia.org/wiki/Gecos_field[GECOS]
+  field) is an almost-free-form text.
+
+There are a few restrictions on the content of each field:
+
+* except for +comment+, all fields are mandatory.
+* except for +comment+, fields may not contain spaces.
+* no field may contain a colon (+:+).
+
+If +home+ is not +-+, then the home directory, and all files below,
+will belong to the user and its main group.
+
+Examples:
+
+----
+foo -1 bar -1 !=blabla /home/foo /bin/sh alpha,bravo Foo user
+----
+
+This will create this user:
+
+- +username+ (aka login name) is: +foo+
+- +uid+ is computed by Buildroot
+- main +group+ is: +bar+
+- main group +gid+ is computed by Buildroot
+- clear-text +password+ is: +blabla+, will be crypt(3)-encoded, and login is disabled.
+- +home+ is: +/home/foo+
+- +shell+ is: +/bin/sh+
+- +foo+ is also a member of +groups+: +alpha+ and +bravo+
+- +comment+ is: +Foo user+
+
+----
+test 8000 wheel -1 = - /bin/sh - Test user
+----
+
+This will create this user:
+
+- +username+ (aka login name) is: +test+
+- +uid+ is : +8000+
+- main +group+ is: +wheel+
+- main group +gid+ is computed by Buildroot, and will use the value defined in the rootfs skeleton
+- +password+ is empty (aka no password).
+- +home+ is +/+ but will not belong to +test+
+- +shell+ is: +/bin/sh+
+- +test+ is not a member of any additional +groups+
+- +comment+ is: +Test user+
diff --git a/fs/common.mk b/fs/common.mk
index 8b5b2f2..a8279b1 100644
--- a/fs/common.mk
+++ b/fs/common.mk
@@ -35,6 +35,7 @@ FAKEROOT_SCRIPT = $(BUILD_DIR)/_fakeroot.fs
 FULL_DEVICE_TABLE = $(BUILD_DIR)/_device_table.txt
 ROOTFS_DEVICE_TABLES = $(call qstrip,$(BR2_ROOTFS_DEVICE_TABLE)) \
 	$(call qstrip,$(BR2_ROOTFS_STATIC_DEVICE_TABLE))
+USERS_TABLE = $(BUILD_DIR)/_users_table.txt
 
 define ROOTFS_TARGET_INTERNAL
 
@@ -55,6 +56,8 @@ endif
 	printf '$$(subst $$(sep),\n,$$(PACKAGES_PERMISSIONS_TABLE))' >> $$(FULL_DEVICE_TABLE)
 	echo "$$(HOST_DIR)/usr/bin/makedevs -d $$(FULL_DEVICE_TABLE) $$(TARGET_DIR)" >> $$(FAKEROOT_SCRIPT)
 endif
+	printf '$(subst $(sep),\n,$(PACKAGES_USERS))' > $(USERS_TABLE)
+	$(TOPDIR)/support/scripts/mkusers $(USERS_TABLE) $(TARGET_DIR) >> $(FAKEROOT_SCRIPT)
 	echo "$$(ROOTFS_$(2)_CMD)" >> $$(FAKEROOT_SCRIPT)
 	chmod a+x $$(FAKEROOT_SCRIPT)
 	$$(HOST_DIR)/usr/bin/fakeroot -- $$(FAKEROOT_SCRIPT)
diff --git a/package/pkg-generic.mk b/package/pkg-generic.mk
index 19a115e..e095100 100644
--- a/package/pkg-generic.mk
+++ b/package/pkg-generic.mk
@@ -524,6 +524,7 @@ ifeq ($$($$($(2)_KCONFIG_VAR)),y)
 TARGETS += $(1)
 PACKAGES_PERMISSIONS_TABLE += $$($(2)_PERMISSIONS)$$(sep)
 PACKAGES_DEVICES_TABLE += $$($(2)_DEVICES)$$(sep)
+PACKAGES_USERS += $$($(2)_USERS)$$(sep)
 
 ifeq ($$($(2)_SITE_METHOD),svn)
 DL_TOOLS_DEPENDENCIES += svn
diff --git a/support/scripts/mkusers b/support/scripts/mkusers
new file mode 100755
index 0000000..d98714c
--- /dev/null
+++ b/support/scripts/mkusers
@@ -0,0 +1,371 @@
+#!/bin/bash
+set -e
+myname="${0##*/}"
+
+#----------------------------------------------------------------------------
+# Configurable items
+MIN_UID=1000
+MAX_UID=1999
+MIN_GID=1000
+MAX_GID=1999
+# No more is configurable below this point
+#----------------------------------------------------------------------------
+
+#----------------------------------------------------------------------------
+error() {
+    local fmt="${1}"
+    shift
+
+    printf "%s: " "${myname}" >&2
+    printf "${fmt}" "${@}" >&2
+}
+fail() {
+    error "$@"
+    exit 1
+}
+
+#----------------------------------------------------------------------------
+if [ ${#} -ne 2 ]; then
+    fail "usage: %s USERS_TBLE TARGET_DIR\n"
+fi
+USERS_TABLE="${1}"
+TARGET_DIR="${2}"
+shift 2
+PASSWD="${TARGET_DIR}/etc/passwd"
+SHADOW="${TARGET_DIR}/etc/shadow"
+GROUP="${TARGET_DIR}/etc/group"
+# /etc/gsahdow is not part of the standard skeleton, so not everybody
+# will have it, but some may hav it, and its content must be in sync
+# with /etc/group, so any use of gshadow must be conditional.
+GSHADOW="${TARGET_DIR}/etc/gshadow"
+PASSWD_METHOD="$( sed -r -e '/^BR2_TARGET_GENERIC_PASSWD_METHOD="(.)*"$/!d' \
+                         -e 's//\1/;'                                       \
+                         "${BUILDROOT_CONFIG}"                              \
+                )"
+
+#----------------------------------------------------------------------------
+get_uid() {
+    local username="${1}"
+
+    awk -F: '$1=="'"${username}"'" { printf( "%d\n", $3 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_ugid() {
+    local username="${1}"
+
+    awk -F: '$1=="'"${username}"'" { printf( "%d\n", $4 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_gid() {
+    local group="${1}"
+
+    awk -F: '$1=="'"${group}"'" { printf( "%d\n", $3 ); }' "${GROUP}"
+}
+
+#----------------------------------------------------------------------------
+get_username() {
+    local uid="${1}"
+
+    awk -F: '$3=="'"${uid}"'" { printf( "%s\n", $1 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_group() {
+    local gid="${1}"
+
+    awk -F: '$3=="'"${gid}"'" { printf( "%s\n", $1 ); }' "${GROUP}"
+}
+
+#----------------------------------------------------------------------------
+get_ugroup() {
+    local username="${1}"
+    local ugid
+
+    ugid="$( get_ugid "${username}" )"
+    if [ -n "${ugid}" ]; then
+        get_group "${ugid}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+# Sanity-check the new user/group:
+#   - check the gid is not already used for another group
+#   - check the group does not already exist with another gid
+#   - check the user does not already exist with another gid
+#   - check the uid is not already used for another user
+#   - check the user does not already exist with another uid
+#   - check the user does not already exist in another group
+check_user_validity() {
+    local username="${1}"
+    local uid="${2}"
+    local group="${3}"
+    local gid="${4}"
+    local _uid _ugid _gid _username _group _ugroup
+
+    _group="$( get_group "${gid}" )"
+    _gid="$( get_gid "${group}" )"
+    _ugid="$( get_ugid "${username}" )"
+    _username="$( get_username "${uid}" )"
+    _uid="$( get_uid "${username}" )"
+    _ugroup="$( get_ugroup "${username}" )"
+
+    if [ "${username}" = "root" ]; then
+        fail "invalid username '%s\n'" "${username}"
+    fi
+
+    if [ ${gid} -lt -1 -o ${gid} -eq 0 ]; then
+        fail "invalid gid '%d'\n" ${gid}
+    elif [ ${gid} -ne -1 ]; then
+        # check the gid is not already used for another group
+        if [ -n "${_group}" -a "${_group}" != "${group}" ]; then
+            fail "gid is already used by group '${_group}'\n"
+        fi
+
+        # check the group does not already exists with another gid
+        if [ -n "${_gid}" -a ${_gid} -ne ${gid} ]; then
+            fail "group already exists with gid '${_gid}'\n"
+        fi
+
+        # check the user does not already exists with another gid
+        if [ -n "${_ugid}" -a ${_ugid} -ne ${gid} ]; then
+            fail "user already exists with gid '${_ugid}'\n"
+        fi
+    fi
+
+    if [ ${uid} -lt -1 -o ${uid} -eq 0 ]; then
+        fail "invalid uid '%d'\n" ${uid}
+    elif [ ${uid} -ne -1 ]; then
+        # check the uid is not already used for another user
+        if [ -n "${_username}" -a "${_username}" != "${username}" ]; then
+            fail "uid is already used by user '${_username}'\n"
+        fi
+
+        # check the user does not already exists with another uid
+        if [ -n "${_uid}" -a ${_uid} -ne ${uid} ]; then
+            fail "user already exists with uid '${_uid}'\n"
+        fi
+    fi
+
+    # check the user does not already exist in another group
+    if [ -n "${_ugroup}" -a "${_ugroup}" != "${group}" ]; then
+        fail "user already exists with group '${_ugroup}'\n"
+    fi
+
+    return 0
+}
+
+#----------------------------------------------------------------------------
+# Generate a unique GID for given group. If the group already exists,
+# then simply report its current GID. Otherwise, generate the lowest GID
+# that is:
+#   - not 0
+#   - comprised in [MIN_GID..MAX_GID]
+#   - not already used by a group
+generate_gid() {
+    local group="${1}"
+    local gid
+
+    gid="$( get_gid "${group}" )"
+    if [ -z "${gid}" ]; then
+        for(( gid=MIN_GID; gid<=MAX_GID; gid++ )); do
+            if [ -z "$( get_group "${gid}" )" ]; then
+                break
+            fi
+        done
+        if [ ${gid} -gt ${MAX_GID} ]; then
+            fail "can not allocate a GID for group '%s'\n" "${group}"
+        fi
+    fi
+    printf "%d\n" "${gid}"
+}
+
+#----------------------------------------------------------------------------
+# Add a group; if it does already exist, remove it first
+add_one_group() {
+    local group="${1}"
+    local gid="${2}"
+    local _f
+
+    # Generate a new GID if needed
+    if [ ${gid} -eq -1 ]; then
+        gid="$( generate_gid "${group}" )"
+    fi
+
+    # Remove any previous instance of this group
+    for _f in "${GROUP}" "${GSHADOW}"; do
+        [ -f "${_f}" ] || continue
+        sed -r -i -e '/^'"${group}"':.*/d;' "${_f}"
+    done
+
+    printf "%s:x:%d:\n" "${group}" "${gid}" >>"${GROUP}"
+    if [ -f "${GSHADOW}" ]; then
+        printf "%s:*::\n" "${group}" >>"${GSHADOW}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+# Generate a unique UID for given username. If the username already exists,
+# then simply report its current UID. Otherwise, generate the lowest UID
+# that is:
+#   - not 0
+#   - comprised in [MIN_UID..MAX_UID]
+#   - not already used by a user
+generate_uid() {
+    local username="${1}"
+    local uid
+
+    uid="$( get_uid "${username}" )"
+    if [ -z "${uid}" ]; then
+        for(( uid=MIN_UID; uid<=MAX_UID; uid++ )); do
+            if [ -z "$( get_username "${uid}" )" ]; then
+                break
+            fi
+        done
+        if [ ${uid} -gt ${MAX_UID} ]; then
+            fail "can not allocate a UID for user '%s'\n" "${username}"
+        fi
+    fi
+    printf "%d\n" "${uid}"
+}
+
+#----------------------------------------------------------------------------
+# Add given user to given group, if not already the case
+add_user_to_group() {
+    local username="${1}"
+    local group="${2}"
+    local _f
+
+    for _f in "${GROUP}" "${GSHADOW}"; do
+        [ -f "${_f}" ] || continue
+        sed -r -i -e 's/^('"${group}"':.*:)(([^:]+,)?)'"${username}"'(,[^:]+*)?$/\1\2\4/;'  \
+                  -e 's/^('"${group}"':.*)$/\1,'"${username}"'/;'                           \
+                  -e 's/,+/,/'                                                              \
+                  -e 's/:,/:/'                                                              \
+                  "${_f}"
+    done
+}
+
+#----------------------------------------------------------------------------
+# Encode a password
+encode_password() {
+    local passwd="${1}"
+
+    mkpasswd -m "${BR2_TARGET_GENERIC_PASSWD_METHOD}" "${passwd}"
+}
+
+#----------------------------------------------------------------------------
+# Add a user; if it does already exist, remove it first
+add_one_user() {
+    local username="${1}"
+    local uid="${2}"
+    local group="${3}"
+    local gid="${4}"
+    local passwd="${5}"
+    local home="${6}"
+    local shell="${7}"
+    local groups="${8}"
+    local comment="${9}"
+    local nb_days="$((($(date +%s)+(24*60*60-1))/(24*60*60)))"
+    local _f _group _home _shell _gid _passwd
+
+    # First, sanity-check the user
+    check_user_validity "${username}" "${uid}" "${group}" "${gid}"
+
+    # Generate a new UID if needed
+    if [ ${uid} -eq -1 ]; then
+        uid="$( generate_uid "${username}" )"
+    fi
+
+    # Remove any previous instance of this user
+    for _f in "${PASSWD}" "${SHADOW}"; do
+        sed -r -i -e '/^'"${username}"':.*/d;' "${_f}"
+    done
+
+    _gid="$( get_gid "${group}" )"
+    _shell="${shell}"
+    if [ "${shell}" = "-" ]; then
+        _shell="/bin/false"
+    fi
+    case "${home}" in
+        -)  _home="/";;
+        /)  fail "home can not explicitly be '/'\n";;
+        /*) _home="${home}";;
+        *)  fail "home must be an absolute path\n";;
+    esac
+    case "${passwd}" in
+        !=*)
+            _passwd='!'"$( encode_passwd "${passwd#!=}" )"
+            ;;
+        =*)
+            _passwd="$( encode_passwd "${passwd#=}" )"
+            ;;
+        *)
+            _passwd="${passwd}"
+            ;;
+    esac
+
+    printf "%s:x:%d:%d:%s:%s:%s\n"              \
+           "${username}" "${uid}" "${_gid}"     \
+           "${comment}" "${_home}" "${_shell}"  \
+           >>"${PASSWD}"
+    printf "%s:%s:%d:0:99999:7:::\n"                \
+           "${username}" "${_passwd}" "${nb_days}"  \
+           >>"${SHADOW}"
+
+    # Add the user to its additional groups
+    if [ "${groups}" != "-" ]; then
+        for _group in ${groups//,/ }; do
+            add_user_to_group "${username}" "${_group}"
+        done
+    fi
+
+    # If the user has a home, chown it
+    # (Note: stdout goes to the fakeroot-script)
+    if [ "${home}" != "-" ]; then
+        mkdir -p "${TARGET_DIR}/${home}"
+        printf "chown -R %d:%d '%s'\n" "${uid}" "${_gid}" "${TARGET_DIR}/${home}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+main() {
+    local username uid group gid passwd home shell groups comment
+
+    # Some sanity checks
+    if [ ${MIN_UID} -le 0 ]; then
+        fail "MIN_UID must be >0 (currently %d)\n" ${MIN_UID}
+    fi
+    if [ ${MIN_GID} -le 0 ]; then
+        fail "MIN_GID must be >0 (currently %d)\n" ${MIN_GID}
+    fi
+
+    # First, create all the main groups
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        add_one_group "${group}" "${gid}"
+    done <"${USERS_TABLE}"
+
+    # Then, create all the additional groups
+    # If any additional group is already a main group, we should use
+    # the gid of that main group; otherwise, we can use any gid
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        if [ "${groups}" != "-" ]; then
+            for g in ${groups//,/ }; do
+                add_one_group "${g}" -1
+            done
+        fi
+    done <"${USERS_TABLE}"
+
+    # Finally, add users
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
+                     "${home}" "${shell}" "${groups}" "${comment}"
+    done <"${USERS_TABLE}"
+}
+
+#----------------------------------------------------------------------------
+main "${@}"
-- 
1.7.2.5

^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 2/2] package/tvheadend: use a non-root user to run the daemon
  2013-02-05 14:54 [Buildroot] [pull request v5] Pull request for branch yem-package-create-user Yann E. MORIN
  2013-02-05 14:54 ` [Buildroot] [PATCH 1/2] packages: add ability for packages to create users Yann E. MORIN
@ 2013-02-05 14:54 ` Yann E. MORIN
  1 sibling, 0 replies; 27+ messages in thread
From: Yann E. MORIN @ 2013-02-05 14:54 UTC (permalink / raw)
  To: buildroot

Signed-off-by: "Yann E. MORIN" <yann.morin.1998@free.fr>
---
 package/tvheadend/etc.default.tvheadend |    5 ++---
 package/tvheadend/tvheadend.mk          |   10 ++++++++--
 2 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/package/tvheadend/etc.default.tvheadend b/package/tvheadend/etc.default.tvheadend
index c769055..253f832 100644
--- a/package/tvheadend/etc.default.tvheadend
+++ b/package/tvheadend/etc.default.tvheadend
@@ -1,6 +1,5 @@
-# Once we have a real user, we'll use it
-TVH_USER=root
-TVH_GROUP=root
+TVH_USER=tvheadend
+TVH_GROUP=tvheadend
 #TVH_ADAPTERS=
 #TVH_HTTP_PORT=9981
 #TVH_HTSP_PORT=9982
diff --git a/package/tvheadend/tvheadend.mk b/package/tvheadend/tvheadend.mk
index 5100781..4a3f882 100644
--- a/package/tvheadend/tvheadend.mk
+++ b/package/tvheadend/tvheadend.mk
@@ -26,9 +26,11 @@ TVHEADEND_DEPENDENCIES     += dvb-apps
 # To run tvheadend, we need:
 #  - a startup script, and its config file
 #  - a default DB with a tvheadend admin
+#  - a non-root user to run as
 define TVHEADEND_INSTALL_DB
-	$(INSTALL) -D package/tvheadend/accesscontrol.1     \
-	              $(TARGET_DIR)/root/.hts/tvheadend/accesscontrol/1
+	$(INSTALL) -D -m 0600 package/tvheadend/accesscontrol.1     \
+	              $(TARGET_DIR)/home/tvheadend/.hts/tvheadend/accesscontrol/1
+	chmod -R go-rwx $(TARGET_DIR)/home/tvheadend
 endef
 TVHEADEND_POST_INSTALL_TARGET_HOOKS  = TVHEADEND_INSTALL_DB
 
@@ -37,6 +39,10 @@ define TVHEADEND_INSTALL_INIT_SYSV
 	$(INSTALL) -D package/tvheadend/S99tvheadend          $(TARGET_DIR)/etc/init.d/S99tvheadend
 endef
 
+define TVHEADEND_USERS
+tvheadend -1 tvheadend -1 * /home/tvheadend - video TVHeadend daemon
+endef
+
 #----------------------------------------------------------------------------
 # tvheadend is not an autotools-based package, but it is possible to
 # call its ./configure script as if it were an autotools one.
-- 
1.7.2.5

^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-02-05 14:54 ` [Buildroot] [PATCH 1/2] packages: add ability for packages to create users Yann E. MORIN
@ 2013-02-06  0:12   ` Arnout Vandecappelle
  2013-02-06 22:59     ` Yann E. MORIN
                       ` (2 more replies)
  0 siblings, 3 replies; 27+ messages in thread
From: Arnout Vandecappelle @ 2013-02-06  0:12 UTC (permalink / raw)
  To: buildroot

  Although I have a shitload of comments below, there's nothing critical so:
Acked-by: Arnout Vandecappelle (Essensium/Mind) <arnout@mind.be>

On 05/02/13 15:54, Yann E. MORIN wrote:
[snip]
> diff --git a/support/scripts/mkusers b/support/scripts/mkusers
> new file mode 100755
> index 0000000..d98714c
> --- /dev/null
> +++ b/support/scripts/mkusers
> @@ -0,0 +1,371 @@
> +#!/bin/bash

  Although we already do depend on bash, I would prefer not to add more 
dependencies. The only bashism I could find is the 'local' declaration, 
which unfortunately is really required where it is used in this script. 
So never mind that.

> +set -e
> +myname="${0##*/}"
> +
> +#----------------------------------------------------------------------------
> +# Configurable items
> +MIN_UID=1000
> +MAX_UID=1999
> +MIN_GID=1000
> +MAX_GID=1999
> +# No more is configurable below this point
> +#----------------------------------------------------------------------------
> +
> +#----------------------------------------------------------------------------
> +error() {
> +    local fmt="${1}"
> +    shift
> +
> +    printf "%s: " "${myname}" >&2
> +    printf "${fmt}" "${@}" >&2
> +}
> +fail() {
> +    error "$@"
> +    exit 1
> +}
> +
> +#----------------------------------------------------------------------------
> +if [ ${#} -ne 2 ]; then
> +    fail "usage: %s USERS_TBLE TARGET_DIR\n"

  USERS_TABLE

> +fi
> +USERS_TABLE="${1}"
> +TARGET_DIR="${2}"
> +shift 2
> +PASSWD="${TARGET_DIR}/etc/passwd"
> +SHADOW="${TARGET_DIR}/etc/shadow"
> +GROUP="${TARGET_DIR}/etc/group"
> +# /etc/gsahdow is not part of the standard skeleton, so not everybody

  gshadow

> +# will have it, but some may hav it, and its content must be in sync
> +# with /etc/group, so any use of gshadow must be conditional.
> +GSHADOW="${TARGET_DIR}/etc/gshadow"
> +PASSWD_METHOD="$( sed -r -e '/^BR2_TARGET_GENERIC_PASSWD_METHOD="(.)*"$/!d' \
> +                         -e 's//\1/;'                                       \
> +                         "${BUILDROOT_CONFIG}"                              \
> +                )"

  I'm personally more in favour of sourcing ${BUILDROOT_CONFIG} and using 
"${BR2_TARGET_GENERIC_PASSWD_METHOD}" directly.

> +
> +#----------------------------------------------------------------------------
> +get_uid() {
> +    local username="${1}"
> +
> +    awk -F: '$1=="'"${username}"'" { printf( "%d\n", $3 ); }' "${PASSWD}"

  I personally find it easier to read an awk script that is written like 
this:

	awk -F: -v username="${1}" \
		'$1 == username { printf( "%d\n", $3 ); }'

(i.e. pass variables as awk variables rather than expanding them in the 
awk script).

  But obviously that's just personal opinion.

> +}
> +
> +#----------------------------------------------------------------------------
> +get_ugid() {
> +    local username="${1}"
> +
> +    awk -F: '$1=="'"${username}"'" { printf( "%d\n", $4 ); }' "${PASSWD}"
> +}
> +
> +#----------------------------------------------------------------------------
> +get_gid() {
> +    local group="${1}"
> +
> +    awk -F: '$1=="'"${group}"'" { printf( "%d\n", $3 ); }' "${GROUP}"
> +}

  I would define a single get_id for passwd and group, where the file is 
passed as a parameter. That allows to factor the generate_uid/gid, below, 
as well.

> +
> +#----------------------------------------------------------------------------
> +get_username() {
> +    local uid="${1}"
> +
> +    awk -F: '$3=="'"${uid}"'" { printf( "%s\n", $1 ); }' "${PASSWD}"
> +}
> +
> +#----------------------------------------------------------------------------
> +get_group() {
> +    local gid="${1}"
> +
> +    awk -F: '$3=="'"${gid}"'" { printf( "%s\n", $1 ); }' "${GROUP}"
> +}
> +
> +#----------------------------------------------------------------------------
> +get_ugroup() {
> +    local username="${1}"
> +    local ugid
> +
> +    ugid="$( get_ugid "${username}" )"
> +    if [ -n "${ugid}" ]; then
> +        get_group "${ugid}"
> +    fi
> +}
> +
> +#----------------------------------------------------------------------------
> +# Sanity-check the new user/group:
> +#   - check the gid is not already used for another group
> +#   - check the group does not already exist with another gid
> +#   - check the user does not already exist with another gid
> +#   - check the uid is not already used for another user
> +#   - check the user does not already exist with another uid
> +#   - check the user does not already exist in another group
> +check_user_validity() {
> +    local username="${1}"
> +    local uid="${2}"
> +    local group="${3}"
> +    local gid="${4}"
> +    local _uid _ugid _gid _username _group _ugroup
> +
> +    _group="$( get_group "${gid}" )"
> +    _gid="$( get_gid "${group}" )"
> +    _ugid="$( get_ugid "${username}" )"
> +    _username="$( get_username "${uid}" )"
> +    _uid="$( get_uid "${username}" )"
> +    _ugroup="$( get_ugroup "${username}" )"
> +
> +    if [ "${username}" = "root" ]; then
> +        fail "invalid username '%s\n'" "${username}"
> +    fi
> +
> +    if [ ${gid} -lt -1 -o ${gid} -eq 0 ]; then

  If ${gid} is not an integer, this will report:
[: foo: integer expression expected
in addition to the error below. Unfortunately there is no way to check 
for integer, but you could redirect stderr to /dev/null.

> +        fail "invalid gid '%d'\n" ${gid}
> +    elif [ ${gid} -ne -1 ]; then
> +        # check the gid is not already used for another group
> +        if [ -n "${_group}" -a "${_group}" != "${group}" ]; then
> +            fail "gid is already used by group '${_group}'\n"
> +        fi
> +
> +        # check the group does not already exists with another gid
> +        if [ -n "${_gid}" -a ${_gid} -ne ${gid} ]; then
> +            fail "group already exists with gid '${_gid}'\n"
> +        fi
> +
> +        # check the user does not already exists with another gid
> +        if [ -n "${_ugid}" -a ${_ugid} -ne ${gid} ]; then
> +            fail "user already exists with gid '${_ugid}'\n"
> +        fi
> +    fi
> +
> +    if [ ${uid} -lt -1 -o ${uid} -eq 0 ]; then
> +        fail "invalid uid '%d'\n" ${uid}
> +    elif [ ${uid} -ne -1 ]; then
> +        # check the uid is not already used for another user
> +        if [ -n "${_username}" -a "${_username}" != "${username}" ]; then
> +            fail "uid is already used by user '${_username}'\n"
> +        fi
> +
> +        # check the user does not already exists with another uid
> +        if [ -n "${_uid}" -a ${_uid} -ne ${uid} ]; then
> +            fail "user already exists with uid '${_uid}'\n"
> +        fi
> +    fi

  If the user already exists, I would check that _all_ fields are exactly 
the same. Otherwise, it depends on the order of evaluation of the .mk 
files what will end up in the passwd/group file.

> +
> +    # check the user does not already exist in another group
> +    if [ -n "${_ugroup}" -a "${_ugroup}" != "${group}" ]; then
> +        fail "user already exists with group '${_ugroup}'\n"
> +    fi
> +
> +    return 0
> +}
> +
> +#----------------------------------------------------------------------------
> +# Generate a unique GID for given group. If the group already exists,
> +# then simply report its current GID. Otherwise, generate the lowest GID
> +# that is:
> +#   - not 0
> +#   - comprised in [MIN_GID..MAX_GID]
> +#   - not already used by a group
> +generate_gid() {

  So you could factor generate_gid/uid into a single generate_id which 
you call like:

generate_id ${group} ${GROUP} ${MIN_GID} ${MAX_GID}

> +    local group="${1}"
> +    local gid
> +
> +    gid="$( get_gid "${group}" )"
> +    if [ -z "${gid}" ]; then
> +        for(( gid=MIN_GID; gid<=MAX_GID; gid++ )); do
> +            if [ -z "$( get_group "${gid}" )" ]; then
> +                break
> +            fi
> +        done
> +        if [ ${gid} -gt ${MAX_GID} ]; then
> +            fail "can not allocate a GID for group '%s'\n" "${group}"
> +        fi
> +    fi
> +    printf "%d\n" "${gid}"
> +}
> +
> +#----------------------------------------------------------------------------
> +# Add a group; if it does already exist, remove it first
> +add_one_group() {
> +    local group="${1}"
> +    local gid="${2}"
> +    local _f
> +
> +    # Generate a new GID if needed
> +    if [ ${gid} -eq -1 ]; then
> +        gid="$( generate_gid "${group}" )"
> +    fi
> +
> +    # Remove any previous instance of this group
> +    for _f in "${GROUP}" "${GSHADOW}"; do

  Since ${GROUP} always exists, I think having a loop here is harder to 
read that just repeating the sed call.

> +        [ -f "${_f}" ] || continue
> +        sed -r -i -e '/^'"${group}"':.*/d;' "${_f}"

  -r is redundant (it's a plain regex).

  Quoting could be simpler:

	sed -i -e "/^${group}:.*/d;" "${GROUP}"

> +    done
> +
> +    printf "%s:x:%d:\n" "${group}" "${gid}" >>"${GROUP}"
> +    if [ -f "${GSHADOW}" ]; then

  Instead of having the condition twice, just put the sed statement for 
GSHADOW here.

> +        printf "%s:*::\n" "${group}" >>"${GSHADOW}"
> +    fi
> +}
> +
> +#----------------------------------------------------------------------------
> +# Generate a unique UID for given username. If the username already exists,
> +# then simply report its current UID. Otherwise, generate the lowest UID
> +# that is:
> +#   - not 0
> +#   - comprised in [MIN_UID..MAX_UID]
> +#   - not already used by a user
> +generate_uid() {
> +    local username="${1}"
> +    local uid
> +
> +    uid="$( get_uid "${username}" )"
> +    if [ -z "${uid}" ]; then
> +        for(( uid=MIN_UID; uid<=MAX_UID; uid++ )); do
> +            if [ -z "$( get_username "${uid}" )" ]; then
> +                break
> +            fi
> +        done
> +        if [ ${uid} -gt ${MAX_UID} ]; then
> +            fail "can not allocate a UID for user '%s'\n" "${username}"
> +        fi
> +    fi
> +    printf "%d\n" "${uid}"
> +}
> +
> +#----------------------------------------------------------------------------
> +# Add given user to given group, if not already the case
> +add_user_to_group() {
> +    local username="${1}"
> +    local group="${2}"
> +    local _f
> +
> +    for _f in "${GROUP}" "${GSHADOW}"; do
> +        [ -f "${_f}" ] || continue
> +        sed -r -i -e 's/^('"${group}"':.*:)(([^:]+,)?)'"${username}"'(,[^:]+*)?$/\1\2\4/;'  \
> +                  -e 's/^('"${group}"':.*)$/\1,'"${username}"'/;'                           \
> +                  -e 's/,+/,/'                                                              \
> +                  -e 's/:,/:/'                                                              \
> +                  "${_f}"
> +    done
> +}
> +
> +#----------------------------------------------------------------------------
> +# Encode a password
> +encode_password() {
> +    local passwd="${1}"
> +
> +    mkpasswd -m "${BR2_TARGET_GENERIC_PASSWD_METHOD}" "${passwd}"

  Err, you defined it as PASSWD_METHOD above...  But if you source 
.config as I suggested, this is OK :-)

> +}
> +
> +#----------------------------------------------------------------------------
> +# Add a user; if it does already exist, remove it first
> +add_one_user() {
> +    local username="${1}"
> +    local uid="${2}"
> +    local group="${3}"
> +    local gid="${4}"
> +    local passwd="${5}"
> +    local home="${6}"
> +    local shell="${7}"
> +    local groups="${8}"
> +    local comment="${9}"
> +    local nb_days="$((($(date +%s)+(24*60*60-1))/(24*60*60)))"

  Hm, I don't like this. If I re-run the same configuration after a year, 
it gives me a different shadow.

  I think the nb_days field in shadow should be left empty. The one in 
the skeleton is arbitrarily set to Dec. 8 1999, it has been like that 
since the first commit in 2001, and it has completely no meaning.

> +    local _f _group _home _shell _gid _passwd
> +
> +    # First, sanity-check the user
> +    check_user_validity "${username}" "${uid}" "${group}" "${gid}"
> +
> +    # Generate a new UID if needed
> +    if [ ${uid} -eq -1 ]; then
> +        uid="$( generate_uid "${username}" )"
> +    fi
> +
> +    # Remove any previous instance of this user
> +    for _f in "${PASSWD}" "${SHADOW}"; do
> +        sed -r -i -e '/^'"${username}"':.*/d;' "${_f}"
> +    done
> +
> +    _gid="$( get_gid "${group}" )"
> +    _shell="${shell}"
> +    if [ "${shell}" = "-" ]; then
> +        _shell="/bin/false"
> +    fi
> +    case "${home}" in
> +        -)  _home="/";;
> +        /)  fail "home can not explicitly be '/'\n";;
> +        /*) _home="${home}";;
> +        *)  fail "home must be an absolute path\n";;
> +    esac
> +    case "${passwd}" in
> +        !=*)
> +            _passwd='!'"$( encode_passwd "${passwd#!=}" )"
> +            ;;
> +        =*)
> +            _passwd="$( encode_passwd "${passwd#=}" )"
> +            ;;
> +        *)
> +            _passwd="${passwd}"
> +            ;;
> +    esac
> +
> +    printf "%s:x:%d:%d:%s:%s:%s\n"              \
> +           "${username}" "${uid}" "${_gid}"     \
> +           "${comment}" "${_home}" "${_shell}"  \
> +           >>"${PASSWD}"
> +    printf "%s:%s:%d:0:99999:7:::\n"                \
> +           "${username}" "${_passwd}" "${nb_days}"  \
> +           >>"${SHADOW}"
> +
> +    # Add the user to its additional groups
> +    if [ "${groups}" != "-" ]; then
> +        for _group in ${groups//,/ }; do

  Oh, here's another bashism. But also one which is much more difficult 
to write in Posix sh.

> +            add_user_to_group "${username}" "${_group}"
> +        done
> +    fi
> +
> +    # If the user has a home, chown it
> +    # (Note: stdout goes to the fakeroot-script)
> +    if [ "${home}" != "-" ]; then
> +        mkdir -p "${TARGET_DIR}/${home}"
> +        printf "chown -R %d:%d '%s'\n" "${uid}" "${_gid}" "${TARGET_DIR}/${home}"
> +    fi
> +}
> +
> +#----------------------------------------------------------------------------
> +main() {
> +    local username uid group gid passwd home shell groups comment
> +
> +    # Some sanity checks
> +    if [ ${MIN_UID} -le 0 ]; then
> +        fail "MIN_UID must be >0 (currently %d)\n" ${MIN_UID}
> +    fi
> +    if [ ${MIN_GID} -le 0 ]; then
> +        fail "MIN_GID must be >0 (currently %d)\n" ${MIN_GID}
> +    fi
> +
> +    # First, create all the main groups
> +    while read username uid group gid passwd home shell groups comment; do
> +        [ -n "${username}" ] || continue    # Package with no user
> +        add_one_group "${group}" "${gid}"

  In the first pass, I would only add groups where gid != -1. Then, if 
there is a line which sets gid and another one which doesn't, it still 
works.  Of course that means you have to repeat the add_one_group for gid 
== -1 in the second pass.

> +    done <"${USERS_TABLE}"
> +
> +    # Then, create all the additional groups
> +    # If any additional group is already a main group, we should use
> +    # the gid of that main group; otherwise, we can use any gid
> +    while read username uid group gid passwd home shell groups comment; do
> +        [ -n "${username}" ] || continue    # Package with no user
> +        if [ "${groups}" != "-" ]; then
> +            for g in ${groups//,/ }; do
> +                add_one_group "${g}" -1
> +            done
> +        fi
> +    done <"${USERS_TABLE}"
> +
> +    # Finally, add users
> +    while read username uid group gid passwd home shell groups comment; do
> +        [ -n "${username}" ] || continue    # Package with no user
> +        add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
> +                     "${home}" "${shell}" "${groups}" "${comment}"

  If you reverse-sort the USERS_TABLE in the makefile before the script 
is called, then you also make it possible for one package setting uid to 
-1 and another giving a specific uid.

  Regards,
  Arnout

> +    done <"${USERS_TABLE}"
> +}
> +
> +#----------------------------------------------------------------------------
> +main "${@}"
>


-- 
Arnout Vandecappelle                          arnout at mind be
Senior Embedded Software Architect            +32-16-286500
Essensium/Mind                                http://www.mind.be
G.Geenslaan 9, 3001 Leuven, Belgium           BE 872 984 063 RPR Leuven
LinkedIn profile: http://www.linkedin.com/in/arnoutvandecappelle
GPG fingerprint:  7CB5 E4CC 6C2E EFD4 6E3D A754 F963 ECAB 2450 2F1F

^ permalink raw reply	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-02-06  0:12   ` Arnout Vandecappelle
@ 2013-02-06 22:59     ` Yann E. MORIN
  2013-02-06 23:20     ` Yann E. MORIN
  2013-02-08 22:02     ` Yann E. MORIN
  2 siblings, 0 replies; 27+ messages in thread
From: Yann E. MORIN @ 2013-02-06 22:59 UTC (permalink / raw)
  To: buildroot

On Wednesday 06 February 2013 Arnout Vandecappelle wrote:
>   Although I have a shitload of comments below, there's nothing critical so:
> Acked-by: Arnout Vandecappelle (Essensium/Mind) <arnout@mind.be>
> 
> On 05/02/13 15:54, Yann E. MORIN wrote:
> [snip]
> > diff --git a/support/scripts/mkusers b/support/scripts/mkusers
> > new file mode 100755
> > index 0000000..d98714c
> > --- /dev/null
> > +++ b/support/scripts/mkusers
> > @@ -0,0 +1,371 @@
> > +#!/bin/bash
> 
>   Although we already do depend on bash, I would prefer not to add more 
> dependencies. The only bashism I could find is the 'local' declaration, 
> which unfortunately is really required where it is used in this script. 
> So never mind that.
> 
> > +set -e
> > +myname="${0##*/}"
> > +
> > +#----------------------------------------------------------------------------
> > +# Configurable items
> > +MIN_UID=1000
> > +MAX_UID=1999
> > +MIN_GID=1000
> > +MAX_GID=1999
> > +# No more is configurable below this point
> > +#----------------------------------------------------------------------------
> > +
> > +#----------------------------------------------------------------------------
> > +error() {
> > +    local fmt="${1}"
> > +    shift
> > +
> > +    printf "%s: " "${myname}" >&2
> > +    printf "${fmt}" "${@}" >&2
> > +}
> > +fail() {
> > +    error "$@"
> > +    exit 1
> > +}
> > +
> > +#----------------------------------------------------------------------------
> > +if [ ${#} -ne 2 ]; then
> > +    fail "usage: %s USERS_TBLE TARGET_DIR\n"
> 
>   USERS_TABLE
> 
> > +fi
> > +USERS_TABLE="${1}"
> > +TARGET_DIR="${2}"
> > +shift 2
> > +PASSWD="${TARGET_DIR}/etc/passwd"
> > +SHADOW="${TARGET_DIR}/etc/shadow"
> > +GROUP="${TARGET_DIR}/etc/group"
> > +# /etc/gsahdow is not part of the standard skeleton, so not everybody
> 
>   gshadow
> 
> > +# will have it, but some may hav it, and its content must be in sync
> > +# with /etc/group, so any use of gshadow must be conditional.
> > +GSHADOW="${TARGET_DIR}/etc/gshadow"
> > +PASSWD_METHOD="$( sed -r -e '/^BR2_TARGET_GENERIC_PASSWD_METHOD="(.)*"$/!d' \
> > +                         -e 's//\1/;'                                       \
> > +                         "${BUILDROOT_CONFIG}"                              \
> > +                )"
> 
>   I'm personally more in favour of sourcing ${BUILDROOT_CONFIG} and using 
> "${BR2_TARGET_GENERIC_PASSWD_METHOD}" directly.
> 
> > +
> > +#----------------------------------------------------------------------------
> > +get_uid() {
> > +    local username="${1}"
> > +
> > +    awk -F: '$1=="'"${username}"'" { printf( "%d\n", $3 ); }' "${PASSWD}"
> 
>   I personally find it easier to read an awk script that is written like 
> this:
> 
> 	awk -F: -v username="${1}" \
> 		'$1 == username { printf( "%d\n", $3 ); }'
> 
> (i.e. pass variables as awk variables rather than expanding them in the 
> awk script).
> 
>   But obviously that's just personal opinion.
> 
> > +}
> > +
> > +#----------------------------------------------------------------------------
> > +get_ugid() {
> > +    local username="${1}"
> > +
> > +    awk -F: '$1=="'"${username}"'" { printf( "%d\n", $4 ); }' "${PASSWD}"
> > +}
> > +
> > +#----------------------------------------------------------------------------
> > +get_gid() {
> > +    local group="${1}"
> > +
> > +    awk -F: '$1=="'"${group}"'" { printf( "%d\n", $3 ); }' "${GROUP}"
> > +}
> 
>   I would define a single get_id for passwd and group, where the file is 
> passed as a parameter. That allows to factor the generate_uid/gid, below, 
> as well.
> 
> > +
> > +#----------------------------------------------------------------------------
> > +get_username() {
> > +    local uid="${1}"
> > +
> > +    awk -F: '$3=="'"${uid}"'" { printf( "%s\n", $1 ); }' "${PASSWD}"
> > +}
> > +
> > +#----------------------------------------------------------------------------
> > +get_group() {
> > +    local gid="${1}"
> > +
> > +    awk -F: '$3=="'"${gid}"'" { printf( "%s\n", $1 ); }' "${GROUP}"
> > +}
> > +
> > +#----------------------------------------------------------------------------
> > +get_ugroup() {
> > +    local username="${1}"
> > +    local ugid
> > +
> > +    ugid="$( get_ugid "${username}" )"
> > +    if [ -n "${ugid}" ]; then
> > +        get_group "${ugid}"
> > +    fi
> > +}
> > +
> > +#----------------------------------------------------------------------------
> > +# Sanity-check the new user/group:
> > +#   - check the gid is not already used for another group
> > +#   - check the group does not already exist with another gid
> > +#   - check the user does not already exist with another gid
> > +#   - check the uid is not already used for another user
> > +#   - check the user does not already exist with another uid
> > +#   - check the user does not already exist in another group
> > +check_user_validity() {
> > +    local username="${1}"
> > +    local uid="${2}"
> > +    local group="${3}"
> > +    local gid="${4}"
> > +    local _uid _ugid _gid _username _group _ugroup
> > +
> > +    _group="$( get_group "${gid}" )"
> > +    _gid="$( get_gid "${group}" )"
> > +    _ugid="$( get_ugid "${username}" )"
> > +    _username="$( get_username "${uid}" )"
> > +    _uid="$( get_uid "${username}" )"
> > +    _ugroup="$( get_ugroup "${username}" )"
> > +
> > +    if [ "${username}" = "root" ]; then
> > +        fail "invalid username '%s\n'" "${username}"
> > +    fi
> > +
> > +    if [ ${gid} -lt -1 -o ${gid} -eq 0 ]; then
> 
>   If ${gid} is not an integer, this will report:
> [: foo: integer expression expected
> in addition to the error below. Unfortunately there is no way to check 
> for integer, but you could redirect stderr to /dev/null.
> 
> > +        fail "invalid gid '%d'\n" ${gid}
> > +    elif [ ${gid} -ne -1 ]; then
> > +        # check the gid is not already used for another group
> > +        if [ -n "${_group}" -a "${_group}" != "${group}" ]; then
> > +            fail "gid is already used by group '${_group}'\n"
> > +        fi
> > +
> > +        # check the group does not already exists with another gid
> > +        if [ -n "${_gid}" -a ${_gid} -ne ${gid} ]; then
> > +            fail "group already exists with gid '${_gid}'\n"
> > +        fi
> > +
> > +        # check the user does not already exists with another gid
> > +        if [ -n "${_ugid}" -a ${_ugid} -ne ${gid} ]; then
> > +            fail "user already exists with gid '${_ugid}'\n"
> > +        fi
> > +    fi
> > +
> > +    if [ ${uid} -lt -1 -o ${uid} -eq 0 ]; then
> > +        fail "invalid uid '%d'\n" ${uid}
> > +    elif [ ${uid} -ne -1 ]; then
> > +        # check the uid is not already used for another user
> > +        if [ -n "${_username}" -a "${_username}" != "${username}" ]; then
> > +            fail "uid is already used by user '${_username}'\n"
> > +        fi
> > +
> > +        # check the user does not already exists with another uid
> > +        if [ -n "${_uid}" -a ${_uid} -ne ${uid} ]; then
> > +            fail "user already exists with uid '${_uid}'\n"
> > +        fi
> > +    fi
> 
>   If the user already exists, I would check that _all_ fields are exactly 
> the same. Otherwise, it depends on the order of evaluation of the .mk 
> files what will end up in the passwd/group file.
> 
> > +
> > +    # check the user does not already exist in another group
> > +    if [ -n "${_ugroup}" -a "${_ugroup}" != "${group}" ]; then
> > +        fail "user already exists with group '${_ugroup}'\n"
> > +    fi
> > +
> > +    return 0
> > +}
> > +
> > +#----------------------------------------------------------------------------
> > +# Generate a unique GID for given group. If the group already exists,
> > +# then simply report its current GID. Otherwise, generate the lowest GID
> > +# that is:
> > +#   - not 0
> > +#   - comprised in [MIN_GID..MAX_GID]
> > +#   - not already used by a group
> > +generate_gid() {
> 
>   So you could factor generate_gid/uid into a single generate_id which 
> you call like:
> 
> generate_id ${group} ${GROUP} ${MIN_GID} ${MAX_GID}
> 
> > +    local group="${1}"
> > +    local gid
> > +
> > +    gid="$( get_gid "${group}" )"
> > +    if [ -z "${gid}" ]; then
> > +        for(( gid=MIN_GID; gid<=MAX_GID; gid++ )); do
> > +            if [ -z "$( get_group "${gid}" )" ]; then
> > +                break
> > +            fi
> > +        done
> > +        if [ ${gid} -gt ${MAX_GID} ]; then
> > +            fail "can not allocate a GID for group '%s'\n" "${group}"
> > +        fi
> > +    fi
> > +    printf "%d\n" "${gid}"
> > +}
> > +
> > +#----------------------------------------------------------------------------
> > +# Add a group; if it does already exist, remove it first
> > +add_one_group() {
> > +    local group="${1}"
> > +    local gid="${2}"
> > +    local _f
> > +
> > +    # Generate a new GID if needed
> > +    if [ ${gid} -eq -1 ]; then
> > +        gid="$( generate_gid "${group}" )"
> > +    fi
> > +
> > +    # Remove any previous instance of this group
> > +    for _f in "${GROUP}" "${GSHADOW}"; do
> 
>   Since ${GROUP} always exists, I think having a loop here is harder to 
> read that just repeating the sed call.
> 
> > +        [ -f "${_f}" ] || continue
> > +        sed -r -i -e '/^'"${group}"':.*/d;' "${_f}"
> 
>   -r is redundant (it's a plain regex).
> 
>   Quoting could be simpler:
> 
> 	sed -i -e "/^${group}:.*/d;" "${GROUP}"
> 
> > +    done
> > +
> > +    printf "%s:x:%d:\n" "${group}" "${gid}" >>"${GROUP}"
> > +    if [ -f "${GSHADOW}" ]; then
> 
>   Instead of having the condition twice, just put the sed statement for 
> GSHADOW here.
> 
> > +        printf "%s:*::\n" "${group}" >>"${GSHADOW}"
> > +    fi
> > +}
> > +
> > +#----------------------------------------------------------------------------
> > +# Generate a unique UID for given username. If the username already exists,
> > +# then simply report its current UID. Otherwise, generate the lowest UID
> > +# that is:
> > +#   - not 0
> > +#   - comprised in [MIN_UID..MAX_UID]
> > +#   - not already used by a user
> > +generate_uid() {
> > +    local username="${1}"
> > +    local uid
> > +
> > +    uid="$( get_uid "${username}" )"
> > +    if [ -z "${uid}" ]; then
> > +        for(( uid=MIN_UID; uid<=MAX_UID; uid++ )); do
> > +            if [ -z "$( get_username "${uid}" )" ]; then
> > +                break
> > +            fi
> > +        done
> > +        if [ ${uid} -gt ${MAX_UID} ]; then
> > +            fail "can not allocate a UID for user '%s'\n" "${username}"
> > +        fi
> > +    fi
> > +    printf "%d\n" "${uid}"
> > +}
> > +
> > +#----------------------------------------------------------------------------
> > +# Add given user to given group, if not already the case
> > +add_user_to_group() {
> > +    local username="${1}"
> > +    local group="${2}"
> > +    local _f
> > +
> > +    for _f in "${GROUP}" "${GSHADOW}"; do
> > +        [ -f "${_f}" ] || continue
> > +        sed -r -i -e 's/^('"${group}"':.*:)(([^:]+,)?)'"${username}"'(,[^:]+*)?$/\1\2\4/;'  \
> > +                  -e 's/^('"${group}"':.*)$/\1,'"${username}"'/;'                           \
> > +                  -e 's/,+/,/'                                                              \
> > +                  -e 's/:,/:/'                                                              \
> > +                  "${_f}"
> > +    done
> > +}
> > +
> > +#----------------------------------------------------------------------------
> > +# Encode a password
> > +encode_password() {
> > +    local passwd="${1}"
> > +
> > +    mkpasswd -m "${BR2_TARGET_GENERIC_PASSWD_METHOD}" "${passwd}"
> 
>   Err, you defined it as PASSWD_METHOD above...  But if you source 
> .config as I suggested, this is OK :-)
> 
> > +}
> > +
> > +#----------------------------------------------------------------------------
> > +# Add a user; if it does already exist, remove it first
> > +add_one_user() {
> > +    local username="${1}"
> > +    local uid="${2}"
> > +    local group="${3}"
> > +    local gid="${4}"
> > +    local passwd="${5}"
> > +    local home="${6}"
> > +    local shell="${7}"
> > +    local groups="${8}"
> > +    local comment="${9}"
> > +    local nb_days="$((($(date +%s)+(24*60*60-1))/(24*60*60)))"
> 
>   Hm, I don't like this. If I re-run the same configuration after a year, 
> it gives me a different shadow.
> 
>   I think the nb_days field in shadow should be left empty. The one in 
> the skeleton is arbitrarily set to Dec. 8 1999, it has been like that 
> since the first commit in 2001, and it has completely no meaning.
> 
> > +    local _f _group _home _shell _gid _passwd
> > +
> > +    # First, sanity-check the user
> > +    check_user_validity "${username}" "${uid}" "${group}" "${gid}"
> > +
> > +    # Generate a new UID if needed
> > +    if [ ${uid} -eq -1 ]; then
> > +        uid="$( generate_uid "${username}" )"
> > +    fi
> > +
> > +    # Remove any previous instance of this user
> > +    for _f in "${PASSWD}" "${SHADOW}"; do
> > +        sed -r -i -e '/^'"${username}"':.*/d;' "${_f}"
> > +    done
> > +
> > +    _gid="$( get_gid "${group}" )"
> > +    _shell="${shell}"
> > +    if [ "${shell}" = "-" ]; then
> > +        _shell="/bin/false"
> > +    fi
> > +    case "${home}" in
> > +        -)  _home="/";;
> > +        /)  fail "home can not explicitly be '/'\n";;
> > +        /*) _home="${home}";;
> > +        *)  fail "home must be an absolute path\n";;
> > +    esac
> > +    case "${passwd}" in
> > +        !=*)
> > +            _passwd='!'"$( encode_passwd "${passwd#!=}" )"
> > +            ;;
> > +        =*)
> > +            _passwd="$( encode_passwd "${passwd#=}" )"
> > +            ;;
> > +        *)
> > +            _passwd="${passwd}"
> > +            ;;
> > +    esac
> > +
> > +    printf "%s:x:%d:%d:%s:%s:%s\n"              \
> > +           "${username}" "${uid}" "${_gid}"     \
> > +           "${comment}" "${_home}" "${_shell}"  \
> > +           >>"${PASSWD}"
> > +    printf "%s:%s:%d:0:99999:7:::\n"                \
> > +           "${username}" "${_passwd}" "${nb_days}"  \
> > +           >>"${SHADOW}"
> > +
> > +    # Add the user to its additional groups
> > +    if [ "${groups}" != "-" ]; then
> > +        for _group in ${groups//,/ }; do
> 
>   Oh, here's another bashism. But also one which is much more difficult 
> to write in Posix sh.
> 
> > +            add_user_to_group "${username}" "${_group}"
> > +        done
> > +    fi
> > +
> > +    # If the user has a home, chown it
> > +    # (Note: stdout goes to the fakeroot-script)
> > +    if [ "${home}" != "-" ]; then
> > +        mkdir -p "${TARGET_DIR}/${home}"
> > +        printf "chown -R %d:%d '%s'\n" "${uid}" "${_gid}" "${TARGET_DIR}/${home}"
> > +    fi
> > +}
> > +
> > +#----------------------------------------------------------------------------
> > +main() {
> > +    local username uid group gid passwd home shell groups comment
> > +
> > +    # Some sanity checks
> > +    if [ ${MIN_UID} -le 0 ]; then
> > +        fail "MIN_UID must be >0 (currently %d)\n" ${MIN_UID}
> > +    fi
> > +    if [ ${MIN_GID} -le 0 ]; then
> > +        fail "MIN_GID must be >0 (currently %d)\n" ${MIN_GID}
> > +    fi
> > +
> > +    # First, create all the main groups
> > +    while read username uid group gid passwd home shell groups comment; do
> > +        [ -n "${username}" ] || continue    # Package with no user
> > +        add_one_group "${group}" "${gid}"
> 
>   In the first pass, I would only add groups where gid != -1. Then, if 
> there is a line which sets gid and another one which doesn't, it still 
> works.  Of course that means you have to repeat the add_one_group for gid 
> == -1 in the second pass.
> 
> > +    done <"${USERS_TABLE}"
> > +
> > +    # Then, create all the additional groups
> > +    # If any additional group is already a main group, we should use
> > +    # the gid of that main group; otherwise, we can use any gid
> > +    while read username uid group gid passwd home shell groups comment; do
> > +        [ -n "${username}" ] || continue    # Package with no user
> > +        if [ "${groups}" != "-" ]; then
> > +            for g in ${groups//,/ }; do
> > +                add_one_group "${g}" -1
> > +            done
> > +        fi
> > +    done <"${USERS_TABLE}"
> > +
> > +    # Finally, add users
> > +    while read username uid group gid passwd home shell groups comment; do
> > +        [ -n "${username}" ] || continue    # Package with no user
> > +        add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
> > +                     "${home}" "${shell}" "${groups}" "${comment}"
> 
>   If you reverse-sort the USERS_TABLE in the makefile before the script 
> is called, then you also make it possible for one package setting uid to 
> -1 and another giving a specific uid.
> 
>   Regards,
>   Arnout
> 
> > +    done <"${USERS_TABLE}"
> > +}
> > +
> > +#----------------------------------------------------------------------------
> > +main "${@}"
> >
> 
> 
> -- 
> Arnout Vandecappelle                          arnout at mind be
> Senior Embedded Software Architect            +32-16-286500
> Essensium/Mind                                http://www.mind.be
> G.Geenslaan 9, 3001 Leuven, Belgium           BE 872 984 063 RPR Leuven
> LinkedIn profile: http://www.linkedin.com/in/arnoutvandecappelle
> GPG fingerprint:  7CB5 E4CC 6C2E EFD4 6E3D A754 F963 ECAB 2450 2F1F
> _______________________________________________
> buildroot mailing list
> buildroot at busybox.net
> http://lists.busybox.net/mailman/listinfo/buildroot
> 
> 

-- 
.-----------------.--------------------.------------------.--------------------.
|  Yann E. MORIN  | Real-Time Embedded | /"\ ASCII RIBBON | Erics' conspiracy: |
| +33 662 376 056 | Software  Designer | \ / CAMPAIGN     |  ___               |
| +33 223 225 172 `------------.-------:  X  AGAINST      |  \e/  There is no  |
| http://ymorin.is-a-geek.org/ | _/*\_ | / \ HTML MAIL    |   v   conspiracy.  |
'------------------------------^-------^------------------^--------------------'

^ permalink raw reply	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-02-06  0:12   ` Arnout Vandecappelle
  2013-02-06 22:59     ` Yann E. MORIN
@ 2013-02-06 23:20     ` Yann E. MORIN
  2013-02-08 22:02     ` Yann E. MORIN
  2 siblings, 0 replies; 27+ messages in thread
From: Yann E. MORIN @ 2013-02-06 23:20 UTC (permalink / raw)
  To: buildroot

Arnout, All,

[sorry about previous post, I inadvertently press Ctrl-Enter which sends
the mail in kmail]

On Wednesday 06 February 2013 Arnout Vandecappelle wrote:
>   Although I have a shitload of comments below, there's nothing critical so:
> Acked-by: Arnout Vandecappelle (Essensium/Mind) <arnout@mind.be>

Thanks!

Typos fixed. See my other comments below, too.

> On 05/02/13 15:54, Yann E. MORIN wrote:
> > diff --git a/support/scripts/mkusers b/support/scripts/mkusers
> > new file mode 100755
> > index 0000000..d98714c
> > --- /dev/null
> > +++ b/support/scripts/mkusers
[--SNIP--]
> > +# will have it, but some may hav it, and its content must be in sync
> > +# with /etc/group, so any use of gshadow must be conditional.
> > +GSHADOW="${TARGET_DIR}/etc/gshadow"
> > +PASSWD_METHOD="$( sed -r -e '/^BR2_TARGET_GENERIC_PASSWD_METHOD="(.)*"$/!d' \
> > +                         -e 's//\1/;'                                       \
> > +                         "${BUILDROOT_CONFIG}"                              \
> > +                )"
> 
>   I'm personally more in favour of sourcing ${BUILDROOT_CONFIG} and using 
> "${BR2_TARGET_GENERIC_PASSWD_METHOD}" directly.

I'll how much it simplifies the code.

> > +
> > +#----------------------------------------------------------------------------
> > +get_uid() {
> > +    local username="${1}"
> > +
> > +    awk -F: '$1=="'"${username}"'" { printf( "%d\n", $3 ); }' "${PASSWD}"
> 
>   I personally find it easier to read an awk script that is written like 
> this:
> 
> 	awk -F: -v username="${1}" \
> 		'$1 == username { printf( "%d\n", $3 ); }'
> 
> (i.e. pass variables as awk variables rather than expanding them in the 
> awk script).

OK, seems reasonable.

> > +}
> > +
> > +#----------------------------------------------------------------------------
> > +get_ugid() {
> > +    local username="${1}"
> > +
> > +    awk -F: '$1=="'"${username}"'" { printf( "%d\n", $4 ); }' "${PASSWD}"
> > +}
> > +
> > +#----------------------------------------------------------------------------
> > +get_gid() {
> > +    local group="${1}"
> > +
> > +    awk -F: '$1=="'"${group}"'" { printf( "%d\n", $3 ); }' "${GROUP}"
> > +}
> 
>   I would define a single get_id for passwd and group, where the file is 
> passed as a parameter. That allows to factor the generate_uid/gid, below, 
> as well.

It's not the same field. While factorising is overall good, it simple enough
like that.

> > +    if [ "${username}" = "root" ]; then
> > +        fail "invalid username '%s\n'" "${username}"
> > +    fi
> > +
> > +    if [ ${gid} -lt -1 -o ${gid} -eq 0 ]; then
> 
>   If ${gid} is not an integer, this will report:
> [: foo: integer expression expected
> in addition to the error below. Unfortunately there is no way to check 
> for integer, but you could redirect stderr to /dev/null.

Well, this is not user-selectable, and is in the code. If the code is
not using an integer, this is a bug in our code.

> > +        # check the user does not already exists with another uid
> > +        if [ -n "${_uid}" -a ${_uid} -ne ${uid} ]; then
> > +            fail "user already exists with uid '${_uid}'\n"
> > +        fi
> > +    fi
> 
>   If the user already exists, I would check that _all_ fields are exactly 
> the same. Otherwise, it depends on the order of evaluation of the .mk 
> files what will end up in the passwd/group file.

OK, I'll queue that change for tomorow, when I'm not as sleepy as I'm now.

> > +#----------------------------------------------------------------------------
> > +# Generate a unique GID for given group. If the group already exists,
> > +# then simply report its current GID. Otherwise, generate the lowest GID
> > +# that is:
> > +#   - not 0
> > +#   - comprised in [MIN_GID..MAX_GID]
> > +#   - not already used by a group
> > +generate_gid() {
> 
>   So you could factor generate_gid/uid into a single generate_id which 
> you call like:
> 
> generate_id ${group} ${GROUP} ${MIN_GID} ${MAX_GID}

Hmm. No sure it would be that simple... :-/

> > +#----------------------------------------------------------------------------
> > +# Add a group; if it does already exist, remove it first
> > +add_one_group() {
> > +    local group="${1}"
> > +    local gid="${2}"
> > +    local _f
> > +
> > +    # Generate a new GID if needed
> > +    if [ ${gid} -eq -1 ]; then
> > +        gid="$( generate_gid "${group}" )"
> > +    fi
> > +
> > +    # Remove any previous instance of this group
> > +    for _f in "${GROUP}" "${GSHADOW}"; do
> 
>   Since ${GROUP} always exists, I think having a loop here is harder to 
> read that just repeating the sed call.

OK.

> > +        [ -f "${_f}" ] || continue
> > +        sed -r -i -e '/^'"${group}"':.*/d;' "${_f}"
> 
>   -r is redundant (it's a plain regex).

I never know what is a plain regexp or an extended regexp. So I always
use -i. Will remove.

>   Quoting could be simpler:
> 
> 	sed -i -e "/^${group}:.*/d;" "${GROUP}"

OK.

> > +    done
> > +
> > +    printf "%s:x:%d:\n" "${group}" "${gid}" >>"${GROUP}"
> > +    if [ -f "${GSHADOW}" ]; then
> 
>   Instead of having the condition twice, just put the sed statement for 
> GSHADOW here.

OK. Makes much more sense that way. :-)

> > +#----------------------------------------------------------------------------
> > +# Encode a password
> > +encode_password() {
> > +    local passwd="${1}"
> > +
> > +    mkpasswd -m "${BR2_TARGET_GENERIC_PASSWD_METHOD}" "${passwd}"
> 
>   Err, you defined it as PASSWD_METHOD above...  But if you source 
> .config as I suggested, this is OK :-)

He! How is it even supposed to work at all? :-/
/me is puzzled...
Will fix.

> > +}
> > +
> > +#----------------------------------------------------------------------------
> > +# Add a user; if it does already exist, remove it first
> > +add_one_user() {
> > +    local username="${1}"
> > +    local uid="${2}"
> > +    local group="${3}"
> > +    local gid="${4}"
> > +    local passwd="${5}"
> > +    local home="${6}"
> > +    local shell="${7}"
> > +    local groups="${8}"
> > +    local comment="${9}"
> > +    local nb_days="$((($(date +%s)+(24*60*60-1))/(24*60*60)))"
> 
>   Hm, I don't like this. If I re-run the same configuration after a year, 
> it gives me a different shadow.
> 
>   I think the nb_days field in shadow should be left empty. The one in 
> the skeleton is arbitrarily set to Dec. 8 1999, it has been like that 
> since the first commit in 2001, and it has completely no meaning.

OK, will go for an empty nb_day field ("empty field means that password
aging features are disabled.").

> > +    # Add the user to its additional groups
> > +    if [ "${groups}" != "-" ]; then
> > +        for _group in ${groups//,/ }; do
> 
>   Oh, here's another bashism. But also one which is much more difficult 
> to write in Posix sh.

It'd need womething along the lines of:

  for _group in $( printf "%s" "${groups}" |sed -r -e 's/,/ /g;' ); do

which is much more verbose... I'd like it to be kept a bash script.

> > +    # First, create all the main groups
> > +    while read username uid group gid passwd home shell groups comment; do
> > +        [ -n "${username}" ] || continue    # Package with no user
> > +        add_one_group "${group}" "${gid}"
> 
>   In the first pass, I would only add groups where gid != -1. Then, if 
> there is a line which sets gid and another one which doesn't, it still 
> works.  Of course that means you have to repeat the add_one_group for gid 
> == -1 in the second pass.

OK.

> > +    done <"${USERS_TABLE}"
> > +
> > +    # Then, create all the additional groups
> > +    # If any additional group is already a main group, we should use
> > +    # the gid of that main group; otherwise, we can use any gid
> > +    while read username uid group gid passwd home shell groups comment; do
> > +        [ -n "${username}" ] || continue    # Package with no user
> > +        if [ "${groups}" != "-" ]; then
> > +            for g in ${groups//,/ }; do
> > +                add_one_group "${g}" -1
> > +            done
> > +        fi
> > +    done <"${USERS_TABLE}"
> > +
> > +    # Finally, add users
> > +    while read username uid group gid passwd home shell groups comment; do
> > +        [ -n "${username}" ] || continue    # Package with no user
> > +        add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
> > +                     "${home}" "${shell}" "${groups}" "${comment}"
> 
>   If you reverse-sort the USERS_TABLE in the makefile before the script 
> is called, then you also make it possible for one package setting uid to 
> -1 and another giving a specific uid.

OK, I'll check this.

Thank you!

Regards,
Yann E. MORIN.

-- 
.-----------------.--------------------.------------------.--------------------.
|  Yann E. MORIN  | Real-Time Embedded | /"\ ASCII RIBBON | Erics' conspiracy: |
| +33 662 376 056 | Software  Designer | \ / CAMPAIGN     |  ___               |
| +33 223 225 172 `------------.-------:  X  AGAINST      |  \e/  There is no  |
| http://ymorin.is-a-geek.org/ | _/*\_ | / \ HTML MAIL    |   v   conspiracy.  |
'------------------------------^-------^------------------^--------------------'

^ permalink raw reply	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-02-06  0:12   ` Arnout Vandecappelle
  2013-02-06 22:59     ` Yann E. MORIN
  2013-02-06 23:20     ` Yann E. MORIN
@ 2013-02-08 22:02     ` Yann E. MORIN
  2013-02-12  6:27       ` Arnout Vandecappelle
  2 siblings, 1 reply; 27+ messages in thread
From: Yann E. MORIN @ 2013-02-08 22:02 UTC (permalink / raw)
  To: buildroot

Arnout, All,

On Wednesday 06 February 2013 Arnout Vandecappelle wrote:
> On 05/02/13 15:54, Yann E. MORIN wrote:
[--SNIP--]
> > +PASSWD_METHOD="$( sed -r -e '/^BR2_TARGET_GENERIC_PASSWD_METHOD="(.)*"$/!d' \
> > +                         -e 's//\1/;'                                       \
> > +                         "${BUILDROOT_CONFIG}"                              \
> > +                )"
> 
>   I'm personally more in favour of sourcing ${BUILDROOT_CONFIG} and using 
> "${BR2_TARGET_GENERIC_PASSWD_METHOD}" directly.

We can't simply source .config from shell scripts. .config may contain
constructs of the form:
    BR2_HOST_DIR="$(BASE_DIR)/host"

which would ultimately try to call a command named 'BASE_DIR' which is
obviously wrong for virtually everybody on Earth (and probably other
planets, too).

So, I kept a sed expression to extract the password encoding method form
the .config.

Note: I applied most of your suggestions, and added your ACK, as per your
initial mail. I hope it will be still valid once you see the nes script! ;-)

Regards,
Yann E. MORIN.

-- 
.-----------------.--------------------.------------------.--------------------.
|  Yann E. MORIN  | Real-Time Embedded | /"\ ASCII RIBBON | Erics' conspiracy: |
| +33 662 376 056 | Software  Designer | \ / CAMPAIGN     |  ___               |
| +33 223 225 172 `------------.-------:  X  AGAINST      |  \e/  There is no  |
| http://ymorin.is-a-geek.org/ | _/*\_ | / \ HTML MAIL    |   v   conspiracy.  |
'------------------------------^-------^------------------^--------------------'

^ permalink raw reply	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-02-08 22:02     ` Yann E. MORIN
@ 2013-02-12  6:27       ` Arnout Vandecappelle
  0 siblings, 0 replies; 27+ messages in thread
From: Arnout Vandecappelle @ 2013-02-12  6:27 UTC (permalink / raw)
  To: buildroot

On 08/02/13 23:02, Yann E. MORIN wrote:
> Arnout, All,
>
> On Wednesday 06 February 2013 Arnout Vandecappelle wrote:
>> On 05/02/13 15:54, Yann E. MORIN wrote:
> [--SNIP--]
>>> +PASSWD_METHOD="$( sed -r -e '/^BR2_TARGET_GENERIC_PASSWD_METHOD="(.)*"$/!d' \
>>> +                         -e 's//\1/;'                                       \
>>> +                         "${BUILDROOT_CONFIG}"                              \
>>> +                )"
>>
>>    I'm personally more in favour of sourcing ${BUILDROOT_CONFIG} and using
>> "${BR2_TARGET_GENERIC_PASSWD_METHOD}" directly.
>
> We can't simply source .config from shell scripts. .config may contain
> constructs of the form:
>      BR2_HOST_DIR="$(BASE_DIR)/host"
>
> which would ultimately try to call a command named 'BASE_DIR' which is
> obviously wrong for virtually everybody on Earth (and probably other
> planets, too).

  Ah, yes, that's why I prefer to put ${...} references in .config 
(${...} is the same as $(...) in GNU make).

  But there are about 10 references to $(...) in Config.in defaults, so 
changing that is a bit too heavy.


> So, I kept a sed expression to extract the password encoding method form
> the .config.
>
> Note: I applied most of your suggestions, and added your ACK, as per your
> initial mail. I hope it will be still valid once you see the nes script! ;-)

  No problem.

  Regards,
  Arnout



-- 
Arnout Vandecappelle                          arnout at mind be
Senior Embedded Software Architect            +32-16-286500
Essensium/Mind                                http://www.mind.be
G.Geenslaan 9, 3001 Leuven, Belgium           BE 872 984 063 RPR Leuven
LinkedIn profile: http://www.linkedin.com/in/arnoutvandecappelle
GPG fingerprint:  7CB5 E4CC 6C2E EFD4 6E3D A754 F963 ECAB 2450 2F1F

^ permalink raw reply	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-04-12 17:14 [Buildroot] [pull request v9] Pull request for branch yem-package-create-user Yann E. MORIN
@ 2013-04-12 17:14 ` Yann E. MORIN
  0 siblings, 0 replies; 27+ messages in thread
From: Yann E. MORIN @ 2013-04-12 17:14 UTC (permalink / raw)
  To: buildroot

Packages that install daemons may need those daemons to run as a non-root,
or an otherwise non-system (eg. 'daemon'), user.

Add infrastructure for packages to create users, by declaring the FOO_USERS
variable that contain a makedev-syntax-like description of the user(s) to
add.

Signed-off-by: "Yann E. MORIN" <yann.morin.1998@free.fr>
Cc: Samuel Martin <s.martin49@gmail.com>
Cc: Cam Hutchison <camh@xdna.net>
Cc: Thomas Petazzoni <thomas.petazzoni@free-electrons.com>
Acked-by: Arnout Vandecappelle (Essensium/Mind) <arnout@mind.be>
---
 docs/manual/adding-packages-generic.txt |   16 +-
 docs/manual/appendix.txt                |    1 +
 docs/manual/makeusers-syntax.txt        |   87 +++++++
 fs/common.mk                            |    3 +
 package/pkg-generic.mk                  |    1 +
 support/scripts/mkusers                 |  409 +++++++++++++++++++++++++++++++
 6 files changed, 515 insertions(+), 2 deletions(-)
 create mode 100644 docs/manual/makeusers-syntax.txt
 create mode 100755 support/scripts/mkusers

diff --git a/docs/manual/adding-packages-generic.txt b/docs/manual/adding-packages-generic.txt
index a615ae2..aa94b88 100644
--- a/docs/manual/adding-packages-generic.txt
+++ b/docs/manual/adding-packages-generic.txt
@@ -53,7 +53,11 @@ system is based on hand-written Makefiles or shell scripts.
 36:	/bin/foo  f  4755  0  0	 -  -  -  -  -
 37: endef
 38:
-39: $(eval $(generic-package))
+39: define LIBFOO_USERS
+40:	foo -1 libfoo -1 * - - - LibFoo daemon
+41: endef
+42:
+43: $(eval $(generic-package))
 --------------------------------
 
 The Makefile begins on line 7 to 11 with metadata information: the
@@ -140,7 +144,10 @@ On line 31..33, we define a device-node file used by this package
 On line 35..37, we define the permissions to set to specific files
 installed by this package (+LIBFOO_PERMISSIONS+).
 
-Finally, on line 39, we call the +generic-package+ function, which
+On lines 39..41, we define a user that is used by this package (eg.
+to run a daemon as non-root) (+LIBFOO_USERS+).
+
+Finally, on line 43, we call the +generic-package+ function, which
 generates, according to the variables defined previously, all the
 Makefile code necessary to make your package working.
 
@@ -307,6 +314,11 @@ information is (assuming the package name is +libfoo+) :
   You can find some documentation for this syntax in the xref:makedev-syntax[].
   This variable is optional.
 
+* +LIBFOO_USERS+ lists the users to create for this package, if it installs
+  a program you want to run as a specific user (eg. as a daemon, or as a
+  cron-job). The syntax is similar in spirit to the makedevs one, and is
+  described in the xref:makeuser-syntax[]. This variable is optional.
+
 * +LIBFOO_LICENSE+ defines the license (or licenses) under which the package
   is released.
   This name will appear in the manifest file produced by +make legal-info+.
diff --git a/docs/manual/appendix.txt b/docs/manual/appendix.txt
index ef34169..c48c3b1 100644
--- a/docs/manual/appendix.txt
+++ b/docs/manual/appendix.txt
@@ -5,6 +5,7 @@ Appendix
 ========
 
 include::makedev-syntax.txt[]
+include::makeusers-syntax.txt[]
 
 [[package-list]]
 Available packages
diff --git a/docs/manual/makeusers-syntax.txt b/docs/manual/makeusers-syntax.txt
new file mode 100644
index 0000000..2199654
--- /dev/null
+++ b/docs/manual/makeusers-syntax.txt
@@ -0,0 +1,87 @@
+// -*- mode:doc -*- ;
+
+[[makeuser-syntax]]
+Makeuser syntax documentation
+-----------------------------
+
+The syntax to create users is inspired by the makedev syntax, above, but
+is specific to Buildroot.
+
+The syntax for adding a user is a space-separated list of fields, one
+user per line; the fields are:
+
+|=================================================================
+|username |uid |group |gid |password |home |shell |groups |comment
+|=================================================================
+
+Where:
+
+- +username+ is the desired user name (aka login name) for the user.
+  It can not be +root+, and must be unique.
+- +uid+ is the desired UID for the user. It must be unique, and not
+  +0+. If set to +-1+, then a unique UID will be computed by Buildroot
+  in the range [1000...1999]
+- +group+ is the desired name for the user's main group. It can not
+  be +root+. If the group does not exist, it will be created.
+- +gid+ is the desired GID for the user's main group. It must be unique,
+  and not +0+. If set to +-1+, and the group does not already exist, then
+  a unique GID will be computed by Buildroot in the range [1000..1999]
+- +password+ is the crypt(3)-encoded password. If prefixed with +!+,
+  then login is disabled. If prefixed with +=+, then it is interpreted
+  as clear-text, and will be crypt-encoded (using MD5). If prefixed with
+  +!=+, then the password will be crypt-encoded (using MD5) and login
+  will be disabled. If set to +*+, then login is not allowed.
+- +home+ is the desired home directory for the user. If set to '-', no
+  home directory will be created, and the user's home will be +/+.
+  Explicitly setting +home+ to +/+ is not allowed.
+- +shell+ is the desired shell for the user. If set to +-+, then
+  +/bin/false+ is set as the user's shell.
+- +groups+ is the comma-separated list of additional groups the user
+  should be part of. If set to +-+, then the user will be a member of
+  no additional group. Missing groups will be created with an arbitrary
+  +gid+.
+- +comment+ (aka https://en.wikipedia.org/wiki/Gecos_field[GECOS]
+  field) is an almost-free-form text.
+
+There are a few restrictions on the content of each field:
+
+* except for +comment+, all fields are mandatory.
+* except for +comment+, fields may not contain spaces.
+* no field may contain a colon (+:+).
+
+If +home+ is not +-+, then the home directory, and all files below,
+will belong to the user and its main group.
+
+Examples:
+
+----
+foo -1 bar -1 !=blabla /home/foo /bin/sh alpha,bravo Foo user
+----
+
+This will create this user:
+
+- +username+ (aka login name) is: +foo+
+- +uid+ is computed by Buildroot
+- main +group+ is: +bar+
+- main group +gid+ is computed by Buildroot
+- clear-text +password+ is: +blabla+, will be crypt(3)-encoded, and login is disabled.
+- +home+ is: +/home/foo+
+- +shell+ is: +/bin/sh+
+- +foo+ is also a member of +groups+: +alpha+ and +bravo+
+- +comment+ is: +Foo user+
+
+----
+test 8000 wheel -1 = - /bin/sh - Test user
+----
+
+This will create this user:
+
+- +username+ (aka login name) is: +test+
+- +uid+ is : +8000+
+- main +group+ is: +wheel+
+- main group +gid+ is computed by Buildroot, and will use the value defined in the rootfs skeleton
+- +password+ is empty (aka no password).
+- +home+ is +/+ but will not belong to +test+
+- +shell+ is: +/bin/sh+
+- +test+ is not a member of any additional +groups+
+- +comment+ is: +Test user+
diff --git a/fs/common.mk b/fs/common.mk
index a0b7b39..f60fbe7 100644
--- a/fs/common.mk
+++ b/fs/common.mk
@@ -35,6 +35,7 @@ FAKEROOT_SCRIPT = $(BUILD_DIR)/_fakeroot.fs
 FULL_DEVICE_TABLE = $(BUILD_DIR)/_device_table.txt
 ROOTFS_DEVICE_TABLES = $(call qstrip,$(BR2_ROOTFS_DEVICE_TABLE) \
        $(BR2_ROOTFS_STATIC_DEVICE_TABLE))
+USERS_TABLE = $(BUILD_DIR)/_users_table.txt
 
 define ROOTFS_TARGET_INTERNAL
 
@@ -55,6 +56,8 @@ endif
 	printf '$$(subst $$(sep),\n,$$(PACKAGES_PERMISSIONS_TABLE))' >> $$(FULL_DEVICE_TABLE)
 	echo "$$(HOST_DIR)/usr/bin/makedevs -d $$(FULL_DEVICE_TABLE) $$(TARGET_DIR)" >> $$(FAKEROOT_SCRIPT)
 endif
+	printf '$(subst $(sep),\n,$(PACKAGES_USERS))' > $(USERS_TABLE)
+	$(TOPDIR)/support/scripts/mkusers $(USERS_TABLE) $(TARGET_DIR) >> $(FAKEROOT_SCRIPT)
 	echo "$$(ROOTFS_$(2)_CMD)" >> $$(FAKEROOT_SCRIPT)
 	chmod a+x $$(FAKEROOT_SCRIPT)
 	$$(HOST_DIR)/usr/bin/fakeroot -- $$(FAKEROOT_SCRIPT)
diff --git a/package/pkg-generic.mk b/package/pkg-generic.mk
index 901bcf7..6b54c97 100644
--- a/package/pkg-generic.mk
+++ b/package/pkg-generic.mk
@@ -529,6 +529,7 @@ ifeq ($$($$($(2)_KCONFIG_VAR)),y)
 TARGETS += $(1)
 PACKAGES_PERMISSIONS_TABLE += $$($(2)_PERMISSIONS)$$(sep)
 PACKAGES_DEVICES_TABLE += $$($(2)_DEVICES)$$(sep)
+PACKAGES_USERS += $$($(2)_USERS)$$(sep)
 
 ifeq ($$($(2)_SITE_METHOD),svn)
 DL_TOOLS_DEPENDENCIES += svn
diff --git a/support/scripts/mkusers b/support/scripts/mkusers
new file mode 100755
index 0000000..19aa085
--- /dev/null
+++ b/support/scripts/mkusers
@@ -0,0 +1,409 @@
+#!/bin/bash
+set -e
+myname="${0##*/}"
+
+#----------------------------------------------------------------------------
+# Configurable items
+MIN_UID=1000
+MAX_UID=1999
+MIN_GID=1000
+MAX_GID=1999
+# No more is configurable below this point
+#----------------------------------------------------------------------------
+
+#----------------------------------------------------------------------------
+error() {
+    local fmt="${1}"
+    shift
+
+    printf "%s: " "${myname}" >&2
+    printf "${fmt}" "${@}" >&2
+}
+fail() {
+    error "$@"
+    exit 1
+}
+
+#----------------------------------------------------------------------------
+if [ ${#} -ne 2 ]; then
+    fail "usage: %s USERS_TABLE TARGET_DIR\n"
+fi
+USERS_TABLE="${1}"
+TARGET_DIR="${2}"
+shift 2
+PASSWD="${TARGET_DIR}/etc/passwd"
+SHADOW="${TARGET_DIR}/etc/shadow"
+GROUP="${TARGET_DIR}/etc/group"
+# /etc/gshadow is not part of the standard skeleton, so not everybody
+# will have it, but some may hav it, and its content must be in sync
+# with /etc/group, so any use of gshadow must be conditional.
+GSHADOW="${TARGET_DIR}/etc/gshadow"
+
+# We can't simply source ${BUILDROOT_CONFIG} as it may contains constructs
+# such as:
+#    BR2_DEFCONFIG="$(CONFIG_DIR)/defconfig"
+# which when sourced from a shell script will eventually try to execute
+# a command name 'CONFIG_DIR', which is plain wrong for virtually every
+# systems out there.
+# So, we have to scan that file instead. Sigh... :-(
+PASSWD_METHOD="$( sed -r -e '/^BR2_TARGET_GENERIC_PASSWD_METHOD="(.*)"$/!d;'    \
+                         -e 's//\1/;'                                           \
+                         "${BUILDROOT_CONFIG}"                                  \
+                )"
+
+#----------------------------------------------------------------------------
+get_uid() {
+    local username="${1}"
+
+    awk -F: -v username="${username}"                           \
+        '$1 == username { printf( "%d\n", $3 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_ugid() {
+    local username="${1}"
+
+    awk -F:i -v username="${username}"                          \
+        '$1 == username { printf( "%d\n", $4 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_gid() {
+    local group="${1}"
+
+    awk -F: -v group="${group}"                             \
+        '$1 == group { printf( "%d\n", $3 ); }' "${GROUP}"
+}
+
+#----------------------------------------------------------------------------
+get_username() {
+    local uid="${1}"
+
+    awk -F: -v uid="${uid}"                                 \
+        '$3 == uid { printf( "%s\n", $1 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_group() {
+    local gid="${1}"
+
+    awk -F: -v gid="${gid}"                             \
+        '$3 == gid { printf( "%s\n", $1 ); }' "${GROUP}"
+}
+
+#----------------------------------------------------------------------------
+get_ugroup() {
+    local username="${1}"
+    local ugid
+
+    ugid="$( get_ugid "${username}" )"
+    if [ -n "${ugid}" ]; then
+        get_group "${ugid}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+# Sanity-check the new user/group:
+#   - check the gid is not already used for another group
+#   - check the group does not already exist with another gid
+#   - check the user does not already exist with another gid
+#   - check the uid is not already used for another user
+#   - check the user does not already exist with another uid
+#   - check the user does not already exist in another group
+check_user_validity() {
+    local username="${1}"
+    local uid="${2}"
+    local group="${3}"
+    local gid="${4}"
+    local _uid _ugid _gid _username _group _ugroup
+
+    _group="$( get_group "${gid}" )"
+    _gid="$( get_gid "${group}" )"
+    _ugid="$( get_ugid "${username}" )"
+    _username="$( get_username "${uid}" )"
+    _uid="$( get_uid "${username}" )"
+    _ugroup="$( get_ugroup "${username}" )"
+
+    if [ "${username}" = "root" ]; then
+        fail "invalid username '%s\n'" "${username}"
+    fi
+
+    if [ ${gid} -lt -1 -o ${gid} -eq 0 ]; then
+        fail "invalid gid '%d'\n" ${gid}
+    elif [ ${gid} -ne -1 ]; then
+        # check the gid is not already used for another group
+        if [ -n "${_group}" -a "${_group}" != "${group}" ]; then
+            fail "gid is already used by group '${_group}'\n"
+        fi
+
+        # check the group does not already exists with another gid
+        if [ -n "${_gid}" -a ${_gid} -ne ${gid} ]; then
+            fail "group already exists with gid '${_gid}'\n"
+        fi
+
+        # check the user does not already exists with another gid
+        if [ -n "${_ugid}" -a ${_ugid} -ne ${gid} ]; then
+            fail "user already exists with gid '${_ugid}'\n"
+        fi
+    fi
+
+    if [ ${uid} -lt -1 -o ${uid} -eq 0 ]; then
+        fail "invalid uid '%d'\n" ${uid}
+    elif [ ${uid} -ne -1 ]; then
+        # check the uid is not already used for another user
+        if [ -n "${_username}" -a "${_username}" != "${username}" ]; then
+            fail "uid is already used by user '${_username}'\n"
+        fi
+
+        # check the user does not already exists with another uid
+        if [ -n "${_uid}" -a ${_uid} -ne ${uid} ]; then
+            fail "user already exists with uid '${_uid}'\n"
+        fi
+    fi
+
+    # check the user does not already exist in another group
+    if [ -n "${_ugroup}" -a "${_ugroup}" != "${group}" ]; then
+        fail "user already exists with group '${_ugroup}'\n"
+    fi
+
+    return 0
+}
+
+#----------------------------------------------------------------------------
+# Generate a unique GID for given group. If the group already exists,
+# then simply report its current GID. Otherwise, generate the lowest GID
+# that is:
+#   - not 0
+#   - comprised in [MIN_GID..MAX_GID]
+#   - not already used by a group
+generate_gid() {
+    local group="${1}"
+    local gid
+
+    gid="$( get_gid "${group}" )"
+    if [ -z "${gid}" ]; then
+        for(( gid=MIN_GID; gid<=MAX_GID; gid++ )); do
+            if [ -z "$( get_group "${gid}" )" ]; then
+                break
+            fi
+        done
+        if [ ${gid} -gt ${MAX_GID} ]; then
+            fail "can not allocate a GID for group '%s'\n" "${group}"
+        fi
+    fi
+    printf "%d\n" "${gid}"
+}
+
+#----------------------------------------------------------------------------
+# Add a group; if it does already exist, remove it first
+add_one_group() {
+    local group="${1}"
+    local gid="${2}"
+    local _f
+
+    # Generate a new GID if needed
+    if [ ${gid} -eq -1 ]; then
+        gid="$( generate_gid "${group}" )"
+    fi
+
+    # Remove any previous instance of this group, and re-add the new one
+    sed -i -e '/^'"${group}"':.*/d;' "${GROUP}"
+    printf "%s:x:%d:\n" "${group}" "${gid}" >>"${GROUP}"
+
+    # Ditto for /etc/gshadow if it exists
+    if [ -f "${GSHADOW}" ]; then
+        sed -i -e '/^'"${group}"':.*/d;' "${GSHADOW}"
+        printf "%s:*::\n" "${group}" >>"${GSHADOW}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+# Generate a unique UID for given username. If the username already exists,
+# then simply report its current UID. Otherwise, generate the lowest UID
+# that is:
+#   - not 0
+#   - comprised in [MIN_UID..MAX_UID]
+#   - not already used by a user
+generate_uid() {
+    local username="${1}"
+    local uid
+
+    uid="$( get_uid "${username}" )"
+    if [ -z "${uid}" ]; then
+        for(( uid=MIN_UID; uid<=MAX_UID; uid++ )); do
+            if [ -z "$( get_username "${uid}" )" ]; then
+                break
+            fi
+        done
+        if [ ${uid} -gt ${MAX_UID} ]; then
+            fail "can not allocate a UID for user '%s'\n" "${username}"
+        fi
+    fi
+    printf "%d\n" "${uid}"
+}
+
+#----------------------------------------------------------------------------
+# Add given user to given group, if not already the case
+add_user_to_group() {
+    local username="${1}"
+    local group="${2}"
+    local _f
+
+    for _f in "${GROUP}" "${GSHADOW}"; do
+        [ -f "${_f}" ] || continue
+        sed -r -i -e 's/^('"${group}"':.*:)(([^:]+,)?)'"${username}"'(,[^:]+*)?$/\1\2\4/;'  \
+                  -e 's/^('"${group}"':.*)$/\1,'"${username}"'/;'                           \
+                  -e 's/,+/,/'                                                              \
+                  -e 's/:,/:/'                                                              \
+                  "${_f}"
+    done
+}
+
+#----------------------------------------------------------------------------
+# Encode a password
+encode_password() {
+    local passwd="${1}"
+
+    mkpasswd -m "${PASSWD_METHOD}" "${passwd}"
+}
+
+#----------------------------------------------------------------------------
+# Add a user; if it does already exist, remove it first
+add_one_user() {
+    local username="${1}"
+    local uid="${2}"
+    local group="${3}"
+    local gid="${4}"
+    local passwd="${5}"
+    local home="${6}"
+    local shell="${7}"
+    local groups="${8}"
+    local comment="${9}"
+    local _f _group _home _shell _gid _passwd
+
+    # First, sanity-check the user
+    check_user_validity "${username}" "${uid}" "${group}" "${gid}"
+
+    # Generate a new UID if needed
+    if [ ${uid} -eq -1 ]; then
+        uid="$( generate_uid "${username}" )"
+    fi
+
+    # Remove any previous instance of this user
+    for _f in "${PASSWD}" "${SHADOW}"; do
+        sed -r -i -e '/^'"${username}"':.*/d;' "${_f}"
+    done
+
+    _gid="$( get_gid "${group}" )"
+    _shell="${shell}"
+    if [ "${shell}" = "-" ]; then
+        _shell="/bin/false"
+    fi
+    case "${home}" in
+        -)  _home="/";;
+        /)  fail "home can not explicitly be '/'\n";;
+        /*) _home="${home}";;
+        *)  fail "home must be an absolute path\n";;
+    esac
+    case "${passwd}" in
+        !=*)
+            _passwd='!'"$( encode_passwd "${passwd#!=}" )"
+            ;;
+        =*)
+            _passwd="$( encode_passwd "${passwd#=}" )"
+            ;;
+        *)
+            _passwd="${passwd}"
+            ;;
+    esac
+
+    printf "%s:x:%d:%d:%s:%s:%s\n"              \
+           "${username}" "${uid}" "${_gid}"     \
+           "${comment}" "${_home}" "${_shell}"  \
+           >>"${PASSWD}"
+    printf "%s:%s:::::::\n"      \
+           "${username}" "${_passwd}"   \
+           >>"${SHADOW}"
+
+    # Add the user to its additional groups
+    if [ "${groups}" != "-" ]; then
+        for _group in ${groups//,/ }; do
+            add_user_to_group "${username}" "${_group}"
+        done
+    fi
+
+    # If the user has a home, chown it
+    # (Note: stdout goes to the fakeroot-script)
+    if [ "${home}" != "-" ]; then
+        mkdir -p "${TARGET_DIR}/${home}"
+        printf "chown -R %d:%d '%s'\n" "${uid}" "${_gid}" "${TARGET_DIR}/${home}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+main() {
+    local username uid group gid passwd home shell groups comment
+
+    # Some sanity checks
+    if [ ${MIN_UID} -le 0 ]; then
+        fail "MIN_UID must be >0 (currently %d)\n" ${MIN_UID}
+    fi
+    if [ ${MIN_GID} -le 0 ]; then
+        fail "MIN_GID must be >0 (currently %d)\n" ${MIN_GID}
+    fi
+
+    # We first create groups whose gid is not -1, and then we create groups
+    # whose gid is -1 (automatic), so that, if a group is defined both with
+    # a specified gid and an automatic gid, we ensure the specified gid is
+    # used, rather than a different automatic gid is computed.
+
+    # First, create all the main groups which gid is *not* automatic
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        [ ${gid} -ge 0     ] || continue    # Automatic gid
+        add_one_group "${group}" "${gid}"
+    done <"${USERS_TABLE}"
+
+    # Then, create all the main groups which gid *is* automatic
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        [ ${gid} -eq -1    ] || continue    # Non-automatic gid
+        add_one_group "${group}" "${gid}"
+    done <"${USERS_TABLE}"
+
+    # Then, create all the additional groups
+    # If any additional group is already a main group, we should use
+    # the gid of that main group; otherwise, we can use any gid
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        if [ "${groups}" != "-" ]; then
+            for g in ${groups//,/ }; do
+                add_one_group "${g}" -1
+            done
+        fi
+    done <"${USERS_TABLE}"
+
+    # When adding users, we do as for groups, in case two packages create
+    # the same user, one with an automatic uid, the other with a specified
+    # uid, to ensure the specified uid is used, rather than an incompatible
+    # uid be generated.
+
+    # Now, add users whose uid is *not* automatic
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        [ ${uid} -ge 0     ] || continue    # Automatic uid
+        add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
+                     "${home}" "${shell}" "${groups}" "${comment}"
+    done <"${USERS_TABLE}"
+
+    # Finally, add users whose uid *is* automatic
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        [ ${uid} -eq -1    ] || continue    # Non-automatic uid
+        add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
+                     "${home}" "${shell}" "${groups}" "${comment}"
+    done <"${USERS_TABLE}"
+}
+
+#----------------------------------------------------------------------------
+main "${@}"
-- 
1.7.2.5

^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-03-08 17:09   ` Yann E. MORIN
@ 2013-03-29 14:49     ` Jeremy Rosen
  0 siblings, 0 replies; 27+ messages in thread
From: Jeremy Rosen @ 2013-03-29 14:49 UTC (permalink / raw)
  To: buildroot


just a quick ping on this particular patch....

it's usefull at least for pulseaudio in system mode, which needs its own user and group.

Note that the pulseaudio documentation recommends never using pulseaudio as a system daemon except for headless embedded system.

So I think it's legitimate to handle this use case for buildroot :)

There are probably other daemons that can use their own user if that feature is available.

Regards

J?r?my Rosen

^ permalink raw reply	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-03-07 21:47 ` [Buildroot] [PATCH 1/2] packages: add ability for packages to create users Yann E. MORIN
@ 2013-03-08 17:09   ` Yann E. MORIN
  2013-03-29 14:49     ` Jeremy Rosen
  0 siblings, 1 reply; 27+ messages in thread
From: Yann E. MORIN @ 2013-03-08 17:09 UTC (permalink / raw)
  To: buildroot

Hello All,

On Thursday 07 March 2013 Yann E. MORIN wrote:
> Packages that install daemons may need those daemons to run as a non-root,
> or an otherwise non-system (eg. 'daemon'), user.
> 
> Add infrastructure for packages to create users, by declaring the FOO_USERS
> variable that contain a makedev-syntax-like description of the user(s) to
> add.
> diff --git a/docs/manual/adding-packages-generic.txt b/docs/manual/adding-packages-generic.txt
> index a615ae2..fc6ea30 100644
> --- a/docs/manual/adding-packages-generic.txt
> +++ b/docs/manual/adding-packages-generic.txt
> @@ -53,7 +53,12 @@ system is based on hand-written Makefiles or shell scripts.
>  36:	/bin/foo  f  4755  0  0	 -  -  -  -  -
>  37: endef
>  38:
> -39: $(eval $(generic-package))
> +39: define LIBFOO_USERS
> +40:	foo -1 libfoo -1 * - - - LibFoo daemon
> +41: endef
> +42:
> +43: $(eval $(generic-package))
> +>>>>>>> packages: add ability for packages to create users

Apparently, I was not careful enough when resolving the conflicts during
the rebase. Will fix and re-submit in the WE.

Regards,
Yann E. MORIN.

-- 
.-----------------.--------------------.------------------.--------------------.
|  Yann E. MORIN  | Real-Time Embedded | /"\ ASCII RIBBON | Erics' conspiracy: |
| +33 662 376 056 | Software  Designer | \ / CAMPAIGN     |  ___               |
| +33 223 225 172 `------------.-------:  X  AGAINST      |  \e/  There is no  |
| http://ymorin.is-a-geek.org/ | _/*\_ | / \ HTML MAIL    |   v   conspiracy.  |
'------------------------------^-------^------------------^--------------------'

^ permalink raw reply	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-03-07 21:47 [Buildroot] [pull request v8] Pull request for branch yem-package-create-user Yann E. MORIN
@ 2013-03-07 21:47 ` Yann E. MORIN
  2013-03-08 17:09   ` Yann E. MORIN
  0 siblings, 1 reply; 27+ messages in thread
From: Yann E. MORIN @ 2013-03-07 21:47 UTC (permalink / raw)
  To: buildroot

Packages that install daemons may need those daemons to run as a non-root,
or an otherwise non-system (eg. 'daemon'), user.

Add infrastructure for packages to create users, by declaring the FOO_USERS
variable that contain a makedev-syntax-like description of the user(s) to
add.

Signed-off-by: "Yann E. MORIN" <yann.morin.1998@free.fr>
Cc: Samuel Martin <s.martin49@gmail.com>
Cc: Cam Hutchison <camh@xdna.net>
Cc: Thomas Petazzoni <thomas.petazzoni@free-electrons.com>
Acked-by: Arnout Vandecappelle (Essensium/Mind) <arnout@mind.be>
---
 docs/manual/adding-packages-generic.txt |   17 ++-
 docs/manual/appendix.txt                |    1 +
 docs/manual/makeusers-syntax.txt        |   87 +++++++
 fs/common.mk                            |    3 +
 package/pkg-generic.mk                  |    1 +
 support/scripts/mkusers                 |  409 +++++++++++++++++++++++++++++++
 6 files changed, 516 insertions(+), 2 deletions(-)
 create mode 100644 docs/manual/makeusers-syntax.txt
 create mode 100755 support/scripts/mkusers

diff --git a/docs/manual/adding-packages-generic.txt b/docs/manual/adding-packages-generic.txt
index a615ae2..fc6ea30 100644
--- a/docs/manual/adding-packages-generic.txt
+++ b/docs/manual/adding-packages-generic.txt
@@ -53,7 +53,12 @@ system is based on hand-written Makefiles or shell scripts.
 36:	/bin/foo  f  4755  0  0	 -  -  -  -  -
 37: endef
 38:
-39: $(eval $(generic-package))
+39: define LIBFOO_USERS
+40:	foo -1 libfoo -1 * - - - LibFoo daemon
+41: endef
+42:
+43: $(eval $(generic-package))
+>>>>>>> packages: add ability for packages to create users
 --------------------------------
 
 The Makefile begins on line 7 to 11 with metadata information: the
@@ -140,7 +145,10 @@ On line 31..33, we define a device-node file used by this package
 On line 35..37, we define the permissions to set to specific files
 installed by this package (+LIBFOO_PERMISSIONS+).
 
-Finally, on line 39, we call the +generic-package+ function, which
+On lines 39..41, we define a user that is used by this package (eg.
+to run a daemon as non-root) (+LIBFOO_USERS+).
+
+Finally, on line 43, we call the +generic-package+ function, which
 generates, according to the variables defined previously, all the
 Makefile code necessary to make your package working.
 
@@ -307,6 +315,11 @@ information is (assuming the package name is +libfoo+) :
   You can find some documentation for this syntax in the xref:makedev-syntax[].
   This variable is optional.
 
+* +LIBFOO_USERS+ lists the users to create for this package, if it installs
+  a program you want to run as a specific user (eg. as a daemon, or as a
+  cron-job). The syntax is similar in spirit to the makedevs one, and is
+  described in the xref:makeuser-syntax[]. This variable is optional.
+
 * +LIBFOO_LICENSE+ defines the license (or licenses) under which the package
   is released.
   This name will appear in the manifest file produced by +make legal-info+.
diff --git a/docs/manual/appendix.txt b/docs/manual/appendix.txt
index ef34169..c48c3b1 100644
--- a/docs/manual/appendix.txt
+++ b/docs/manual/appendix.txt
@@ -5,6 +5,7 @@ Appendix
 ========
 
 include::makedev-syntax.txt[]
+include::makeusers-syntax.txt[]
 
 [[package-list]]
 Available packages
diff --git a/docs/manual/makeusers-syntax.txt b/docs/manual/makeusers-syntax.txt
new file mode 100644
index 0000000..2199654
--- /dev/null
+++ b/docs/manual/makeusers-syntax.txt
@@ -0,0 +1,87 @@
+// -*- mode:doc -*- ;
+
+[[makeuser-syntax]]
+Makeuser syntax documentation
+-----------------------------
+
+The syntax to create users is inspired by the makedev syntax, above, but
+is specific to Buildroot.
+
+The syntax for adding a user is a space-separated list of fields, one
+user per line; the fields are:
+
+|=================================================================
+|username |uid |group |gid |password |home |shell |groups |comment
+|=================================================================
+
+Where:
+
+- +username+ is the desired user name (aka login name) for the user.
+  It can not be +root+, and must be unique.
+- +uid+ is the desired UID for the user. It must be unique, and not
+  +0+. If set to +-1+, then a unique UID will be computed by Buildroot
+  in the range [1000...1999]
+- +group+ is the desired name for the user's main group. It can not
+  be +root+. If the group does not exist, it will be created.
+- +gid+ is the desired GID for the user's main group. It must be unique,
+  and not +0+. If set to +-1+, and the group does not already exist, then
+  a unique GID will be computed by Buildroot in the range [1000..1999]
+- +password+ is the crypt(3)-encoded password. If prefixed with +!+,
+  then login is disabled. If prefixed with +=+, then it is interpreted
+  as clear-text, and will be crypt-encoded (using MD5). If prefixed with
+  +!=+, then the password will be crypt-encoded (using MD5) and login
+  will be disabled. If set to +*+, then login is not allowed.
+- +home+ is the desired home directory for the user. If set to '-', no
+  home directory will be created, and the user's home will be +/+.
+  Explicitly setting +home+ to +/+ is not allowed.
+- +shell+ is the desired shell for the user. If set to +-+, then
+  +/bin/false+ is set as the user's shell.
+- +groups+ is the comma-separated list of additional groups the user
+  should be part of. If set to +-+, then the user will be a member of
+  no additional group. Missing groups will be created with an arbitrary
+  +gid+.
+- +comment+ (aka https://en.wikipedia.org/wiki/Gecos_field[GECOS]
+  field) is an almost-free-form text.
+
+There are a few restrictions on the content of each field:
+
+* except for +comment+, all fields are mandatory.
+* except for +comment+, fields may not contain spaces.
+* no field may contain a colon (+:+).
+
+If +home+ is not +-+, then the home directory, and all files below,
+will belong to the user and its main group.
+
+Examples:
+
+----
+foo -1 bar -1 !=blabla /home/foo /bin/sh alpha,bravo Foo user
+----
+
+This will create this user:
+
+- +username+ (aka login name) is: +foo+
+- +uid+ is computed by Buildroot
+- main +group+ is: +bar+
+- main group +gid+ is computed by Buildroot
+- clear-text +password+ is: +blabla+, will be crypt(3)-encoded, and login is disabled.
+- +home+ is: +/home/foo+
+- +shell+ is: +/bin/sh+
+- +foo+ is also a member of +groups+: +alpha+ and +bravo+
+- +comment+ is: +Foo user+
+
+----
+test 8000 wheel -1 = - /bin/sh - Test user
+----
+
+This will create this user:
+
+- +username+ (aka login name) is: +test+
+- +uid+ is : +8000+
+- main +group+ is: +wheel+
+- main group +gid+ is computed by Buildroot, and will use the value defined in the rootfs skeleton
+- +password+ is empty (aka no password).
+- +home+ is +/+ but will not belong to +test+
+- +shell+ is: +/bin/sh+
+- +test+ is not a member of any additional +groups+
+- +comment+ is: +Test user+
diff --git a/fs/common.mk b/fs/common.mk
index 8b5b2f2..a8279b1 100644
--- a/fs/common.mk
+++ b/fs/common.mk
@@ -35,6 +35,7 @@ FAKEROOT_SCRIPT = $(BUILD_DIR)/_fakeroot.fs
 FULL_DEVICE_TABLE = $(BUILD_DIR)/_device_table.txt
 ROOTFS_DEVICE_TABLES = $(call qstrip,$(BR2_ROOTFS_DEVICE_TABLE)) \
 	$(call qstrip,$(BR2_ROOTFS_STATIC_DEVICE_TABLE))
+USERS_TABLE = $(BUILD_DIR)/_users_table.txt
 
 define ROOTFS_TARGET_INTERNAL
 
@@ -55,6 +56,8 @@ endif
 	printf '$$(subst $$(sep),\n,$$(PACKAGES_PERMISSIONS_TABLE))' >> $$(FULL_DEVICE_TABLE)
 	echo "$$(HOST_DIR)/usr/bin/makedevs -d $$(FULL_DEVICE_TABLE) $$(TARGET_DIR)" >> $$(FAKEROOT_SCRIPT)
 endif
+	printf '$(subst $(sep),\n,$(PACKAGES_USERS))' > $(USERS_TABLE)
+	$(TOPDIR)/support/scripts/mkusers $(USERS_TABLE) $(TARGET_DIR) >> $(FAKEROOT_SCRIPT)
 	echo "$$(ROOTFS_$(2)_CMD)" >> $$(FAKEROOT_SCRIPT)
 	chmod a+x $$(FAKEROOT_SCRIPT)
 	$$(HOST_DIR)/usr/bin/fakeroot -- $$(FAKEROOT_SCRIPT)
diff --git a/package/pkg-generic.mk b/package/pkg-generic.mk
index 57b0fd0..f641766 100644
--- a/package/pkg-generic.mk
+++ b/package/pkg-generic.mk
@@ -529,6 +529,7 @@ ifeq ($$($$($(2)_KCONFIG_VAR)),y)
 TARGETS += $(1)
 PACKAGES_PERMISSIONS_TABLE += $$($(2)_PERMISSIONS)$$(sep)
 PACKAGES_DEVICES_TABLE += $$($(2)_DEVICES)$$(sep)
+PACKAGES_USERS += $$($(2)_USERS)$$(sep)
 
 ifeq ($$($(2)_SITE_METHOD),svn)
 DL_TOOLS_DEPENDENCIES += svn
diff --git a/support/scripts/mkusers b/support/scripts/mkusers
new file mode 100755
index 0000000..19aa085
--- /dev/null
+++ b/support/scripts/mkusers
@@ -0,0 +1,409 @@
+#!/bin/bash
+set -e
+myname="${0##*/}"
+
+#----------------------------------------------------------------------------
+# Configurable items
+MIN_UID=1000
+MAX_UID=1999
+MIN_GID=1000
+MAX_GID=1999
+# No more is configurable below this point
+#----------------------------------------------------------------------------
+
+#----------------------------------------------------------------------------
+error() {
+    local fmt="${1}"
+    shift
+
+    printf "%s: " "${myname}" >&2
+    printf "${fmt}" "${@}" >&2
+}
+fail() {
+    error "$@"
+    exit 1
+}
+
+#----------------------------------------------------------------------------
+if [ ${#} -ne 2 ]; then
+    fail "usage: %s USERS_TABLE TARGET_DIR\n"
+fi
+USERS_TABLE="${1}"
+TARGET_DIR="${2}"
+shift 2
+PASSWD="${TARGET_DIR}/etc/passwd"
+SHADOW="${TARGET_DIR}/etc/shadow"
+GROUP="${TARGET_DIR}/etc/group"
+# /etc/gshadow is not part of the standard skeleton, so not everybody
+# will have it, but some may hav it, and its content must be in sync
+# with /etc/group, so any use of gshadow must be conditional.
+GSHADOW="${TARGET_DIR}/etc/gshadow"
+
+# We can't simply source ${BUILDROOT_CONFIG} as it may contains constructs
+# such as:
+#    BR2_DEFCONFIG="$(CONFIG_DIR)/defconfig"
+# which when sourced from a shell script will eventually try to execute
+# a command name 'CONFIG_DIR', which is plain wrong for virtually every
+# systems out there.
+# So, we have to scan that file instead. Sigh... :-(
+PASSWD_METHOD="$( sed -r -e '/^BR2_TARGET_GENERIC_PASSWD_METHOD="(.*)"$/!d;'    \
+                         -e 's//\1/;'                                           \
+                         "${BUILDROOT_CONFIG}"                                  \
+                )"
+
+#----------------------------------------------------------------------------
+get_uid() {
+    local username="${1}"
+
+    awk -F: -v username="${username}"                           \
+        '$1 == username { printf( "%d\n", $3 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_ugid() {
+    local username="${1}"
+
+    awk -F:i -v username="${username}"                          \
+        '$1 == username { printf( "%d\n", $4 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_gid() {
+    local group="${1}"
+
+    awk -F: -v group="${group}"                             \
+        '$1 == group { printf( "%d\n", $3 ); }' "${GROUP}"
+}
+
+#----------------------------------------------------------------------------
+get_username() {
+    local uid="${1}"
+
+    awk -F: -v uid="${uid}"                                 \
+        '$3 == uid { printf( "%s\n", $1 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_group() {
+    local gid="${1}"
+
+    awk -F: -v gid="${gid}"                             \
+        '$3 == gid { printf( "%s\n", $1 ); }' "${GROUP}"
+}
+
+#----------------------------------------------------------------------------
+get_ugroup() {
+    local username="${1}"
+    local ugid
+
+    ugid="$( get_ugid "${username}" )"
+    if [ -n "${ugid}" ]; then
+        get_group "${ugid}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+# Sanity-check the new user/group:
+#   - check the gid is not already used for another group
+#   - check the group does not already exist with another gid
+#   - check the user does not already exist with another gid
+#   - check the uid is not already used for another user
+#   - check the user does not already exist with another uid
+#   - check the user does not already exist in another group
+check_user_validity() {
+    local username="${1}"
+    local uid="${2}"
+    local group="${3}"
+    local gid="${4}"
+    local _uid _ugid _gid _username _group _ugroup
+
+    _group="$( get_group "${gid}" )"
+    _gid="$( get_gid "${group}" )"
+    _ugid="$( get_ugid "${username}" )"
+    _username="$( get_username "${uid}" )"
+    _uid="$( get_uid "${username}" )"
+    _ugroup="$( get_ugroup "${username}" )"
+
+    if [ "${username}" = "root" ]; then
+        fail "invalid username '%s\n'" "${username}"
+    fi
+
+    if [ ${gid} -lt -1 -o ${gid} -eq 0 ]; then
+        fail "invalid gid '%d'\n" ${gid}
+    elif [ ${gid} -ne -1 ]; then
+        # check the gid is not already used for another group
+        if [ -n "${_group}" -a "${_group}" != "${group}" ]; then
+            fail "gid is already used by group '${_group}'\n"
+        fi
+
+        # check the group does not already exists with another gid
+        if [ -n "${_gid}" -a ${_gid} -ne ${gid} ]; then
+            fail "group already exists with gid '${_gid}'\n"
+        fi
+
+        # check the user does not already exists with another gid
+        if [ -n "${_ugid}" -a ${_ugid} -ne ${gid} ]; then
+            fail "user already exists with gid '${_ugid}'\n"
+        fi
+    fi
+
+    if [ ${uid} -lt -1 -o ${uid} -eq 0 ]; then
+        fail "invalid uid '%d'\n" ${uid}
+    elif [ ${uid} -ne -1 ]; then
+        # check the uid is not already used for another user
+        if [ -n "${_username}" -a "${_username}" != "${username}" ]; then
+            fail "uid is already used by user '${_username}'\n"
+        fi
+
+        # check the user does not already exists with another uid
+        if [ -n "${_uid}" -a ${_uid} -ne ${uid} ]; then
+            fail "user already exists with uid '${_uid}'\n"
+        fi
+    fi
+
+    # check the user does not already exist in another group
+    if [ -n "${_ugroup}" -a "${_ugroup}" != "${group}" ]; then
+        fail "user already exists with group '${_ugroup}'\n"
+    fi
+
+    return 0
+}
+
+#----------------------------------------------------------------------------
+# Generate a unique GID for given group. If the group already exists,
+# then simply report its current GID. Otherwise, generate the lowest GID
+# that is:
+#   - not 0
+#   - comprised in [MIN_GID..MAX_GID]
+#   - not already used by a group
+generate_gid() {
+    local group="${1}"
+    local gid
+
+    gid="$( get_gid "${group}" )"
+    if [ -z "${gid}" ]; then
+        for(( gid=MIN_GID; gid<=MAX_GID; gid++ )); do
+            if [ -z "$( get_group "${gid}" )" ]; then
+                break
+            fi
+        done
+        if [ ${gid} -gt ${MAX_GID} ]; then
+            fail "can not allocate a GID for group '%s'\n" "${group}"
+        fi
+    fi
+    printf "%d\n" "${gid}"
+}
+
+#----------------------------------------------------------------------------
+# Add a group; if it does already exist, remove it first
+add_one_group() {
+    local group="${1}"
+    local gid="${2}"
+    local _f
+
+    # Generate a new GID if needed
+    if [ ${gid} -eq -1 ]; then
+        gid="$( generate_gid "${group}" )"
+    fi
+
+    # Remove any previous instance of this group, and re-add the new one
+    sed -i -e '/^'"${group}"':.*/d;' "${GROUP}"
+    printf "%s:x:%d:\n" "${group}" "${gid}" >>"${GROUP}"
+
+    # Ditto for /etc/gshadow if it exists
+    if [ -f "${GSHADOW}" ]; then
+        sed -i -e '/^'"${group}"':.*/d;' "${GSHADOW}"
+        printf "%s:*::\n" "${group}" >>"${GSHADOW}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+# Generate a unique UID for given username. If the username already exists,
+# then simply report its current UID. Otherwise, generate the lowest UID
+# that is:
+#   - not 0
+#   - comprised in [MIN_UID..MAX_UID]
+#   - not already used by a user
+generate_uid() {
+    local username="${1}"
+    local uid
+
+    uid="$( get_uid "${username}" )"
+    if [ -z "${uid}" ]; then
+        for(( uid=MIN_UID; uid<=MAX_UID; uid++ )); do
+            if [ -z "$( get_username "${uid}" )" ]; then
+                break
+            fi
+        done
+        if [ ${uid} -gt ${MAX_UID} ]; then
+            fail "can not allocate a UID for user '%s'\n" "${username}"
+        fi
+    fi
+    printf "%d\n" "${uid}"
+}
+
+#----------------------------------------------------------------------------
+# Add given user to given group, if not already the case
+add_user_to_group() {
+    local username="${1}"
+    local group="${2}"
+    local _f
+
+    for _f in "${GROUP}" "${GSHADOW}"; do
+        [ -f "${_f}" ] || continue
+        sed -r -i -e 's/^('"${group}"':.*:)(([^:]+,)?)'"${username}"'(,[^:]+*)?$/\1\2\4/;'  \
+                  -e 's/^('"${group}"':.*)$/\1,'"${username}"'/;'                           \
+                  -e 's/,+/,/'                                                              \
+                  -e 's/:,/:/'                                                              \
+                  "${_f}"
+    done
+}
+
+#----------------------------------------------------------------------------
+# Encode a password
+encode_password() {
+    local passwd="${1}"
+
+    mkpasswd -m "${PASSWD_METHOD}" "${passwd}"
+}
+
+#----------------------------------------------------------------------------
+# Add a user; if it does already exist, remove it first
+add_one_user() {
+    local username="${1}"
+    local uid="${2}"
+    local group="${3}"
+    local gid="${4}"
+    local passwd="${5}"
+    local home="${6}"
+    local shell="${7}"
+    local groups="${8}"
+    local comment="${9}"
+    local _f _group _home _shell _gid _passwd
+
+    # First, sanity-check the user
+    check_user_validity "${username}" "${uid}" "${group}" "${gid}"
+
+    # Generate a new UID if needed
+    if [ ${uid} -eq -1 ]; then
+        uid="$( generate_uid "${username}" )"
+    fi
+
+    # Remove any previous instance of this user
+    for _f in "${PASSWD}" "${SHADOW}"; do
+        sed -r -i -e '/^'"${username}"':.*/d;' "${_f}"
+    done
+
+    _gid="$( get_gid "${group}" )"
+    _shell="${shell}"
+    if [ "${shell}" = "-" ]; then
+        _shell="/bin/false"
+    fi
+    case "${home}" in
+        -)  _home="/";;
+        /)  fail "home can not explicitly be '/'\n";;
+        /*) _home="${home}";;
+        *)  fail "home must be an absolute path\n";;
+    esac
+    case "${passwd}" in
+        !=*)
+            _passwd='!'"$( encode_passwd "${passwd#!=}" )"
+            ;;
+        =*)
+            _passwd="$( encode_passwd "${passwd#=}" )"
+            ;;
+        *)
+            _passwd="${passwd}"
+            ;;
+    esac
+
+    printf "%s:x:%d:%d:%s:%s:%s\n"              \
+           "${username}" "${uid}" "${_gid}"     \
+           "${comment}" "${_home}" "${_shell}"  \
+           >>"${PASSWD}"
+    printf "%s:%s:::::::\n"      \
+           "${username}" "${_passwd}"   \
+           >>"${SHADOW}"
+
+    # Add the user to its additional groups
+    if [ "${groups}" != "-" ]; then
+        for _group in ${groups//,/ }; do
+            add_user_to_group "${username}" "${_group}"
+        done
+    fi
+
+    # If the user has a home, chown it
+    # (Note: stdout goes to the fakeroot-script)
+    if [ "${home}" != "-" ]; then
+        mkdir -p "${TARGET_DIR}/${home}"
+        printf "chown -R %d:%d '%s'\n" "${uid}" "${_gid}" "${TARGET_DIR}/${home}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+main() {
+    local username uid group gid passwd home shell groups comment
+
+    # Some sanity checks
+    if [ ${MIN_UID} -le 0 ]; then
+        fail "MIN_UID must be >0 (currently %d)\n" ${MIN_UID}
+    fi
+    if [ ${MIN_GID} -le 0 ]; then
+        fail "MIN_GID must be >0 (currently %d)\n" ${MIN_GID}
+    fi
+
+    # We first create groups whose gid is not -1, and then we create groups
+    # whose gid is -1 (automatic), so that, if a group is defined both with
+    # a specified gid and an automatic gid, we ensure the specified gid is
+    # used, rather than a different automatic gid is computed.
+
+    # First, create all the main groups which gid is *not* automatic
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        [ ${gid} -ge 0     ] || continue    # Automatic gid
+        add_one_group "${group}" "${gid}"
+    done <"${USERS_TABLE}"
+
+    # Then, create all the main groups which gid *is* automatic
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        [ ${gid} -eq -1    ] || continue    # Non-automatic gid
+        add_one_group "${group}" "${gid}"
+    done <"${USERS_TABLE}"
+
+    # Then, create all the additional groups
+    # If any additional group is already a main group, we should use
+    # the gid of that main group; otherwise, we can use any gid
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        if [ "${groups}" != "-" ]; then
+            for g in ${groups//,/ }; do
+                add_one_group "${g}" -1
+            done
+        fi
+    done <"${USERS_TABLE}"
+
+    # When adding users, we do as for groups, in case two packages create
+    # the same user, one with an automatic uid, the other with a specified
+    # uid, to ensure the specified uid is used, rather than an incompatible
+    # uid be generated.
+
+    # Now, add users whose uid is *not* automatic
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        [ ${uid} -ge 0     ] || continue    # Automatic uid
+        add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
+                     "${home}" "${shell}" "${groups}" "${comment}"
+    done <"${USERS_TABLE}"
+
+    # Finally, add users whose uid *is* automatic
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        [ ${uid} -eq -1    ] || continue    # Non-automatic uid
+        add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
+                     "${home}" "${shell}" "${groups}" "${comment}"
+    done <"${USERS_TABLE}"
+}
+
+#----------------------------------------------------------------------------
+main "${@}"
-- 
1.7.2.5

^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-02-17 22:59 [Buildroot] [pull request v7 'next'] Pull request for branch yem-package-create-user Yann E. MORIN
@ 2013-02-17 22:59 ` Yann E. MORIN
  0 siblings, 0 replies; 27+ messages in thread
From: Yann E. MORIN @ 2013-02-17 22:59 UTC (permalink / raw)
  To: buildroot

Packages that install daemons may need those daemons to run as a non-root,
or an otherwise non-system (eg. 'daemon'), user.

Add infrastructure for packages to create users, by declaring the FOO_USERS
variable that contain a makedev-syntax-like description of the user(s) to
add.

Signed-off-by: "Yann E. MORIN" <yann.morin.1998@free.fr>
Cc: Samuel Martin <s.martin49@gmail.com>
Cc: Cam Hutchison <camh@xdna.net>
Cc: Thomas Petazzoni <thomas.petazzoni@free-electrons.com>
Acked-by: Arnout Vandecappelle (Essensium/Mind) <arnout@mind.be>
---
 docs/manual/adding-packages-generic.txt |   16 +-
 docs/manual/appendix.txt                |    1 +
 docs/manual/makeusers-syntax.txt        |   87 +++++++
 fs/common.mk                            |    3 +
 package/pkg-generic.mk                  |    1 +
 support/scripts/mkusers                 |  409 +++++++++++++++++++++++++++++++
 6 files changed, 515 insertions(+), 2 deletions(-)
 create mode 100644 docs/manual/makeusers-syntax.txt
 create mode 100755 support/scripts/mkusers

diff --git a/docs/manual/adding-packages-generic.txt b/docs/manual/adding-packages-generic.txt
index 78df24a..85c9e37 100644
--- a/docs/manual/adding-packages-generic.txt
+++ b/docs/manual/adding-packages-generic.txt
@@ -51,7 +51,11 @@ system is based on hand-written Makefiles or shell scripts.
 35:	/bin/foo  f  4755  0  0	 -  -  -  -  -
 36: endef
 37:
-38: $(eval $(generic-package))
+38: define LIBFOO_USERS
+39:	foo -1 libfoo -1 * - - - LibFoo daemon
+40: endef
+41:
+42: $(eval $(generic-package))
 --------------------------------
 
 The Makefile begins on line 6 to 10 with metadata information: the
@@ -138,7 +142,10 @@ On line 29..31, we define a device-node file used by this package
 On line 33..35, we define the permissions to set to specific files
 installed by this package (+LIBFOO_PERMISSIONS+).
 
-Finally, on line 37, we call the +generic-package+ function, which
+On lines 38..40, we define a user that is used by this package (eg.
+to run a daemon as non-root) (+LIBFOO_USERS+).
+
+Finally, on line 42, we call the +generic-package+ function, which
 generates, according to the variables defined previously, all the
 Makefile code necessary to make your package working.
 
@@ -305,6 +312,11 @@ information is (assuming the package name is +libfoo+) :
   You can find some documentation for this syntax in the xref:makedev-syntax[].
   This variable is optional.
 
+* +LIBFOO_USERS+ lists the users to create for this package, if it installs
+  a program you want to run as a specific user (eg. as a daemon, or as a
+  cron-job). The syntax is similar in spirit to the makedevs one, and is
+  described in the xref:makeuser-syntax[]. This variable is optional.
+
 * +LIBFOO_LICENSE+ defines the license (or licenses) under which the package
   is released.
   This name will appear in the manifest file produced by +make legal-info+.
diff --git a/docs/manual/appendix.txt b/docs/manual/appendix.txt
index 6f1e9f3..63b172b 100644
--- a/docs/manual/appendix.txt
+++ b/docs/manual/appendix.txt
@@ -4,6 +4,7 @@ Appendix
 ========
 
 include::makedev-syntax.txt[]
+include::makeusers-syntax.txt[]
 
 [[package-list]]
 Available packages
diff --git a/docs/manual/makeusers-syntax.txt b/docs/manual/makeusers-syntax.txt
new file mode 100644
index 0000000..2199654
--- /dev/null
+++ b/docs/manual/makeusers-syntax.txt
@@ -0,0 +1,87 @@
+// -*- mode:doc -*- ;
+
+[[makeuser-syntax]]
+Makeuser syntax documentation
+-----------------------------
+
+The syntax to create users is inspired by the makedev syntax, above, but
+is specific to Buildroot.
+
+The syntax for adding a user is a space-separated list of fields, one
+user per line; the fields are:
+
+|=================================================================
+|username |uid |group |gid |password |home |shell |groups |comment
+|=================================================================
+
+Where:
+
+- +username+ is the desired user name (aka login name) for the user.
+  It can not be +root+, and must be unique.
+- +uid+ is the desired UID for the user. It must be unique, and not
+  +0+. If set to +-1+, then a unique UID will be computed by Buildroot
+  in the range [1000...1999]
+- +group+ is the desired name for the user's main group. It can not
+  be +root+. If the group does not exist, it will be created.
+- +gid+ is the desired GID for the user's main group. It must be unique,
+  and not +0+. If set to +-1+, and the group does not already exist, then
+  a unique GID will be computed by Buildroot in the range [1000..1999]
+- +password+ is the crypt(3)-encoded password. If prefixed with +!+,
+  then login is disabled. If prefixed with +=+, then it is interpreted
+  as clear-text, and will be crypt-encoded (using MD5). If prefixed with
+  +!=+, then the password will be crypt-encoded (using MD5) and login
+  will be disabled. If set to +*+, then login is not allowed.
+- +home+ is the desired home directory for the user. If set to '-', no
+  home directory will be created, and the user's home will be +/+.
+  Explicitly setting +home+ to +/+ is not allowed.
+- +shell+ is the desired shell for the user. If set to +-+, then
+  +/bin/false+ is set as the user's shell.
+- +groups+ is the comma-separated list of additional groups the user
+  should be part of. If set to +-+, then the user will be a member of
+  no additional group. Missing groups will be created with an arbitrary
+  +gid+.
+- +comment+ (aka https://en.wikipedia.org/wiki/Gecos_field[GECOS]
+  field) is an almost-free-form text.
+
+There are a few restrictions on the content of each field:
+
+* except for +comment+, all fields are mandatory.
+* except for +comment+, fields may not contain spaces.
+* no field may contain a colon (+:+).
+
+If +home+ is not +-+, then the home directory, and all files below,
+will belong to the user and its main group.
+
+Examples:
+
+----
+foo -1 bar -1 !=blabla /home/foo /bin/sh alpha,bravo Foo user
+----
+
+This will create this user:
+
+- +username+ (aka login name) is: +foo+
+- +uid+ is computed by Buildroot
+- main +group+ is: +bar+
+- main group +gid+ is computed by Buildroot
+- clear-text +password+ is: +blabla+, will be crypt(3)-encoded, and login is disabled.
+- +home+ is: +/home/foo+
+- +shell+ is: +/bin/sh+
+- +foo+ is also a member of +groups+: +alpha+ and +bravo+
+- +comment+ is: +Foo user+
+
+----
+test 8000 wheel -1 = - /bin/sh - Test user
+----
+
+This will create this user:
+
+- +username+ (aka login name) is: +test+
+- +uid+ is : +8000+
+- main +group+ is: +wheel+
+- main group +gid+ is computed by Buildroot, and will use the value defined in the rootfs skeleton
+- +password+ is empty (aka no password).
+- +home+ is +/+ but will not belong to +test+
+- +shell+ is: +/bin/sh+
+- +test+ is not a member of any additional +groups+
+- +comment+ is: +Test user+
diff --git a/fs/common.mk b/fs/common.mk
index 8b5b2f2..a8279b1 100644
--- a/fs/common.mk
+++ b/fs/common.mk
@@ -35,6 +35,7 @@ FAKEROOT_SCRIPT = $(BUILD_DIR)/_fakeroot.fs
 FULL_DEVICE_TABLE = $(BUILD_DIR)/_device_table.txt
 ROOTFS_DEVICE_TABLES = $(call qstrip,$(BR2_ROOTFS_DEVICE_TABLE)) \
 	$(call qstrip,$(BR2_ROOTFS_STATIC_DEVICE_TABLE))
+USERS_TABLE = $(BUILD_DIR)/_users_table.txt
 
 define ROOTFS_TARGET_INTERNAL
 
@@ -55,6 +56,8 @@ endif
 	printf '$$(subst $$(sep),\n,$$(PACKAGES_PERMISSIONS_TABLE))' >> $$(FULL_DEVICE_TABLE)
 	echo "$$(HOST_DIR)/usr/bin/makedevs -d $$(FULL_DEVICE_TABLE) $$(TARGET_DIR)" >> $$(FAKEROOT_SCRIPT)
 endif
+	printf '$(subst $(sep),\n,$(PACKAGES_USERS))' > $(USERS_TABLE)
+	$(TOPDIR)/support/scripts/mkusers $(USERS_TABLE) $(TARGET_DIR) >> $(FAKEROOT_SCRIPT)
 	echo "$$(ROOTFS_$(2)_CMD)" >> $$(FAKEROOT_SCRIPT)
 	chmod a+x $$(FAKEROOT_SCRIPT)
 	$$(HOST_DIR)/usr/bin/fakeroot -- $$(FAKEROOT_SCRIPT)
diff --git a/package/pkg-generic.mk b/package/pkg-generic.mk
index 57b0fd0..f641766 100644
--- a/package/pkg-generic.mk
+++ b/package/pkg-generic.mk
@@ -529,6 +529,7 @@ ifeq ($$($$($(2)_KCONFIG_VAR)),y)
 TARGETS += $(1)
 PACKAGES_PERMISSIONS_TABLE += $$($(2)_PERMISSIONS)$$(sep)
 PACKAGES_DEVICES_TABLE += $$($(2)_DEVICES)$$(sep)
+PACKAGES_USERS += $$($(2)_USERS)$$(sep)
 
 ifeq ($$($(2)_SITE_METHOD),svn)
 DL_TOOLS_DEPENDENCIES += svn
diff --git a/support/scripts/mkusers b/support/scripts/mkusers
new file mode 100755
index 0000000..19aa085
--- /dev/null
+++ b/support/scripts/mkusers
@@ -0,0 +1,409 @@
+#!/bin/bash
+set -e
+myname="${0##*/}"
+
+#----------------------------------------------------------------------------
+# Configurable items
+MIN_UID=1000
+MAX_UID=1999
+MIN_GID=1000
+MAX_GID=1999
+# No more is configurable below this point
+#----------------------------------------------------------------------------
+
+#----------------------------------------------------------------------------
+error() {
+    local fmt="${1}"
+    shift
+
+    printf "%s: " "${myname}" >&2
+    printf "${fmt}" "${@}" >&2
+}
+fail() {
+    error "$@"
+    exit 1
+}
+
+#----------------------------------------------------------------------------
+if [ ${#} -ne 2 ]; then
+    fail "usage: %s USERS_TABLE TARGET_DIR\n"
+fi
+USERS_TABLE="${1}"
+TARGET_DIR="${2}"
+shift 2
+PASSWD="${TARGET_DIR}/etc/passwd"
+SHADOW="${TARGET_DIR}/etc/shadow"
+GROUP="${TARGET_DIR}/etc/group"
+# /etc/gshadow is not part of the standard skeleton, so not everybody
+# will have it, but some may hav it, and its content must be in sync
+# with /etc/group, so any use of gshadow must be conditional.
+GSHADOW="${TARGET_DIR}/etc/gshadow"
+
+# We can't simply source ${BUILDROOT_CONFIG} as it may contains constructs
+# such as:
+#    BR2_DEFCONFIG="$(CONFIG_DIR)/defconfig"
+# which when sourced from a shell script will eventually try to execute
+# a command name 'CONFIG_DIR', which is plain wrong for virtually every
+# systems out there.
+# So, we have to scan that file instead. Sigh... :-(
+PASSWD_METHOD="$( sed -r -e '/^BR2_TARGET_GENERIC_PASSWD_METHOD="(.*)"$/!d;'    \
+                         -e 's//\1/;'                                           \
+                         "${BUILDROOT_CONFIG}"                                  \
+                )"
+
+#----------------------------------------------------------------------------
+get_uid() {
+    local username="${1}"
+
+    awk -F: -v username="${username}"                           \
+        '$1 == username { printf( "%d\n", $3 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_ugid() {
+    local username="${1}"
+
+    awk -F:i -v username="${username}"                          \
+        '$1 == username { printf( "%d\n", $4 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_gid() {
+    local group="${1}"
+
+    awk -F: -v group="${group}"                             \
+        '$1 == group { printf( "%d\n", $3 ); }' "${GROUP}"
+}
+
+#----------------------------------------------------------------------------
+get_username() {
+    local uid="${1}"
+
+    awk -F: -v uid="${uid}"                                 \
+        '$3 == uid { printf( "%s\n", $1 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_group() {
+    local gid="${1}"
+
+    awk -F: -v gid="${gid}"                             \
+        '$3 == gid { printf( "%s\n", $1 ); }' "${GROUP}"
+}
+
+#----------------------------------------------------------------------------
+get_ugroup() {
+    local username="${1}"
+    local ugid
+
+    ugid="$( get_ugid "${username}" )"
+    if [ -n "${ugid}" ]; then
+        get_group "${ugid}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+# Sanity-check the new user/group:
+#   - check the gid is not already used for another group
+#   - check the group does not already exist with another gid
+#   - check the user does not already exist with another gid
+#   - check the uid is not already used for another user
+#   - check the user does not already exist with another uid
+#   - check the user does not already exist in another group
+check_user_validity() {
+    local username="${1}"
+    local uid="${2}"
+    local group="${3}"
+    local gid="${4}"
+    local _uid _ugid _gid _username _group _ugroup
+
+    _group="$( get_group "${gid}" )"
+    _gid="$( get_gid "${group}" )"
+    _ugid="$( get_ugid "${username}" )"
+    _username="$( get_username "${uid}" )"
+    _uid="$( get_uid "${username}" )"
+    _ugroup="$( get_ugroup "${username}" )"
+
+    if [ "${username}" = "root" ]; then
+        fail "invalid username '%s\n'" "${username}"
+    fi
+
+    if [ ${gid} -lt -1 -o ${gid} -eq 0 ]; then
+        fail "invalid gid '%d'\n" ${gid}
+    elif [ ${gid} -ne -1 ]; then
+        # check the gid is not already used for another group
+        if [ -n "${_group}" -a "${_group}" != "${group}" ]; then
+            fail "gid is already used by group '${_group}'\n"
+        fi
+
+        # check the group does not already exists with another gid
+        if [ -n "${_gid}" -a ${_gid} -ne ${gid} ]; then
+            fail "group already exists with gid '${_gid}'\n"
+        fi
+
+        # check the user does not already exists with another gid
+        if [ -n "${_ugid}" -a ${_ugid} -ne ${gid} ]; then
+            fail "user already exists with gid '${_ugid}'\n"
+        fi
+    fi
+
+    if [ ${uid} -lt -1 -o ${uid} -eq 0 ]; then
+        fail "invalid uid '%d'\n" ${uid}
+    elif [ ${uid} -ne -1 ]; then
+        # check the uid is not already used for another user
+        if [ -n "${_username}" -a "${_username}" != "${username}" ]; then
+            fail "uid is already used by user '${_username}'\n"
+        fi
+
+        # check the user does not already exists with another uid
+        if [ -n "${_uid}" -a ${_uid} -ne ${uid} ]; then
+            fail "user already exists with uid '${_uid}'\n"
+        fi
+    fi
+
+    # check the user does not already exist in another group
+    if [ -n "${_ugroup}" -a "${_ugroup}" != "${group}" ]; then
+        fail "user already exists with group '${_ugroup}'\n"
+    fi
+
+    return 0
+}
+
+#----------------------------------------------------------------------------
+# Generate a unique GID for given group. If the group already exists,
+# then simply report its current GID. Otherwise, generate the lowest GID
+# that is:
+#   - not 0
+#   - comprised in [MIN_GID..MAX_GID]
+#   - not already used by a group
+generate_gid() {
+    local group="${1}"
+    local gid
+
+    gid="$( get_gid "${group}" )"
+    if [ -z "${gid}" ]; then
+        for(( gid=MIN_GID; gid<=MAX_GID; gid++ )); do
+            if [ -z "$( get_group "${gid}" )" ]; then
+                break
+            fi
+        done
+        if [ ${gid} -gt ${MAX_GID} ]; then
+            fail "can not allocate a GID for group '%s'\n" "${group}"
+        fi
+    fi
+    printf "%d\n" "${gid}"
+}
+
+#----------------------------------------------------------------------------
+# Add a group; if it does already exist, remove it first
+add_one_group() {
+    local group="${1}"
+    local gid="${2}"
+    local _f
+
+    # Generate a new GID if needed
+    if [ ${gid} -eq -1 ]; then
+        gid="$( generate_gid "${group}" )"
+    fi
+
+    # Remove any previous instance of this group, and re-add the new one
+    sed -i -e '/^'"${group}"':.*/d;' "${GROUP}"
+    printf "%s:x:%d:\n" "${group}" "${gid}" >>"${GROUP}"
+
+    # Ditto for /etc/gshadow if it exists
+    if [ -f "${GSHADOW}" ]; then
+        sed -i -e '/^'"${group}"':.*/d;' "${GSHADOW}"
+        printf "%s:*::\n" "${group}" >>"${GSHADOW}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+# Generate a unique UID for given username. If the username already exists,
+# then simply report its current UID. Otherwise, generate the lowest UID
+# that is:
+#   - not 0
+#   - comprised in [MIN_UID..MAX_UID]
+#   - not already used by a user
+generate_uid() {
+    local username="${1}"
+    local uid
+
+    uid="$( get_uid "${username}" )"
+    if [ -z "${uid}" ]; then
+        for(( uid=MIN_UID; uid<=MAX_UID; uid++ )); do
+            if [ -z "$( get_username "${uid}" )" ]; then
+                break
+            fi
+        done
+        if [ ${uid} -gt ${MAX_UID} ]; then
+            fail "can not allocate a UID for user '%s'\n" "${username}"
+        fi
+    fi
+    printf "%d\n" "${uid}"
+}
+
+#----------------------------------------------------------------------------
+# Add given user to given group, if not already the case
+add_user_to_group() {
+    local username="${1}"
+    local group="${2}"
+    local _f
+
+    for _f in "${GROUP}" "${GSHADOW}"; do
+        [ -f "${_f}" ] || continue
+        sed -r -i -e 's/^('"${group}"':.*:)(([^:]+,)?)'"${username}"'(,[^:]+*)?$/\1\2\4/;'  \
+                  -e 's/^('"${group}"':.*)$/\1,'"${username}"'/;'                           \
+                  -e 's/,+/,/'                                                              \
+                  -e 's/:,/:/'                                                              \
+                  "${_f}"
+    done
+}
+
+#----------------------------------------------------------------------------
+# Encode a password
+encode_password() {
+    local passwd="${1}"
+
+    mkpasswd -m "${PASSWD_METHOD}" "${passwd}"
+}
+
+#----------------------------------------------------------------------------
+# Add a user; if it does already exist, remove it first
+add_one_user() {
+    local username="${1}"
+    local uid="${2}"
+    local group="${3}"
+    local gid="${4}"
+    local passwd="${5}"
+    local home="${6}"
+    local shell="${7}"
+    local groups="${8}"
+    local comment="${9}"
+    local _f _group _home _shell _gid _passwd
+
+    # First, sanity-check the user
+    check_user_validity "${username}" "${uid}" "${group}" "${gid}"
+
+    # Generate a new UID if needed
+    if [ ${uid} -eq -1 ]; then
+        uid="$( generate_uid "${username}" )"
+    fi
+
+    # Remove any previous instance of this user
+    for _f in "${PASSWD}" "${SHADOW}"; do
+        sed -r -i -e '/^'"${username}"':.*/d;' "${_f}"
+    done
+
+    _gid="$( get_gid "${group}" )"
+    _shell="${shell}"
+    if [ "${shell}" = "-" ]; then
+        _shell="/bin/false"
+    fi
+    case "${home}" in
+        -)  _home="/";;
+        /)  fail "home can not explicitly be '/'\n";;
+        /*) _home="${home}";;
+        *)  fail "home must be an absolute path\n";;
+    esac
+    case "${passwd}" in
+        !=*)
+            _passwd='!'"$( encode_passwd "${passwd#!=}" )"
+            ;;
+        =*)
+            _passwd="$( encode_passwd "${passwd#=}" )"
+            ;;
+        *)
+            _passwd="${passwd}"
+            ;;
+    esac
+
+    printf "%s:x:%d:%d:%s:%s:%s\n"              \
+           "${username}" "${uid}" "${_gid}"     \
+           "${comment}" "${_home}" "${_shell}"  \
+           >>"${PASSWD}"
+    printf "%s:%s:::::::\n"      \
+           "${username}" "${_passwd}"   \
+           >>"${SHADOW}"
+
+    # Add the user to its additional groups
+    if [ "${groups}" != "-" ]; then
+        for _group in ${groups//,/ }; do
+            add_user_to_group "${username}" "${_group}"
+        done
+    fi
+
+    # If the user has a home, chown it
+    # (Note: stdout goes to the fakeroot-script)
+    if [ "${home}" != "-" ]; then
+        mkdir -p "${TARGET_DIR}/${home}"
+        printf "chown -R %d:%d '%s'\n" "${uid}" "${_gid}" "${TARGET_DIR}/${home}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+main() {
+    local username uid group gid passwd home shell groups comment
+
+    # Some sanity checks
+    if [ ${MIN_UID} -le 0 ]; then
+        fail "MIN_UID must be >0 (currently %d)\n" ${MIN_UID}
+    fi
+    if [ ${MIN_GID} -le 0 ]; then
+        fail "MIN_GID must be >0 (currently %d)\n" ${MIN_GID}
+    fi
+
+    # We first create groups whose gid is not -1, and then we create groups
+    # whose gid is -1 (automatic), so that, if a group is defined both with
+    # a specified gid and an automatic gid, we ensure the specified gid is
+    # used, rather than a different automatic gid is computed.
+
+    # First, create all the main groups which gid is *not* automatic
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        [ ${gid} -ge 0     ] || continue    # Automatic gid
+        add_one_group "${group}" "${gid}"
+    done <"${USERS_TABLE}"
+
+    # Then, create all the main groups which gid *is* automatic
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        [ ${gid} -eq -1    ] || continue    # Non-automatic gid
+        add_one_group "${group}" "${gid}"
+    done <"${USERS_TABLE}"
+
+    # Then, create all the additional groups
+    # If any additional group is already a main group, we should use
+    # the gid of that main group; otherwise, we can use any gid
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        if [ "${groups}" != "-" ]; then
+            for g in ${groups//,/ }; do
+                add_one_group "${g}" -1
+            done
+        fi
+    done <"${USERS_TABLE}"
+
+    # When adding users, we do as for groups, in case two packages create
+    # the same user, one with an automatic uid, the other with a specified
+    # uid, to ensure the specified uid is used, rather than an incompatible
+    # uid be generated.
+
+    # Now, add users whose uid is *not* automatic
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        [ ${uid} -ge 0     ] || continue    # Automatic uid
+        add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
+                     "${home}" "${shell}" "${groups}" "${comment}"
+    done <"${USERS_TABLE}"
+
+    # Finally, add users whose uid *is* automatic
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        [ ${uid} -eq -1    ] || continue    # Non-automatic uid
+        add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
+                     "${home}" "${shell}" "${groups}" "${comment}"
+    done <"${USERS_TABLE}"
+}
+
+#----------------------------------------------------------------------------
+main "${@}"
-- 
1.7.2.5

^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-02-08 22:06 [Buildroot] [pull request v6] Pull request for branch yem-package-create-user Yann E. MORIN
@ 2013-02-08 22:06 ` Yann E. MORIN
  0 siblings, 0 replies; 27+ messages in thread
From: Yann E. MORIN @ 2013-02-08 22:06 UTC (permalink / raw)
  To: buildroot

Packages that install daemons may need those daemons to run as a non-root,
or an otherwise non-system (eg. 'daemon'), user.

Add infrastructure for packages to create users, by declaring the FOO_USERS
variable that contain a makedev-syntax-like description of the user(s) to
add.

Signed-off-by: "Yann E. MORIN" <yann.morin.1998@free.fr>
Cc: Samuel Martin <s.martin49@gmail.com>
Cc: Cam Hutchison <camh@xdna.net>
Cc: Thomas Petazzoni <thomas.petazzoni@free-electrons.com>
Acked-by: Arnout Vandecappelle (Essensium/Mind) <arnout@mind.be>
---
 docs/manual/adding-packages-generic.txt |   16 +-
 docs/manual/appendix.txt                |    1 +
 docs/manual/makeusers-syntax.txt        |   87 +++++++
 fs/common.mk                            |    3 +
 package/pkg-generic.mk                  |    1 +
 support/scripts/mkusers                 |  409 +++++++++++++++++++++++++++++++
 6 files changed, 515 insertions(+), 2 deletions(-)
 create mode 100644 docs/manual/makeusers-syntax.txt
 create mode 100755 support/scripts/mkusers

diff --git a/docs/manual/adding-packages-generic.txt b/docs/manual/adding-packages-generic.txt
index 78df24a..85c9e37 100644
--- a/docs/manual/adding-packages-generic.txt
+++ b/docs/manual/adding-packages-generic.txt
@@ -51,7 +51,11 @@ system is based on hand-written Makefiles or shell scripts.
 35:	/bin/foo  f  4755  0  0	 -  -  -  -  -
 36: endef
 37:
-38: $(eval $(generic-package))
+38: define LIBFOO_USERS
+39:	foo -1 libfoo -1 * - - - LibFoo daemon
+40: endef
+41:
+42: $(eval $(generic-package))
 --------------------------------
 
 The Makefile begins on line 6 to 10 with metadata information: the
@@ -138,7 +142,10 @@ On line 29..31, we define a device-node file used by this package
 On line 33..35, we define the permissions to set to specific files
 installed by this package (+LIBFOO_PERMISSIONS+).
 
-Finally, on line 37, we call the +generic-package+ function, which
+On lines 38..40, we define a user that is used by this package (eg.
+to run a daemon as non-root) (+LIBFOO_USERS+).
+
+Finally, on line 42, we call the +generic-package+ function, which
 generates, according to the variables defined previously, all the
 Makefile code necessary to make your package working.
 
@@ -305,6 +312,11 @@ information is (assuming the package name is +libfoo+) :
   You can find some documentation for this syntax in the xref:makedev-syntax[].
   This variable is optional.
 
+* +LIBFOO_USERS+ lists the users to create for this package, if it installs
+  a program you want to run as a specific user (eg. as a daemon, or as a
+  cron-job). The syntax is similar in spirit to the makedevs one, and is
+  described in the xref:makeuser-syntax[]. This variable is optional.
+
 * +LIBFOO_LICENSE+ defines the license (or licenses) under which the package
   is released.
   This name will appear in the manifest file produced by +make legal-info+.
diff --git a/docs/manual/appendix.txt b/docs/manual/appendix.txt
index 6f1e9f3..63b172b 100644
--- a/docs/manual/appendix.txt
+++ b/docs/manual/appendix.txt
@@ -4,6 +4,7 @@ Appendix
 ========
 
 include::makedev-syntax.txt[]
+include::makeusers-syntax.txt[]
 
 [[package-list]]
 Available packages
diff --git a/docs/manual/makeusers-syntax.txt b/docs/manual/makeusers-syntax.txt
new file mode 100644
index 0000000..2199654
--- /dev/null
+++ b/docs/manual/makeusers-syntax.txt
@@ -0,0 +1,87 @@
+// -*- mode:doc -*- ;
+
+[[makeuser-syntax]]
+Makeuser syntax documentation
+-----------------------------
+
+The syntax to create users is inspired by the makedev syntax, above, but
+is specific to Buildroot.
+
+The syntax for adding a user is a space-separated list of fields, one
+user per line; the fields are:
+
+|=================================================================
+|username |uid |group |gid |password |home |shell |groups |comment
+|=================================================================
+
+Where:
+
+- +username+ is the desired user name (aka login name) for the user.
+  It can not be +root+, and must be unique.
+- +uid+ is the desired UID for the user. It must be unique, and not
+  +0+. If set to +-1+, then a unique UID will be computed by Buildroot
+  in the range [1000...1999]
+- +group+ is the desired name for the user's main group. It can not
+  be +root+. If the group does not exist, it will be created.
+- +gid+ is the desired GID for the user's main group. It must be unique,
+  and not +0+. If set to +-1+, and the group does not already exist, then
+  a unique GID will be computed by Buildroot in the range [1000..1999]
+- +password+ is the crypt(3)-encoded password. If prefixed with +!+,
+  then login is disabled. If prefixed with +=+, then it is interpreted
+  as clear-text, and will be crypt-encoded (using MD5). If prefixed with
+  +!=+, then the password will be crypt-encoded (using MD5) and login
+  will be disabled. If set to +*+, then login is not allowed.
+- +home+ is the desired home directory for the user. If set to '-', no
+  home directory will be created, and the user's home will be +/+.
+  Explicitly setting +home+ to +/+ is not allowed.
+- +shell+ is the desired shell for the user. If set to +-+, then
+  +/bin/false+ is set as the user's shell.
+- +groups+ is the comma-separated list of additional groups the user
+  should be part of. If set to +-+, then the user will be a member of
+  no additional group. Missing groups will be created with an arbitrary
+  +gid+.
+- +comment+ (aka https://en.wikipedia.org/wiki/Gecos_field[GECOS]
+  field) is an almost-free-form text.
+
+There are a few restrictions on the content of each field:
+
+* except for +comment+, all fields are mandatory.
+* except for +comment+, fields may not contain spaces.
+* no field may contain a colon (+:+).
+
+If +home+ is not +-+, then the home directory, and all files below,
+will belong to the user and its main group.
+
+Examples:
+
+----
+foo -1 bar -1 !=blabla /home/foo /bin/sh alpha,bravo Foo user
+----
+
+This will create this user:
+
+- +username+ (aka login name) is: +foo+
+- +uid+ is computed by Buildroot
+- main +group+ is: +bar+
+- main group +gid+ is computed by Buildroot
+- clear-text +password+ is: +blabla+, will be crypt(3)-encoded, and login is disabled.
+- +home+ is: +/home/foo+
+- +shell+ is: +/bin/sh+
+- +foo+ is also a member of +groups+: +alpha+ and +bravo+
+- +comment+ is: +Foo user+
+
+----
+test 8000 wheel -1 = - /bin/sh - Test user
+----
+
+This will create this user:
+
+- +username+ (aka login name) is: +test+
+- +uid+ is : +8000+
+- main +group+ is: +wheel+
+- main group +gid+ is computed by Buildroot, and will use the value defined in the rootfs skeleton
+- +password+ is empty (aka no password).
+- +home+ is +/+ but will not belong to +test+
+- +shell+ is: +/bin/sh+
+- +test+ is not a member of any additional +groups+
+- +comment+ is: +Test user+
diff --git a/fs/common.mk b/fs/common.mk
index 8b5b2f2..a8279b1 100644
--- a/fs/common.mk
+++ b/fs/common.mk
@@ -35,6 +35,7 @@ FAKEROOT_SCRIPT = $(BUILD_DIR)/_fakeroot.fs
 FULL_DEVICE_TABLE = $(BUILD_DIR)/_device_table.txt
 ROOTFS_DEVICE_TABLES = $(call qstrip,$(BR2_ROOTFS_DEVICE_TABLE)) \
 	$(call qstrip,$(BR2_ROOTFS_STATIC_DEVICE_TABLE))
+USERS_TABLE = $(BUILD_DIR)/_users_table.txt
 
 define ROOTFS_TARGET_INTERNAL
 
@@ -55,6 +56,8 @@ endif
 	printf '$$(subst $$(sep),\n,$$(PACKAGES_PERMISSIONS_TABLE))' >> $$(FULL_DEVICE_TABLE)
 	echo "$$(HOST_DIR)/usr/bin/makedevs -d $$(FULL_DEVICE_TABLE) $$(TARGET_DIR)" >> $$(FAKEROOT_SCRIPT)
 endif
+	printf '$(subst $(sep),\n,$(PACKAGES_USERS))' > $(USERS_TABLE)
+	$(TOPDIR)/support/scripts/mkusers $(USERS_TABLE) $(TARGET_DIR) >> $(FAKEROOT_SCRIPT)
 	echo "$$(ROOTFS_$(2)_CMD)" >> $$(FAKEROOT_SCRIPT)
 	chmod a+x $$(FAKEROOT_SCRIPT)
 	$$(HOST_DIR)/usr/bin/fakeroot -- $$(FAKEROOT_SCRIPT)
diff --git a/package/pkg-generic.mk b/package/pkg-generic.mk
index 57b0fd0..f641766 100644
--- a/package/pkg-generic.mk
+++ b/package/pkg-generic.mk
@@ -529,6 +529,7 @@ ifeq ($$($$($(2)_KCONFIG_VAR)),y)
 TARGETS += $(1)
 PACKAGES_PERMISSIONS_TABLE += $$($(2)_PERMISSIONS)$$(sep)
 PACKAGES_DEVICES_TABLE += $$($(2)_DEVICES)$$(sep)
+PACKAGES_USERS += $$($(2)_USERS)$$(sep)
 
 ifeq ($$($(2)_SITE_METHOD),svn)
 DL_TOOLS_DEPENDENCIES += svn
diff --git a/support/scripts/mkusers b/support/scripts/mkusers
new file mode 100755
index 0000000..19aa085
--- /dev/null
+++ b/support/scripts/mkusers
@@ -0,0 +1,409 @@
+#!/bin/bash
+set -e
+myname="${0##*/}"
+
+#----------------------------------------------------------------------------
+# Configurable items
+MIN_UID=1000
+MAX_UID=1999
+MIN_GID=1000
+MAX_GID=1999
+# No more is configurable below this point
+#----------------------------------------------------------------------------
+
+#----------------------------------------------------------------------------
+error() {
+    local fmt="${1}"
+    shift
+
+    printf "%s: " "${myname}" >&2
+    printf "${fmt}" "${@}" >&2
+}
+fail() {
+    error "$@"
+    exit 1
+}
+
+#----------------------------------------------------------------------------
+if [ ${#} -ne 2 ]; then
+    fail "usage: %s USERS_TABLE TARGET_DIR\n"
+fi
+USERS_TABLE="${1}"
+TARGET_DIR="${2}"
+shift 2
+PASSWD="${TARGET_DIR}/etc/passwd"
+SHADOW="${TARGET_DIR}/etc/shadow"
+GROUP="${TARGET_DIR}/etc/group"
+# /etc/gshadow is not part of the standard skeleton, so not everybody
+# will have it, but some may hav it, and its content must be in sync
+# with /etc/group, so any use of gshadow must be conditional.
+GSHADOW="${TARGET_DIR}/etc/gshadow"
+
+# We can't simply source ${BUILDROOT_CONFIG} as it may contains constructs
+# such as:
+#    BR2_DEFCONFIG="$(CONFIG_DIR)/defconfig"
+# which when sourced from a shell script will eventually try to execute
+# a command name 'CONFIG_DIR', which is plain wrong for virtually every
+# systems out there.
+# So, we have to scan that file instead. Sigh... :-(
+PASSWD_METHOD="$( sed -r -e '/^BR2_TARGET_GENERIC_PASSWD_METHOD="(.*)"$/!d;'    \
+                         -e 's//\1/;'                                           \
+                         "${BUILDROOT_CONFIG}"                                  \
+                )"
+
+#----------------------------------------------------------------------------
+get_uid() {
+    local username="${1}"
+
+    awk -F: -v username="${username}"                           \
+        '$1 == username { printf( "%d\n", $3 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_ugid() {
+    local username="${1}"
+
+    awk -F:i -v username="${username}"                          \
+        '$1 == username { printf( "%d\n", $4 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_gid() {
+    local group="${1}"
+
+    awk -F: -v group="${group}"                             \
+        '$1 == group { printf( "%d\n", $3 ); }' "${GROUP}"
+}
+
+#----------------------------------------------------------------------------
+get_username() {
+    local uid="${1}"
+
+    awk -F: -v uid="${uid}"                                 \
+        '$3 == uid { printf( "%s\n", $1 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_group() {
+    local gid="${1}"
+
+    awk -F: -v gid="${gid}"                             \
+        '$3 == gid { printf( "%s\n", $1 ); }' "${GROUP}"
+}
+
+#----------------------------------------------------------------------------
+get_ugroup() {
+    local username="${1}"
+    local ugid
+
+    ugid="$( get_ugid "${username}" )"
+    if [ -n "${ugid}" ]; then
+        get_group "${ugid}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+# Sanity-check the new user/group:
+#   - check the gid is not already used for another group
+#   - check the group does not already exist with another gid
+#   - check the user does not already exist with another gid
+#   - check the uid is not already used for another user
+#   - check the user does not already exist with another uid
+#   - check the user does not already exist in another group
+check_user_validity() {
+    local username="${1}"
+    local uid="${2}"
+    local group="${3}"
+    local gid="${4}"
+    local _uid _ugid _gid _username _group _ugroup
+
+    _group="$( get_group "${gid}" )"
+    _gid="$( get_gid "${group}" )"
+    _ugid="$( get_ugid "${username}" )"
+    _username="$( get_username "${uid}" )"
+    _uid="$( get_uid "${username}" )"
+    _ugroup="$( get_ugroup "${username}" )"
+
+    if [ "${username}" = "root" ]; then
+        fail "invalid username '%s\n'" "${username}"
+    fi
+
+    if [ ${gid} -lt -1 -o ${gid} -eq 0 ]; then
+        fail "invalid gid '%d'\n" ${gid}
+    elif [ ${gid} -ne -1 ]; then
+        # check the gid is not already used for another group
+        if [ -n "${_group}" -a "${_group}" != "${group}" ]; then
+            fail "gid is already used by group '${_group}'\n"
+        fi
+
+        # check the group does not already exists with another gid
+        if [ -n "${_gid}" -a ${_gid} -ne ${gid} ]; then
+            fail "group already exists with gid '${_gid}'\n"
+        fi
+
+        # check the user does not already exists with another gid
+        if [ -n "${_ugid}" -a ${_ugid} -ne ${gid} ]; then
+            fail "user already exists with gid '${_ugid}'\n"
+        fi
+    fi
+
+    if [ ${uid} -lt -1 -o ${uid} -eq 0 ]; then
+        fail "invalid uid '%d'\n" ${uid}
+    elif [ ${uid} -ne -1 ]; then
+        # check the uid is not already used for another user
+        if [ -n "${_username}" -a "${_username}" != "${username}" ]; then
+            fail "uid is already used by user '${_username}'\n"
+        fi
+
+        # check the user does not already exists with another uid
+        if [ -n "${_uid}" -a ${_uid} -ne ${uid} ]; then
+            fail "user already exists with uid '${_uid}'\n"
+        fi
+    fi
+
+    # check the user does not already exist in another group
+    if [ -n "${_ugroup}" -a "${_ugroup}" != "${group}" ]; then
+        fail "user already exists with group '${_ugroup}'\n"
+    fi
+
+    return 0
+}
+
+#----------------------------------------------------------------------------
+# Generate a unique GID for given group. If the group already exists,
+# then simply report its current GID. Otherwise, generate the lowest GID
+# that is:
+#   - not 0
+#   - comprised in [MIN_GID..MAX_GID]
+#   - not already used by a group
+generate_gid() {
+    local group="${1}"
+    local gid
+
+    gid="$( get_gid "${group}" )"
+    if [ -z "${gid}" ]; then
+        for(( gid=MIN_GID; gid<=MAX_GID; gid++ )); do
+            if [ -z "$( get_group "${gid}" )" ]; then
+                break
+            fi
+        done
+        if [ ${gid} -gt ${MAX_GID} ]; then
+            fail "can not allocate a GID for group '%s'\n" "${group}"
+        fi
+    fi
+    printf "%d\n" "${gid}"
+}
+
+#----------------------------------------------------------------------------
+# Add a group; if it does already exist, remove it first
+add_one_group() {
+    local group="${1}"
+    local gid="${2}"
+    local _f
+
+    # Generate a new GID if needed
+    if [ ${gid} -eq -1 ]; then
+        gid="$( generate_gid "${group}" )"
+    fi
+
+    # Remove any previous instance of this group, and re-add the new one
+    sed -i -e '/^'"${group}"':.*/d;' "${GROUP}"
+    printf "%s:x:%d:\n" "${group}" "${gid}" >>"${GROUP}"
+
+    # Ditto for /etc/gshadow if it exists
+    if [ -f "${GSHADOW}" ]; then
+        sed -i -e '/^'"${group}"':.*/d;' "${GSHADOW}"
+        printf "%s:*::\n" "${group}" >>"${GSHADOW}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+# Generate a unique UID for given username. If the username already exists,
+# then simply report its current UID. Otherwise, generate the lowest UID
+# that is:
+#   - not 0
+#   - comprised in [MIN_UID..MAX_UID]
+#   - not already used by a user
+generate_uid() {
+    local username="${1}"
+    local uid
+
+    uid="$( get_uid "${username}" )"
+    if [ -z "${uid}" ]; then
+        for(( uid=MIN_UID; uid<=MAX_UID; uid++ )); do
+            if [ -z "$( get_username "${uid}" )" ]; then
+                break
+            fi
+        done
+        if [ ${uid} -gt ${MAX_UID} ]; then
+            fail "can not allocate a UID for user '%s'\n" "${username}"
+        fi
+    fi
+    printf "%d\n" "${uid}"
+}
+
+#----------------------------------------------------------------------------
+# Add given user to given group, if not already the case
+add_user_to_group() {
+    local username="${1}"
+    local group="${2}"
+    local _f
+
+    for _f in "${GROUP}" "${GSHADOW}"; do
+        [ -f "${_f}" ] || continue
+        sed -r -i -e 's/^('"${group}"':.*:)(([^:]+,)?)'"${username}"'(,[^:]+*)?$/\1\2\4/;'  \
+                  -e 's/^('"${group}"':.*)$/\1,'"${username}"'/;'                           \
+                  -e 's/,+/,/'                                                              \
+                  -e 's/:,/:/'                                                              \
+                  "${_f}"
+    done
+}
+
+#----------------------------------------------------------------------------
+# Encode a password
+encode_password() {
+    local passwd="${1}"
+
+    mkpasswd -m "${PASSWD_METHOD}" "${passwd}"
+}
+
+#----------------------------------------------------------------------------
+# Add a user; if it does already exist, remove it first
+add_one_user() {
+    local username="${1}"
+    local uid="${2}"
+    local group="${3}"
+    local gid="${4}"
+    local passwd="${5}"
+    local home="${6}"
+    local shell="${7}"
+    local groups="${8}"
+    local comment="${9}"
+    local _f _group _home _shell _gid _passwd
+
+    # First, sanity-check the user
+    check_user_validity "${username}" "${uid}" "${group}" "${gid}"
+
+    # Generate a new UID if needed
+    if [ ${uid} -eq -1 ]; then
+        uid="$( generate_uid "${username}" )"
+    fi
+
+    # Remove any previous instance of this user
+    for _f in "${PASSWD}" "${SHADOW}"; do
+        sed -r -i -e '/^'"${username}"':.*/d;' "${_f}"
+    done
+
+    _gid="$( get_gid "${group}" )"
+    _shell="${shell}"
+    if [ "${shell}" = "-" ]; then
+        _shell="/bin/false"
+    fi
+    case "${home}" in
+        -)  _home="/";;
+        /)  fail "home can not explicitly be '/'\n";;
+        /*) _home="${home}";;
+        *)  fail "home must be an absolute path\n";;
+    esac
+    case "${passwd}" in
+        !=*)
+            _passwd='!'"$( encode_passwd "${passwd#!=}" )"
+            ;;
+        =*)
+            _passwd="$( encode_passwd "${passwd#=}" )"
+            ;;
+        *)
+            _passwd="${passwd}"
+            ;;
+    esac
+
+    printf "%s:x:%d:%d:%s:%s:%s\n"              \
+           "${username}" "${uid}" "${_gid}"     \
+           "${comment}" "${_home}" "${_shell}"  \
+           >>"${PASSWD}"
+    printf "%s:%s:::::::\n"      \
+           "${username}" "${_passwd}"   \
+           >>"${SHADOW}"
+
+    # Add the user to its additional groups
+    if [ "${groups}" != "-" ]; then
+        for _group in ${groups//,/ }; do
+            add_user_to_group "${username}" "${_group}"
+        done
+    fi
+
+    # If the user has a home, chown it
+    # (Note: stdout goes to the fakeroot-script)
+    if [ "${home}" != "-" ]; then
+        mkdir -p "${TARGET_DIR}/${home}"
+        printf "chown -R %d:%d '%s'\n" "${uid}" "${_gid}" "${TARGET_DIR}/${home}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+main() {
+    local username uid group gid passwd home shell groups comment
+
+    # Some sanity checks
+    if [ ${MIN_UID} -le 0 ]; then
+        fail "MIN_UID must be >0 (currently %d)\n" ${MIN_UID}
+    fi
+    if [ ${MIN_GID} -le 0 ]; then
+        fail "MIN_GID must be >0 (currently %d)\n" ${MIN_GID}
+    fi
+
+    # We first create groups whose gid is not -1, and then we create groups
+    # whose gid is -1 (automatic), so that, if a group is defined both with
+    # a specified gid and an automatic gid, we ensure the specified gid is
+    # used, rather than a different automatic gid is computed.
+
+    # First, create all the main groups which gid is *not* automatic
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        [ ${gid} -ge 0     ] || continue    # Automatic gid
+        add_one_group "${group}" "${gid}"
+    done <"${USERS_TABLE}"
+
+    # Then, create all the main groups which gid *is* automatic
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        [ ${gid} -eq -1    ] || continue    # Non-automatic gid
+        add_one_group "${group}" "${gid}"
+    done <"${USERS_TABLE}"
+
+    # Then, create all the additional groups
+    # If any additional group is already a main group, we should use
+    # the gid of that main group; otherwise, we can use any gid
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        if [ "${groups}" != "-" ]; then
+            for g in ${groups//,/ }; do
+                add_one_group "${g}" -1
+            done
+        fi
+    done <"${USERS_TABLE}"
+
+    # When adding users, we do as for groups, in case two packages create
+    # the same user, one with an automatic uid, the other with a specified
+    # uid, to ensure the specified uid is used, rather than an incompatible
+    # uid be generated.
+
+    # Now, add users whose uid is *not* automatic
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        [ ${uid} -ge 0     ] || continue    # Automatic uid
+        add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
+                     "${home}" "${shell}" "${groups}" "${comment}"
+    done <"${USERS_TABLE}"
+
+    # Finally, add users whose uid *is* automatic
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        [ ${uid} -eq -1    ] || continue    # Non-automatic uid
+        add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
+                     "${home}" "${shell}" "${groups}" "${comment}"
+    done <"${USERS_TABLE}"
+}
+
+#----------------------------------------------------------------------------
+main "${@}"
-- 
1.7.2.5

^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-01-13 22:50 [Buildroot] [pull request v4] Pull request for branch yem-package-create-user Yann E. MORIN
@ 2013-01-13 22:50 ` Yann E. MORIN
  0 siblings, 0 replies; 27+ messages in thread
From: Yann E. MORIN @ 2013-01-13 22:50 UTC (permalink / raw)
  To: buildroot

Packages that install daemons may need those daemons to run as a non-root,
or an otherwise non-system (eg. 'daemon'), user.

Add infrastructure for packages to create users, by declaring the FOO_USERS
variable that contain a makedev-syntax-like description of the user(s) to
add.

Signed-off-by: "Yann E. MORIN" <yann.morin.1998@free.fr>
Cc: Samuel Martin <s.martin49@gmail.com>
Cc: Cam Hutchison <camh@xdna.net>
Cc: Thomas Petazzoni <thomas.petazzoni@free-electrons.com>
---
 docs/manual/adding-packages-generic.txt |   16 ++-
 docs/manual/appendix.txt                |    1 +
 docs/manual/makeusers-syntax.txt        |   87 +++++++
 fs/common.mk                            |    5 +-
 package/pkg-generic.mk                  |    1 +
 support/scripts/mkusers                 |  371 +++++++++++++++++++++++++++++++
 6 files changed, 478 insertions(+), 3 deletions(-)
 create mode 100644 docs/manual/makeusers-syntax.txt
 create mode 100755 support/scripts/mkusers

diff --git a/docs/manual/adding-packages-generic.txt b/docs/manual/adding-packages-generic.txt
index 7b8561a..19e5a32 100644
--- a/docs/manual/adding-packages-generic.txt
+++ b/docs/manual/adding-packages-generic.txt
@@ -50,7 +50,11 @@ system is based on hand-written Makefiles or shell scripts.
 34:	/bin/foo  f  4755  0  0	 -  -  -  -  -
 35: endef
 36:
-37: $(eval $(generic-package))
+37: define LIBFOO_USERS
+38:	foo -1 libfoo -1 * - - - LibFoo daemon
+39: endef
+40
+41: $(eval $(generic-package))
 --------------------------------
 
 The Makefile begins on line 6 to 10 with metadata information: the
@@ -95,7 +99,10 @@ On line 29..31, we define a device-node file used by this package
 On line 33..35, we define the permissions to set to specific files
 installed by this package (+LIBFOO_PERMISSIONS+).
 
-Finally, on line 37, we call the +generic-package+ function, which
+On lines 37..39, we define a user that is used by this package (eg.
+to run a daemon as non-root) (+LIBFOO_USERS+).
+
+Finally, on line 41, we call the +generic-package+ function, which
 generates, according to the variables defined previously, all the
 Makefile code necessary to make your package working.
 
@@ -255,6 +262,11 @@ information is (assuming the package name is +libfoo+) :
   You can find some documentation for this syntax in the xref:makedev-syntax[].
   This variable is optional.
 
+* +LIBFOO_USERS+ lists the users to create for this package, if it installs
+  a program you want to run as a specific user (eg. as a daemon, or as a
+  cron-job). The syntax is similar in spirit to the makedevs one, and is
+  described in the xref:makeuser-syntax[]. This variable is optional.
+
 * +LIBFOO_LICENSE+ defines the license (or licenses) under which the package
   is released.
   This name will appear in the manifest file produced by +make legal-info+.
diff --git a/docs/manual/appendix.txt b/docs/manual/appendix.txt
index 6f1e9f3..63b172b 100644
--- a/docs/manual/appendix.txt
+++ b/docs/manual/appendix.txt
@@ -4,6 +4,7 @@ Appendix
 ========
 
 include::makedev-syntax.txt[]
+include::makeusers-syntax.txt[]
 
 [[package-list]]
 Available packages
diff --git a/docs/manual/makeusers-syntax.txt b/docs/manual/makeusers-syntax.txt
new file mode 100644
index 0000000..2199654
--- /dev/null
+++ b/docs/manual/makeusers-syntax.txt
@@ -0,0 +1,87 @@
+// -*- mode:doc -*- ;
+
+[[makeuser-syntax]]
+Makeuser syntax documentation
+-----------------------------
+
+The syntax to create users is inspired by the makedev syntax, above, but
+is specific to Buildroot.
+
+The syntax for adding a user is a space-separated list of fields, one
+user per line; the fields are:
+
+|=================================================================
+|username |uid |group |gid |password |home |shell |groups |comment
+|=================================================================
+
+Where:
+
+- +username+ is the desired user name (aka login name) for the user.
+  It can not be +root+, and must be unique.
+- +uid+ is the desired UID for the user. It must be unique, and not
+  +0+. If set to +-1+, then a unique UID will be computed by Buildroot
+  in the range [1000...1999]
+- +group+ is the desired name for the user's main group. It can not
+  be +root+. If the group does not exist, it will be created.
+- +gid+ is the desired GID for the user's main group. It must be unique,
+  and not +0+. If set to +-1+, and the group does not already exist, then
+  a unique GID will be computed by Buildroot in the range [1000..1999]
+- +password+ is the crypt(3)-encoded password. If prefixed with +!+,
+  then login is disabled. If prefixed with +=+, then it is interpreted
+  as clear-text, and will be crypt-encoded (using MD5). If prefixed with
+  +!=+, then the password will be crypt-encoded (using MD5) and login
+  will be disabled. If set to +*+, then login is not allowed.
+- +home+ is the desired home directory for the user. If set to '-', no
+  home directory will be created, and the user's home will be +/+.
+  Explicitly setting +home+ to +/+ is not allowed.
+- +shell+ is the desired shell for the user. If set to +-+, then
+  +/bin/false+ is set as the user's shell.
+- +groups+ is the comma-separated list of additional groups the user
+  should be part of. If set to +-+, then the user will be a member of
+  no additional group. Missing groups will be created with an arbitrary
+  +gid+.
+- +comment+ (aka https://en.wikipedia.org/wiki/Gecos_field[GECOS]
+  field) is an almost-free-form text.
+
+There are a few restrictions on the content of each field:
+
+* except for +comment+, all fields are mandatory.
+* except for +comment+, fields may not contain spaces.
+* no field may contain a colon (+:+).
+
+If +home+ is not +-+, then the home directory, and all files below,
+will belong to the user and its main group.
+
+Examples:
+
+----
+foo -1 bar -1 !=blabla /home/foo /bin/sh alpha,bravo Foo user
+----
+
+This will create this user:
+
+- +username+ (aka login name) is: +foo+
+- +uid+ is computed by Buildroot
+- main +group+ is: +bar+
+- main group +gid+ is computed by Buildroot
+- clear-text +password+ is: +blabla+, will be crypt(3)-encoded, and login is disabled.
+- +home+ is: +/home/foo+
+- +shell+ is: +/bin/sh+
+- +foo+ is also a member of +groups+: +alpha+ and +bravo+
+- +comment+ is: +Foo user+
+
+----
+test 8000 wheel -1 = - /bin/sh - Test user
+----
+
+This will create this user:
+
+- +username+ (aka login name) is: +test+
+- +uid+ is : +8000+
+- main +group+ is: +wheel+
+- main group +gid+ is computed by Buildroot, and will use the value defined in the rootfs skeleton
+- +password+ is empty (aka no password).
+- +home+ is +/+ but will not belong to +test+
+- +shell+ is: +/bin/sh+
+- +test+ is not a member of any additional +groups+
+- +comment+ is: +Test user+
diff --git a/fs/common.mk b/fs/common.mk
index b1512dd..b5a7950 100644
--- a/fs/common.mk
+++ b/fs/common.mk
@@ -35,6 +35,7 @@ FAKEROOT_SCRIPT = $(BUILD_DIR)/_fakeroot.fs
 FULL_DEVICE_TABLE = $(BUILD_DIR)/_device_table.txt
 ROOTFS_DEVICE_TABLES = $(call qstrip,$(BR2_ROOTFS_DEVICE_TABLE)) \
 	$(call qstrip,$(BR2_ROOTFS_STATIC_DEVICE_TABLE))
+USERS_TABLE = $(BUILD_DIR)/_users_table.txt
 
 define ROOTFS_TARGET_INTERNAL
 
@@ -55,11 +56,13 @@ endif
 	printf '$(subst $(sep),\n,$(PACKAGES_PERMISSIONS_TABLE))' >> $(FULL_DEVICE_TABLE)
 	echo "$(HOST_DIR)/usr/bin/makedevs -d $(FULL_DEVICE_TABLE) $(TARGET_DIR)" >> $(FAKEROOT_SCRIPT)
 endif
+	printf '$(subst $(sep),\n,$(PACKAGES_USERS))' > $(USERS_TABLE)
+	$(TOPDIR)/support/scripts/mkusers $(USERS_TABLE) $(TARGET_DIR) >> $(FAKEROOT_SCRIPT)
 	echo "$(ROOTFS_$(2)_CMD)" >> $(FAKEROOT_SCRIPT)
 	chmod a+x $(FAKEROOT_SCRIPT)
 	$(HOST_DIR)/usr/bin/fakeroot -- $(FAKEROOT_SCRIPT)
 	cp support/misc/target-dir-warning.txt $(TARGET_DIR_WARNING_FILE)
-	- at rm -f $(FAKEROOT_SCRIPT) $(FULL_DEVICE_TABLE)
+	- at rm -f $(FAKEROOT_SCRIPT) $(FULL_DEVICE_TABLE) $(USERS_TABLE)
 	$(foreach hook,$(ROOTFS_$(2)_POST_GEN_HOOKS),$(call $(hook))$(sep))
 ifeq ($$(BR2_TARGET_ROOTFS_$(2)_GZIP),y)
 	gzip -9 -c $$@ > $$@.gz
diff --git a/package/pkg-generic.mk b/package/pkg-generic.mk
index 59de0f0..b39782a 100644
--- a/package/pkg-generic.mk
+++ b/package/pkg-generic.mk
@@ -517,6 +517,7 @@ ifeq ($$($$($(2)_KCONFIG_VAR)),y)
 TARGETS += $(1)
 PACKAGES_PERMISSIONS_TABLE += $$($(2)_PERMISSIONS)$$(sep)
 PACKAGES_DEVICES_TABLE += $$($(2)_DEVICES)$$(sep)
+PACKAGES_USERS += $$($(2)_USERS)$$(sep)
 
 ifeq ($$($(2)_SITE_METHOD),svn)
 DL_TOOLS_DEPENDENCIES += svn
diff --git a/support/scripts/mkusers b/support/scripts/mkusers
new file mode 100755
index 0000000..d98714c
--- /dev/null
+++ b/support/scripts/mkusers
@@ -0,0 +1,371 @@
+#!/bin/bash
+set -e
+myname="${0##*/}"
+
+#----------------------------------------------------------------------------
+# Configurable items
+MIN_UID=1000
+MAX_UID=1999
+MIN_GID=1000
+MAX_GID=1999
+# No more is configurable below this point
+#----------------------------------------------------------------------------
+
+#----------------------------------------------------------------------------
+error() {
+    local fmt="${1}"
+    shift
+
+    printf "%s: " "${myname}" >&2
+    printf "${fmt}" "${@}" >&2
+}
+fail() {
+    error "$@"
+    exit 1
+}
+
+#----------------------------------------------------------------------------
+if [ ${#} -ne 2 ]; then
+    fail "usage: %s USERS_TBLE TARGET_DIR\n"
+fi
+USERS_TABLE="${1}"
+TARGET_DIR="${2}"
+shift 2
+PASSWD="${TARGET_DIR}/etc/passwd"
+SHADOW="${TARGET_DIR}/etc/shadow"
+GROUP="${TARGET_DIR}/etc/group"
+# /etc/gsahdow is not part of the standard skeleton, so not everybody
+# will have it, but some may hav it, and its content must be in sync
+# with /etc/group, so any use of gshadow must be conditional.
+GSHADOW="${TARGET_DIR}/etc/gshadow"
+PASSWD_METHOD="$( sed -r -e '/^BR2_TARGET_GENERIC_PASSWD_METHOD="(.)*"$/!d' \
+                         -e 's//\1/;'                                       \
+                         "${BUILDROOT_CONFIG}"                              \
+                )"
+
+#----------------------------------------------------------------------------
+get_uid() {
+    local username="${1}"
+
+    awk -F: '$1=="'"${username}"'" { printf( "%d\n", $3 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_ugid() {
+    local username="${1}"
+
+    awk -F: '$1=="'"${username}"'" { printf( "%d\n", $4 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_gid() {
+    local group="${1}"
+
+    awk -F: '$1=="'"${group}"'" { printf( "%d\n", $3 ); }' "${GROUP}"
+}
+
+#----------------------------------------------------------------------------
+get_username() {
+    local uid="${1}"
+
+    awk -F: '$3=="'"${uid}"'" { printf( "%s\n", $1 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_group() {
+    local gid="${1}"
+
+    awk -F: '$3=="'"${gid}"'" { printf( "%s\n", $1 ); }' "${GROUP}"
+}
+
+#----------------------------------------------------------------------------
+get_ugroup() {
+    local username="${1}"
+    local ugid
+
+    ugid="$( get_ugid "${username}" )"
+    if [ -n "${ugid}" ]; then
+        get_group "${ugid}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+# Sanity-check the new user/group:
+#   - check the gid is not already used for another group
+#   - check the group does not already exist with another gid
+#   - check the user does not already exist with another gid
+#   - check the uid is not already used for another user
+#   - check the user does not already exist with another uid
+#   - check the user does not already exist in another group
+check_user_validity() {
+    local username="${1}"
+    local uid="${2}"
+    local group="${3}"
+    local gid="${4}"
+    local _uid _ugid _gid _username _group _ugroup
+
+    _group="$( get_group "${gid}" )"
+    _gid="$( get_gid "${group}" )"
+    _ugid="$( get_ugid "${username}" )"
+    _username="$( get_username "${uid}" )"
+    _uid="$( get_uid "${username}" )"
+    _ugroup="$( get_ugroup "${username}" )"
+
+    if [ "${username}" = "root" ]; then
+        fail "invalid username '%s\n'" "${username}"
+    fi
+
+    if [ ${gid} -lt -1 -o ${gid} -eq 0 ]; then
+        fail "invalid gid '%d'\n" ${gid}
+    elif [ ${gid} -ne -1 ]; then
+        # check the gid is not already used for another group
+        if [ -n "${_group}" -a "${_group}" != "${group}" ]; then
+            fail "gid is already used by group '${_group}'\n"
+        fi
+
+        # check the group does not already exists with another gid
+        if [ -n "${_gid}" -a ${_gid} -ne ${gid} ]; then
+            fail "group already exists with gid '${_gid}'\n"
+        fi
+
+        # check the user does not already exists with another gid
+        if [ -n "${_ugid}" -a ${_ugid} -ne ${gid} ]; then
+            fail "user already exists with gid '${_ugid}'\n"
+        fi
+    fi
+
+    if [ ${uid} -lt -1 -o ${uid} -eq 0 ]; then
+        fail "invalid uid '%d'\n" ${uid}
+    elif [ ${uid} -ne -1 ]; then
+        # check the uid is not already used for another user
+        if [ -n "${_username}" -a "${_username}" != "${username}" ]; then
+            fail "uid is already used by user '${_username}'\n"
+        fi
+
+        # check the user does not already exists with another uid
+        if [ -n "${_uid}" -a ${_uid} -ne ${uid} ]; then
+            fail "user already exists with uid '${_uid}'\n"
+        fi
+    fi
+
+    # check the user does not already exist in another group
+    if [ -n "${_ugroup}" -a "${_ugroup}" != "${group}" ]; then
+        fail "user already exists with group '${_ugroup}'\n"
+    fi
+
+    return 0
+}
+
+#----------------------------------------------------------------------------
+# Generate a unique GID for given group. If the group already exists,
+# then simply report its current GID. Otherwise, generate the lowest GID
+# that is:
+#   - not 0
+#   - comprised in [MIN_GID..MAX_GID]
+#   - not already used by a group
+generate_gid() {
+    local group="${1}"
+    local gid
+
+    gid="$( get_gid "${group}" )"
+    if [ -z "${gid}" ]; then
+        for(( gid=MIN_GID; gid<=MAX_GID; gid++ )); do
+            if [ -z "$( get_group "${gid}" )" ]; then
+                break
+            fi
+        done
+        if [ ${gid} -gt ${MAX_GID} ]; then
+            fail "can not allocate a GID for group '%s'\n" "${group}"
+        fi
+    fi
+    printf "%d\n" "${gid}"
+}
+
+#----------------------------------------------------------------------------
+# Add a group; if it does already exist, remove it first
+add_one_group() {
+    local group="${1}"
+    local gid="${2}"
+    local _f
+
+    # Generate a new GID if needed
+    if [ ${gid} -eq -1 ]; then
+        gid="$( generate_gid "${group}" )"
+    fi
+
+    # Remove any previous instance of this group
+    for _f in "${GROUP}" "${GSHADOW}"; do
+        [ -f "${_f}" ] || continue
+        sed -r -i -e '/^'"${group}"':.*/d;' "${_f}"
+    done
+
+    printf "%s:x:%d:\n" "${group}" "${gid}" >>"${GROUP}"
+    if [ -f "${GSHADOW}" ]; then
+        printf "%s:*::\n" "${group}" >>"${GSHADOW}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+# Generate a unique UID for given username. If the username already exists,
+# then simply report its current UID. Otherwise, generate the lowest UID
+# that is:
+#   - not 0
+#   - comprised in [MIN_UID..MAX_UID]
+#   - not already used by a user
+generate_uid() {
+    local username="${1}"
+    local uid
+
+    uid="$( get_uid "${username}" )"
+    if [ -z "${uid}" ]; then
+        for(( uid=MIN_UID; uid<=MAX_UID; uid++ )); do
+            if [ -z "$( get_username "${uid}" )" ]; then
+                break
+            fi
+        done
+        if [ ${uid} -gt ${MAX_UID} ]; then
+            fail "can not allocate a UID for user '%s'\n" "${username}"
+        fi
+    fi
+    printf "%d\n" "${uid}"
+}
+
+#----------------------------------------------------------------------------
+# Add given user to given group, if not already the case
+add_user_to_group() {
+    local username="${1}"
+    local group="${2}"
+    local _f
+
+    for _f in "${GROUP}" "${GSHADOW}"; do
+        [ -f "${_f}" ] || continue
+        sed -r -i -e 's/^('"${group}"':.*:)(([^:]+,)?)'"${username}"'(,[^:]+*)?$/\1\2\4/;'  \
+                  -e 's/^('"${group}"':.*)$/\1,'"${username}"'/;'                           \
+                  -e 's/,+/,/'                                                              \
+                  -e 's/:,/:/'                                                              \
+                  "${_f}"
+    done
+}
+
+#----------------------------------------------------------------------------
+# Encode a password
+encode_password() {
+    local passwd="${1}"
+
+    mkpasswd -m "${BR2_TARGET_GENERIC_PASSWD_METHOD}" "${passwd}"
+}
+
+#----------------------------------------------------------------------------
+# Add a user; if it does already exist, remove it first
+add_one_user() {
+    local username="${1}"
+    local uid="${2}"
+    local group="${3}"
+    local gid="${4}"
+    local passwd="${5}"
+    local home="${6}"
+    local shell="${7}"
+    local groups="${8}"
+    local comment="${9}"
+    local nb_days="$((($(date +%s)+(24*60*60-1))/(24*60*60)))"
+    local _f _group _home _shell _gid _passwd
+
+    # First, sanity-check the user
+    check_user_validity "${username}" "${uid}" "${group}" "${gid}"
+
+    # Generate a new UID if needed
+    if [ ${uid} -eq -1 ]; then
+        uid="$( generate_uid "${username}" )"
+    fi
+
+    # Remove any previous instance of this user
+    for _f in "${PASSWD}" "${SHADOW}"; do
+        sed -r -i -e '/^'"${username}"':.*/d;' "${_f}"
+    done
+
+    _gid="$( get_gid "${group}" )"
+    _shell="${shell}"
+    if [ "${shell}" = "-" ]; then
+        _shell="/bin/false"
+    fi
+    case "${home}" in
+        -)  _home="/";;
+        /)  fail "home can not explicitly be '/'\n";;
+        /*) _home="${home}";;
+        *)  fail "home must be an absolute path\n";;
+    esac
+    case "${passwd}" in
+        !=*)
+            _passwd='!'"$( encode_passwd "${passwd#!=}" )"
+            ;;
+        =*)
+            _passwd="$( encode_passwd "${passwd#=}" )"
+            ;;
+        *)
+            _passwd="${passwd}"
+            ;;
+    esac
+
+    printf "%s:x:%d:%d:%s:%s:%s\n"              \
+           "${username}" "${uid}" "${_gid}"     \
+           "${comment}" "${_home}" "${_shell}"  \
+           >>"${PASSWD}"
+    printf "%s:%s:%d:0:99999:7:::\n"                \
+           "${username}" "${_passwd}" "${nb_days}"  \
+           >>"${SHADOW}"
+
+    # Add the user to its additional groups
+    if [ "${groups}" != "-" ]; then
+        for _group in ${groups//,/ }; do
+            add_user_to_group "${username}" "${_group}"
+        done
+    fi
+
+    # If the user has a home, chown it
+    # (Note: stdout goes to the fakeroot-script)
+    if [ "${home}" != "-" ]; then
+        mkdir -p "${TARGET_DIR}/${home}"
+        printf "chown -R %d:%d '%s'\n" "${uid}" "${_gid}" "${TARGET_DIR}/${home}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+main() {
+    local username uid group gid passwd home shell groups comment
+
+    # Some sanity checks
+    if [ ${MIN_UID} -le 0 ]; then
+        fail "MIN_UID must be >0 (currently %d)\n" ${MIN_UID}
+    fi
+    if [ ${MIN_GID} -le 0 ]; then
+        fail "MIN_GID must be >0 (currently %d)\n" ${MIN_GID}
+    fi
+
+    # First, create all the main groups
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        add_one_group "${group}" "${gid}"
+    done <"${USERS_TABLE}"
+
+    # Then, create all the additional groups
+    # If any additional group is already a main group, we should use
+    # the gid of that main group; otherwise, we can use any gid
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        if [ "${groups}" != "-" ]; then
+            for g in ${groups//,/ }; do
+                add_one_group "${g}" -1
+            done
+        fi
+    done <"${USERS_TABLE}"
+
+    # Finally, add users
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
+                     "${home}" "${shell}" "${groups}" "${comment}"
+    done <"${USERS_TABLE}"
+}
+
+#----------------------------------------------------------------------------
+main "${@}"
-- 
1.7.2.5

^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-01-03 21:47 [Buildroot] [pull request v3] Pull request for branch yem-package-create-user Yann E. MORIN
@ 2013-01-03 21:47 ` Yann E. MORIN
  0 siblings, 0 replies; 27+ messages in thread
From: Yann E. MORIN @ 2013-01-03 21:47 UTC (permalink / raw)
  To: buildroot

Packages that install daemons may need those daemons to run as a non-root,
or an otherwise non-system (eg. 'daemon'), user.

Add infrastructure for packages to create users, by declaring the
FOO_USERS variable that contain a makedev-syntax-like description
of the user(s) to add.

Signed-off-by: "Yann E. MORIN" <yann.morin.1998@free.fr>
Cc: Samuel Martin <s.martin49@gmail.com>
Cc: Cam Hutchison <camh@xdna.net>
Cc: Thomas Petazzoni <thomas.petazzoni@free-electrons.com>
---
 docs/manual/adding-packages-generic.txt |   16 ++-
 docs/manual/appendix.txt                |    1 +
 docs/manual/makeusers-syntax.txt        |   87 ++++++++
 fs/common.mk                            |    5 +-
 package/pkg-generic.mk                  |    1 +
 support/scripts/mkusers                 |  359 +++++++++++++++++++++++++++++++
 6 files changed, 466 insertions(+), 3 deletions(-)
 create mode 100644 docs/manual/makeusers-syntax.txt
 create mode 100755 support/scripts/mkusers

diff --git a/docs/manual/adding-packages-generic.txt b/docs/manual/adding-packages-generic.txt
index 0759d4f..42897e9 100644
--- a/docs/manual/adding-packages-generic.txt
+++ b/docs/manual/adding-packages-generic.txt
@@ -50,7 +50,11 @@ system is based on hand-written Makefiles or shell scripts.
 34:	/bin/foo  f  4755  0  0	 -  -  -  -  -
 35: endef
 36:
-37: $(eval $(generic-package))
+37: define LIBFOO_USERS
+38: foo -1 libfoo -1 * - - - LibFoo daemon
+39: endef
+40
+41: $(eval $(generic-package))
 --------------------------------
 
 The Makefile begins on line 6 to 10 with metadata information: the
@@ -95,7 +99,10 @@ On line 29..31, we define a device-node file used by this package
 On line 33..35, we define the permissions to set to specific files
 installed by this package (+LIBFOO_PERMISSIONS+).
 
-Finally, on line 37, we call the +generic-package+ function, which
+On lines 37..39, we define a user that is used by this package (eg.
+to run a daemon as non-root).
+
+Finally, on line 41, we call the +generic-package+ function, which
 generates, according to the variables defined previously, all the
 Makefile code necessary to make your package working.
 
@@ -252,6 +259,11 @@ information is (assuming the package name is +libfoo+) :
   You can find some documentation for this syntax in the xref:makedev-syntax[].
   This variable is optional.
 
+* +LIBFOO_USERS+ lists the users to create for this package, if it installs
+  a program you want to run as a specific user (eg. as a daemon, or as a
+  cron-job). The syntax is similar in spirit to the makedevs one, and is
+  described in the xref:makeuser-syntax[]. This variable is optional.
+
 * +LIBFOO_LICENSE+ defines the license (or licenses) under which the package
   is released.
   This name will appear in the manifest file produced by +make legal-info+.
diff --git a/docs/manual/appendix.txt b/docs/manual/appendix.txt
index 6f1e9f3..63b172b 100644
--- a/docs/manual/appendix.txt
+++ b/docs/manual/appendix.txt
@@ -4,6 +4,7 @@ Appendix
 ========
 
 include::makedev-syntax.txt[]
+include::makeusers-syntax.txt[]
 
 [[package-list]]
 Available packages
diff --git a/docs/manual/makeusers-syntax.txt b/docs/manual/makeusers-syntax.txt
new file mode 100644
index 0000000..2199654
--- /dev/null
+++ b/docs/manual/makeusers-syntax.txt
@@ -0,0 +1,87 @@
+// -*- mode:doc -*- ;
+
+[[makeuser-syntax]]
+Makeuser syntax documentation
+-----------------------------
+
+The syntax to create users is inspired by the makedev syntax, above, but
+is specific to Buildroot.
+
+The syntax for adding a user is a space-separated list of fields, one
+user per line; the fields are:
+
+|=================================================================
+|username |uid |group |gid |password |home |shell |groups |comment
+|=================================================================
+
+Where:
+
+- +username+ is the desired user name (aka login name) for the user.
+  It can not be +root+, and must be unique.
+- +uid+ is the desired UID for the user. It must be unique, and not
+  +0+. If set to +-1+, then a unique UID will be computed by Buildroot
+  in the range [1000...1999]
+- +group+ is the desired name for the user's main group. It can not
+  be +root+. If the group does not exist, it will be created.
+- +gid+ is the desired GID for the user's main group. It must be unique,
+  and not +0+. If set to +-1+, and the group does not already exist, then
+  a unique GID will be computed by Buildroot in the range [1000..1999]
+- +password+ is the crypt(3)-encoded password. If prefixed with +!+,
+  then login is disabled. If prefixed with +=+, then it is interpreted
+  as clear-text, and will be crypt-encoded (using MD5). If prefixed with
+  +!=+, then the password will be crypt-encoded (using MD5) and login
+  will be disabled. If set to +*+, then login is not allowed.
+- +home+ is the desired home directory for the user. If set to '-', no
+  home directory will be created, and the user's home will be +/+.
+  Explicitly setting +home+ to +/+ is not allowed.
+- +shell+ is the desired shell for the user. If set to +-+, then
+  +/bin/false+ is set as the user's shell.
+- +groups+ is the comma-separated list of additional groups the user
+  should be part of. If set to +-+, then the user will be a member of
+  no additional group. Missing groups will be created with an arbitrary
+  +gid+.
+- +comment+ (aka https://en.wikipedia.org/wiki/Gecos_field[GECOS]
+  field) is an almost-free-form text.
+
+There are a few restrictions on the content of each field:
+
+* except for +comment+, all fields are mandatory.
+* except for +comment+, fields may not contain spaces.
+* no field may contain a colon (+:+).
+
+If +home+ is not +-+, then the home directory, and all files below,
+will belong to the user and its main group.
+
+Examples:
+
+----
+foo -1 bar -1 !=blabla /home/foo /bin/sh alpha,bravo Foo user
+----
+
+This will create this user:
+
+- +username+ (aka login name) is: +foo+
+- +uid+ is computed by Buildroot
+- main +group+ is: +bar+
+- main group +gid+ is computed by Buildroot
+- clear-text +password+ is: +blabla+, will be crypt(3)-encoded, and login is disabled.
+- +home+ is: +/home/foo+
+- +shell+ is: +/bin/sh+
+- +foo+ is also a member of +groups+: +alpha+ and +bravo+
+- +comment+ is: +Foo user+
+
+----
+test 8000 wheel -1 = - /bin/sh - Test user
+----
+
+This will create this user:
+
+- +username+ (aka login name) is: +test+
+- +uid+ is : +8000+
+- main +group+ is: +wheel+
+- main group +gid+ is computed by Buildroot, and will use the value defined in the rootfs skeleton
+- +password+ is empty (aka no password).
+- +home+ is +/+ but will not belong to +test+
+- +shell+ is: +/bin/sh+
+- +test+ is not a member of any additional +groups+
+- +comment+ is: +Test user+
diff --git a/fs/common.mk b/fs/common.mk
index b1512dd..b5a7950 100644
--- a/fs/common.mk
+++ b/fs/common.mk
@@ -35,6 +35,7 @@ FAKEROOT_SCRIPT = $(BUILD_DIR)/_fakeroot.fs
 FULL_DEVICE_TABLE = $(BUILD_DIR)/_device_table.txt
 ROOTFS_DEVICE_TABLES = $(call qstrip,$(BR2_ROOTFS_DEVICE_TABLE)) \
 	$(call qstrip,$(BR2_ROOTFS_STATIC_DEVICE_TABLE))
+USERS_TABLE = $(BUILD_DIR)/_users_table.txt
 
 define ROOTFS_TARGET_INTERNAL
 
@@ -55,11 +56,13 @@ endif
 	printf '$(subst $(sep),\n,$(PACKAGES_PERMISSIONS_TABLE))' >> $(FULL_DEVICE_TABLE)
 	echo "$(HOST_DIR)/usr/bin/makedevs -d $(FULL_DEVICE_TABLE) $(TARGET_DIR)" >> $(FAKEROOT_SCRIPT)
 endif
+	printf '$(subst $(sep),\n,$(PACKAGES_USERS))' > $(USERS_TABLE)
+	$(TOPDIR)/support/scripts/mkusers $(USERS_TABLE) $(TARGET_DIR) >> $(FAKEROOT_SCRIPT)
 	echo "$(ROOTFS_$(2)_CMD)" >> $(FAKEROOT_SCRIPT)
 	chmod a+x $(FAKEROOT_SCRIPT)
 	$(HOST_DIR)/usr/bin/fakeroot -- $(FAKEROOT_SCRIPT)
 	cp support/misc/target-dir-warning.txt $(TARGET_DIR_WARNING_FILE)
-	- at rm -f $(FAKEROOT_SCRIPT) $(FULL_DEVICE_TABLE)
+	- at rm -f $(FAKEROOT_SCRIPT) $(FULL_DEVICE_TABLE) $(USERS_TABLE)
 	$(foreach hook,$(ROOTFS_$(2)_POST_GEN_HOOKS),$(call $(hook))$(sep))
 ifeq ($$(BR2_TARGET_ROOTFS_$(2)_GZIP),y)
 	gzip -9 -c $$@ > $$@.gz
diff --git a/package/pkg-generic.mk b/package/pkg-generic.mk
index a570ad7..871544c 100644
--- a/package/pkg-generic.mk
+++ b/package/pkg-generic.mk
@@ -510,6 +510,7 @@ ifeq ($$($$($(2)_KCONFIG_VAR)),y)
 TARGETS += $(1)
 PACKAGES_PERMISSIONS_TABLE += $$($(2)_PERMISSIONS)$$(sep)
 PACKAGES_DEVICES_TABLE += $$($(2)_DEVICES)$$(sep)
+PACKAGES_USERS += $$($(2)_USERS)$$(sep)
 
 ifeq ($$($(2)_SITE_METHOD),svn)
 DL_TOOLS_DEPENDENCIES += svn
diff --git a/support/scripts/mkusers b/support/scripts/mkusers
new file mode 100755
index 0000000..5df3493
--- /dev/null
+++ b/support/scripts/mkusers
@@ -0,0 +1,359 @@
+#!/bin/bash
+set -e
+myname="${0##*/}"
+
+#----------------------------------------------------------------------------
+# Configurable items
+MIN_UID=1000
+MAX_UID=1999
+MIN_GID=1000
+MAX_GID=1999
+# No more is configurable below this point
+#----------------------------------------------------------------------------
+
+#----------------------------------------------------------------------------
+error() {
+    local fmt="${1}"
+    shift
+
+    printf "%s: " "${myname}" >&2
+    printf "${fmt}" "${@}" >&2
+}
+fail() {
+    error "$@"
+    exit 1
+}
+
+#----------------------------------------------------------------------------
+if [ ${#} -ne 2 ]; then
+    fail "usage: %s USERS_TBLE TARGET_DIR\n"
+fi
+USERS_TABLE="${1}"
+TARGET_DIR="${2}"
+shift 2
+PASSWD="${TARGET_DIR}/etc/passwd"
+SHADOW="${TARGET_DIR}/etc/shadow"
+GROUP="${TARGET_DIR}/etc/group"
+# /etc/gsahdow is not part of the standard skeleton, so not everybody
+# will have it, but some may hav it, and its content must be in sync
+# with /etc/group, so any use of gshadow must be conditional.
+GSHADOW="${TARGET_DIR}/etc/gshadow"
+
+#----------------------------------------------------------------------------
+get_uid() {
+    local username="${1}"
+
+    awk -F: '$1=="'"${username}"'" { printf( "%d\n", $3 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_ugid() {
+    local username="${1}"
+
+    awk -F: '$1=="'"${username}"'" { printf( "%d\n", $4 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_gid() {
+    local group="${1}"
+
+    awk -F: '$1=="'"${group}"'" { printf( "%d\n", $3 ); }' "${GROUP}"
+}
+
+#----------------------------------------------------------------------------
+get_username() {
+    local uid="${1}"
+
+    awk -F: '$3=="'"${uid}"'" { printf( "%s\n", $1 ); }' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_group() {
+    local gid="${1}"
+
+    awk -F: '$3=="'"${gid}"'" { printf( "%s\n", $1 ); }' "${GROUP}"
+}
+
+#----------------------------------------------------------------------------
+get_ugroup() {
+    local username="${1}"
+    local ugid
+
+    ugid="$( get_ugid "${username}" )"
+    if [ -n "${ugid}" ]; then
+        get_group "${ugid}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+# Sanity-check the new user/group:
+#   - check the gid is not already used for another group
+#   - check the group does not already exist with another gid
+#   - check the user does not already exist with another gid
+#   - check the uid is not already used for another user
+#   - check the user does not already exist with another uid
+#   - check the user does not already exist in another group
+check_user_validity() {
+    local username="${1}"
+    local uid="${2}"
+    local group="${3}"
+    local gid="${4}"
+    local _uid _ugid _gid _username _group _ugroup
+
+    _group="$( get_group "${gid}" )"
+    _gid="$( get_gid "${group}" )"
+    _ugid="$( get_ugid "${username}" )"
+    _username="$( get_username "${uid}" )"
+    _uid="$( get_uid "${username}" )"
+    _ugroup="$( get_ugroup "${username}" )"
+
+    if [ "${username}" = "root" ]; then
+        fail "invalid username '%s\n'" "${username}"
+    fi
+
+    if [ ${gid} -lt -1 -o ${gid} -eq 0 ]; then
+        fail "invalid gid '%d'\n" ${gid}
+    elif [ ${gid} -ne -1 ]; then
+        # check the gid is not already used for another group
+        if [ -n "${_group}" -a "${_group}" != "${group}" ]; then
+            fail "gid is already used by group '${_group}'\n"
+        fi
+
+        # check the group does not already exists with another gid
+        if [ -n "${_gid}" -a ${_gid} -ne ${gid} ]; then
+            fail "group already exists with gid '${_gid}'\n"
+        fi
+
+        # check the user does not already exists with another gid
+        if [ -n "${_ugid}" -a ${_ugid} -ne ${gid} ]; then
+            fail "user already exists with gid '${_ugid}'\n"
+        fi
+    fi
+
+    if [ ${uid} -lt -1 -o ${uid} -eq 0 ]; then
+        fail "invalid uid '%d'\n" ${uid}
+    elif [ ${uid} -ne -1 ]; then
+        # check the uid is not already used for another user
+        if [ -n "${_username}" -a "${_username}" != "${username}" ]; then
+            fail "uid is already used by user '${_username}'\n"
+        fi
+
+        # check the user does not already exists with another uid
+        if [ -n "${_uid}" -a ${_uid} -ne ${uid} ]; then
+            fail "user already exists with uid '${_uid}'\n"
+        fi
+    fi
+
+    # check the user does not already exist in another group
+    if [ -n "${_ugroup}" -a "${_ugroup}" != "${group}" ]; then
+        fail "user already exists with group '${_ugroup}'\n"
+    fi
+
+    return 0
+}
+
+#----------------------------------------------------------------------------
+# Generate a unique GID for given group. If the group already exists,
+# then simply report its current GID. Otherwise, generate the lowest GID
+# that is:
+#   - not 0
+#   - comprised in [MIN_GID..MAX_GID]
+#   - not already used by a group
+generate_gid() {
+    local group="${1}"
+    local gid
+
+    gid="$( get_gid "${group}" )"
+    if [ -z "${gid}" ]; then
+        for(( gid=MIN_GID; gid<=MAX_GID; gid++ )); do
+            if [ -z "$( get_group "${gid}" )" ]; then
+                break
+            fi
+        done
+        if [ ${gid} -gt ${MAX_GID} ]; then
+            fail "can not allocate a GID for group '%s'\n" "${group}"
+        fi
+    fi
+    printf "%d\n" "${gid}"
+}
+
+#----------------------------------------------------------------------------
+# Add a group; if it does already exist, remove it first
+add_one_group() {
+    local group="${1}"
+    local gid="${2}"
+    local _f
+
+    # Generate a new GID if needed
+    if [ ${gid} -eq -1 ]; then
+        gid="$( generate_gid "${group}" )"
+    fi
+
+    # Remove any previous instance of this group
+    for _f in "${GROUP}" "${GSHADOW}"; do
+        [ -f "${_f}" ] || continue
+        sed -r -i -e '/^'"${group}"':.*/d;' "${_f}"
+    done
+
+    printf "%s:x:%d:\n" "${group}" "${gid}" >>"${GROUP}"
+    if [ -f "${GSHADOW}" ]; then
+        printf "%s:*::\n" "${group}" >>"${GSHADOW}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+# Generate a unique UID for given username. If the username already exists,
+# then simply report its current UID. Otherwise, generate the lowest UID
+# that is:
+#   - not 0
+#   - comprised in [MIN_UID..MAX_UID]
+#   - not already used by a user
+generate_uid() {
+    local username="${1}"
+    local uid
+
+    uid="$( get_uid "${username}" )"
+    if [ -z "${uid}" ]; then
+        for(( uid=MIN_UID; uid<=MAX_UID; uid++ )); do
+            if [ -z "$( get_username "${uid}" )" ]; then
+                break
+            fi
+        done
+        if [ ${uid} -gt ${MAX_UID} ]; then
+            fail "can not allocate a UID for user '%s'\n" "${username}"
+        fi
+    fi
+    printf "%d\n" "${uid}"
+}
+
+#----------------------------------------------------------------------------
+# Add given user to given group, if not already the case
+add_user_to_group() {
+    local username="${1}"
+    local group="${2}"
+    local _f
+
+    for _f in "${GROUP}" "${GSHADOW}"; do
+        [ -f "${_f}" ] || continue
+        sed -r -i -e 's/^('"${group}"':.*:)(([^:]+,)?)'"${username}"'(,[^:]+*)?$/\1\2\4/;'  \
+                  -e 's/^('"${group}"':.*)$/\1,'"${username}"'/;'                           \
+                  -e 's/,+/,/'                                                              \
+                  -e 's/:,/:/'                                                              \
+                  "${_f}"
+    done
+}
+
+#----------------------------------------------------------------------------
+# Add a user; if it does already exist, remove it first
+add_one_user() {
+    local username="${1}"
+    local uid="${2}"
+    local group="${3}"
+    local gid="${4}"
+    local passwd="${5}"
+    local home="${6}"
+    local shell="${7}"
+    local groups="${8}"
+    local comment="${9}"
+    local nb_days="$((($(date +%s)+(24*60*60-1))/(24*60*60)))"
+    local _f _group _home _shell _gid _passwd
+
+    # First, sanity-check the user
+    check_user_validity "${username}" "${uid}" "${group}" "${gid}"
+
+    # Generate a new UID if needed
+    if [ ${uid} -eq -1 ]; then
+        uid="$( generate_uid "${username}" )"
+    fi
+
+    # Remove any previous instance of this user
+    for _f in "${PASSWD}" "${SHADOW}"; do
+        sed -r -i -e '/^'"${username}"':.*/d;' "${_f}"
+    done
+
+    _gid="$( get_gid "${group}" )"
+    _shell="${shell}"
+    if [ "${shell}" = "-" ]; then
+        _shell="/bin/false"
+    fi
+    case "${home}" in
+        -)  _home="/";;
+        /)  fail "home can not explicitly be '/'\n";;
+        /*) _home="${home}";;
+        *)  fail "home must be an absolute path\n";;
+    esac
+    case "${passwd}" in
+        !=*)
+            _passwd='!'"$( mkpasswd -m md5 "${passwd#!=}" )"
+            ;;
+        =*)
+            _passwd="$( mkpasswd -m md5 "${passwd#=}" )"
+            ;;
+        *)
+            _passwd="${passwd}"
+            ;;
+    esac
+
+    printf "%s:x:%d:%d:%s:%s:%s\n"              \
+           "${username}" "${uid}" "${_gid}"     \
+           "${comment}" "${_home}" "${_shell}"  \
+           >>"${PASSWD}"
+    printf "%s:%s:%d:0:99999:7:::\n"                \
+           "${username}" "${_passwd}" "${nb_days}"  \
+           >>"${SHADOW}"
+
+    # Add the user to its additional groups
+    if [ "${groups}" != "-" ]; then
+        for _group in ${groups//,/ }; do
+            add_user_to_group "${username}" "${_group}"
+        done
+    fi
+
+    # If the user has a home, chown it
+    # (Note: stdout goes to the fakeroot-script)
+    if [ "${home}" != "-" ]; then
+        mkdir -p "${TARGET_DIR}/${home}"
+        printf "chown -R %d:%d '%s'\n" "${uid}" "${_gid}" "${TARGET_DIR}/${home}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+main() {
+    local username uid group gid passwd home shell groups comment
+
+    # Some sanity checks
+    if [ ${MIN_UID} -le 0 ]; then
+        fail "MIN_UID must be >0 (currently %d)\n" ${MIN_UID}
+    fi
+    if [ ${MIN_GID} -le 0 ]; then
+        fail "MIN_GID must be >0 (currently %d)\n" ${MIN_GID}
+    fi
+
+    # First, create all the main groups
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        add_one_group "${group}" "${gid}"
+    done <"${USERS_TABLE}"
+
+    # Then, create all the additional groups
+    # If any additional group is already a main group, we should use
+    # the gid of that main group; otherwise, we can use any gid
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        if [ "${groups}" != "-" ]; then
+            for g in ${groups//,/ }; do
+                add_one_group "${g}" -1
+            done
+        fi
+    done <"${USERS_TABLE}"
+
+    # Finally, add users
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
+                     "${home}" "${shell}" "${groups}" "${comment}"
+    done <"${USERS_TABLE}"
+}
+
+#----------------------------------------------------------------------------
+main "${@}"
-- 
1.7.2.5

^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-01-01 22:32     ` Yann E. MORIN
@ 2013-01-03 21:46       ` Yann E. MORIN
  0 siblings, 0 replies; 27+ messages in thread
From: Yann E. MORIN @ 2013-01-03 21:46 UTC (permalink / raw)
  To: buildroot

Samuel, All,

On Tuesday 01 January 2013 Yann E. MORIN wrote:
> On Tuesday 01 January 2013 Samuel Martin wrote:
> > 2013/1/1 Yann E. MORIN <yann.morin.1998@free.fr>:
[--SNIP--]
> > > +- +password+ is the crypt(3)-encrypted password. If prefixed with +=+,
> > > +  then it is interpreted as clear-text, and will be cypt-encoded. If
> > > +  prefixed with +!+, then login is disabled. If set to +*+, then login
> > > +  is not allowed.
> > Multiple prefix is allowed/supported; it could be explicitly mentioned.
> > Is the prefix order important?
> > 
> > So, the clear-text password itself should not starts with any prefix character?
[--SNIP--]
> So, valid combinations are:
>     *           no password, login not allowed
>     XXXX        crypt-encoded password
>     !XXXX       crypt-encoded password, login disabled
>     =1234       clear-text password
>     !=1234      clear-text password, login disabled

And of course, '!' and '=' are not in the set of characters that crypt can
emit for an encoded password, so it is legit to write:
    =           empty password
    =!foo       clear-text password starts with a '!'
    ==bar       clear-text password starts with a '='
    !=          empty password                       , login is disabled
    !=!foo      clear-text password starts with a '!', login is disabled
    !==bar      clear-text password starts with a '=', login is disabled

Regards,
Yann E. MORIN.

-- 
.-----------------.--------------------.------------------.--------------------.
|  Yann E. MORIN  | Real-Time Embedded | /"\ ASCII RIBBON | Erics' conspiracy: |
| +33 662 376 056 | Software  Designer | \ / CAMPAIGN     |  ___               |
| +33 223 225 172 `------------.-------:  X  AGAINST      |  \e/  There is no  |
| http://ymorin.is-a-geek.org/ | _/*\_ | / \ HTML MAIL    |   v   conspiracy.  |
'------------------------------^-------^------------------^--------------------'

^ permalink raw reply	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-01-03 10:31         ` Thomas Petazzoni
@ 2013-01-03 17:35           ` Yann E. MORIN
  0 siblings, 0 replies; 27+ messages in thread
From: Yann E. MORIN @ 2013-01-03 17:35 UTC (permalink / raw)
  To: buildroot

Thomas, All,

On Thursday 03 January 2013 Thomas Petazzoni wrote:
> On Thu, 03 Jan 2013 02:35:16 -0000, Cam Hutchison wrote:
> > Another +1 for awk here - processing record-oriented files is what
> > awk is good at, but I can understand not wanting to introduce that
> > dependency.
> 
> We already depend on awk:
>   http://git.buildroot.net/buildroot/tree/support/dependencies/dependencies.sh#n133

OK, I'll update the script, then.

Thanks!

Regards,
Yann E. MORIN.

-- 
.-----------------.--------------------.------------------.--------------------.
|  Yann E. MORIN  | Real-Time Embedded | /"\ ASCII RIBBON | Erics' conspiracy: |
| +33 662 376 056 | Software  Designer | \ / CAMPAIGN     |  ___               |
| +33 223 225 172 `------------.-------:  X  AGAINST      |  \e/  There is no  |
| http://ymorin.is-a-geek.org/ | _/*\_ | / \ HTML MAIL    |   v   conspiracy.  |
'------------------------------^-------^------------------^--------------------'

^ permalink raw reply	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-01-03  2:35       ` Cam Hutchison
@ 2013-01-03 10:31         ` Thomas Petazzoni
  2013-01-03 17:35           ` Yann E. MORIN
  0 siblings, 1 reply; 27+ messages in thread
From: Thomas Petazzoni @ 2013-01-03 10:31 UTC (permalink / raw)
  To: buildroot

Dear Cam Hutchison,

On Thu, 03 Jan 2013 02:35:16 -0000, Cam Hutchison wrote:

> Another +1 for awk here - processing record-oriented files is what
> awk is good at, but I can understand not wanting to introduce that
> dependency.

We already depend on awk:

  http://git.buildroot.net/buildroot/tree/support/dependencies/dependencies.sh#n133

Best regards,

Thomas
-- 
Thomas Petazzoni, Free Electrons
Kernel, drivers, real-time and embedded Linux
development, consulting, training and support.
http://free-electrons.com

^ permalink raw reply	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-01-02 18:31     ` Yann E. MORIN
@ 2013-01-03  2:35       ` Cam Hutchison
  2013-01-03 10:31         ` Thomas Petazzoni
  0 siblings, 1 reply; 27+ messages in thread
From: Cam Hutchison @ 2013-01-03  2:35 UTC (permalink / raw)
  To: buildroot

"Yann E. MORIN" <yann.morin.1998@free.fr> writes:

>Cam, All,

>On Wednesday 02 January 2013 Cam Hutchison wrote:
>> "Yann E. MORIN" <yann.morin.1998@free.fr> writes:
>> 
>> >+* +LIBFOO_USERS+ lists the users to create for this package, if it installs
>> >+  a daemon you want to run with a specific user. The syntax is similar in
>> 
>> "if it installs a program you want to run as a specific user"
>> 
>> that is, s/daemon/program/ and s/with/as/

>Well, I would like to emphasise that this is primarily for running
>programs as daemons (ie. started by startup scripts). It does not
>really make sense to run program as a specific user when logged in,
>especially for embedded systems, where logging in a seldom done.

>What about:

>.... if it installs a daemon program you want to run as ...

I was considering a case of a periodic cron job running as a non-root
user - that is not a daemon, and is not related to logged-in users.

Since there are no actual constraints that require that the user added
be used only by a daemon, I figured the documentation should not introduce
that constraint. But documenting intentions is fine and I have no
strong feeling either way.

>> >+#----------------------------------------------------------------------------
>> >+get_uid() {
>> >+    local username="${1}"
>> >+
>> >+    grep -r -E "${username}:" "${PASSWD}" |cut -d: -f3
>> 

>> An argument could be made that you should be using grep -F.

>I don't know (ie. I don't usualy use) this switch, so I am not confident in
>using it here. If plain 'grep' does the job, lets just use that.

The problem with plain grep is that if the username contains a regular
expression metachar, then grep will not work as expected in some cases.
The most likely one is a period - a username of foo.bar will match
an existing user of fooxbar and will not allow the creation of the user
foo.bar.

More strictly, it is a matter of handling user input correctly. The user
was not expecting a username to be a regular expression, so it should not
be treated as such ...

>> You should also anchor ${username}

>Yes, indeed.

.... but a start-of-line anchor will not work with grep -F :-(

Another +1 for awk here - processing record-oriented files is what awk is
good at, but I can understand not wanting to introduce that dependency.

^ permalink raw reply	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-01-02  3:40   ` Cam Hutchison
@ 2013-01-02 18:31     ` Yann E. MORIN
  2013-01-03  2:35       ` Cam Hutchison
  0 siblings, 1 reply; 27+ messages in thread
From: Yann E. MORIN @ 2013-01-02 18:31 UTC (permalink / raw)
  To: buildroot

Cam, All,

On Wednesday 02 January 2013 Cam Hutchison wrote:
> "Yann E. MORIN" <yann.morin.1998@free.fr> writes:
> 
> >+* +LIBFOO_USERS+ lists the users to create for this package, if it installs
> >+  a daemon you want to run with a specific user. The syntax is similar in
> 
> "if it installs a program you want to run as a specific user"
> 
> that is, s/daemon/program/ and s/with/as/

Well, I would like to emphasise that this is primarily for running
programs as daemons (ie. started by startup scripts). It does not
really make sense to run program as a specific user when logged in,
especially for embedded systems, where logging in a seldom done.

What about:

... if it installs a daemon program you want to run as ...

> >+- +group+ is the desired name for the user's main group.
> >+- +gid+ is the desired GID for the user's main group. It must be unique,
> >+  and not +0+. If set to +-1+, then a unique GID will be computed by
> >+  buildroot.
> 
> I think this is saying it creates groups as well as users. If so, the
> documentation should say so.

OK.

> Also, how do you specifiy that you do not want a group created - such
> as when you want a new user in an existing system group. I would assume
> something like
> 
> foo -1 daemon -1 ...
> 
> to create user "foo" in the system "daemon" group, but the documentation
> implies a new GID will be allocated.

Ah, yes. If the group already exists (because it is in the skeleton, or
because a previous user added it), then the GID for that group is used,
no new GID is created, of course.

I'll add this to the doc. Good catch!

> >+- +password+ is the crypt(3)-encrypted password. If prefixed with +=+,
> >+  then it is interpreted as clear-text, and will be cypt-encoded. If
> >+  prefixed with +!+, then login is disabled. If set to +*+, then login
> >+  is not allowed.
> 
> What is the status of the support of other encryption algorithms? e.g.
> $6$salt$enc for SHA-512 encrypted passwords? This will depend on settings
> in BuildRoot itself, but it would be worth making a reference here
> since it is relevant to a user providing encrypted user passwords.

First, this is not meant for buildroot users, but for buildroot developpers.
This makes a huge difference, but shall be documented nonetheless, of course.

If the developper enters an already-encoded des/md5/sha256/sha512 pasword
(ie. a password that does not start with '=') then the content is used as-is.

The current password encoding is md5 ($1$salt$...).

Once my other patch to add selection of an alternative encoding, I'll
use that here, too:
    http://lists.busybox.net/pipermail/buildroot/2012-December/064411.html

> >+set -e
> ....
> >+USERS_TABLE="${1}"
> >+TARGET_DIR="${2}"
> >+shift 2
> 
> This will fail under 'set -e' if there are less than two parameters, but fail
> without any useful diagnostics. An error/usage message here would be useful.

OK.

> >+#----------------------------------------------------------------------------
> >+error() {
> >+    local fmt="${1}"
> >+    shift
> >+
> >+    printf "%s: " "${myname}" >&2
> >+    printf "${fmt}" "${@}" >&2
> 
> I think you should put a \n in the format string here. Every call to this
> function (via fail) includes a \n, so it seems redundant require each caller
> to put a \n on the string. In fact, the last call to fail does not have
> a \n in the string which it should, so this will also fix what would
> probably become a common error.

I prefer to keep the \n is every calls. The missing one is an bug.

> >+}
> >+fail() {
> >+    error "$@"
> >+    exit 1
> >+}
> >+
> >+#----------------------------------------------------------------------------
> >+get_uid() {
> >+    local username="${1}"
> >+
> >+    grep -r -E "${username}:" "${PASSWD}" |cut -d: -f3
> 
> Why grep -r?

Indeed.

> Why grep -E? You are not using any features of extended regular expressions.

Because I *always* use 'grep -E', and I /forgot/ what is a standard regexp.
But I can remove, yes.

> An argument could be made that you should be using grep -F.

I don't know (ie. I don't usualy use) this switch, so I am not confident in
using it here. If plain 'grep' does the job, lets just use that.

> You should also anchor ${username}

Yes, indeed.

> >+get_username() {
> >+    local uid="${1}"
> >+
> >+    sed -r -e '/^([^:]+):[^:]+:'"${uid}"':.*/!d; s//\1/;' "${PASSWD}"
> 
> Is awk available in the host toolchain?

Nopt systematically. There's host-gawk that package can depend on.
If we were to use awk instead of sed, then host-gawk would always be
built as soon as a rootfs image (fs/tarball/...) is generated.

So, I prefer to keep using sed for now, unless Peter agrees we can build
always build host-gawk.

> >+check_user_validity() {
> >+    local username="${1}"
> >+    local uid="${2}"
> >+    local group="${3}"
> >+    local gid="${4}"
> >+    local _uid _ugid _gid _username _group _ugroup
> >+
> >+    _group="$( get_group "${gid}" )"
> >+    _gid="$( get_gid "${group}" )"
> >+    _ugid="$( get_ugid "${username}" )"
> >+    _username="$( get_username "${uid}" )"
> >+    _uid="$( get_uid "${username}" )"
> >+    _ugroup="$( get_ugroup "${username}" )"
> >+
> >+    if [ ${gid} -ge 0 ]; then
> 
> Elsewhere in this script you check uid/gid against -1, not >= 0. This
> should be changed to "${gid} -eq -1" to be consistent with that and the
> documentation.

OK. Should also check for invalid (ie. -lt -1).

> >+    case "${home}" in
> >+        -)  _home="/";;
> >+        /)  fail "home can not be explicitly '/'\n";;
> >+        /*) _home="${home}";;
> >+        *)  fail "home must be an absolute path";;
> 
> This is where you missed the \n in the error message.

OK.

> There is also another spelling mistake somewhere :-) I noticed three when
> I first skimmed this patch; two have already been pointed out; and I did
> not see the third when I went through to comment.

Fixed! ;-)

Thank you!

Regards,
Yann E. MORIN.

-- 
.-----------------.--------------------.------------------.--------------------.
|  Yann E. MORIN  | Real-Time Embedded | /"\ ASCII RIBBON | Erics' conspiracy: |
| +33 662 376 056 | Software  Designer | \ / CAMPAIGN     |  ___               |
| +33 223 225 172 `------------.-------:  X  AGAINST      |  \e/  There is no  |
| http://ymorin.is-a-geek.org/ | _/*\_ | / \ HTML MAIL    |   v   conspiracy.  |
'------------------------------^-------^------------------^--------------------'

^ permalink raw reply	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-01-02  3:44   ` Cam Hutchison
@ 2013-01-02 18:05     ` Yann E. MORIN
  0 siblings, 0 replies; 27+ messages in thread
From: Yann E. MORIN @ 2013-01-02 18:05 UTC (permalink / raw)
  To: buildroot

Cam, All,

On Wednesday 02 January 2013 Cam Hutchison wrote:
> "Yann E. MORIN" <yann.morin.1998@free.fr> writes:
> 
> >+The syntax for adding a user is a space-separated list of fields, one
> >+user per-line; the fields are:
> 
> per line - no hyphen here.
> 
> >+* no field may contain a column (+:+).
> 
> Found it - this is the third spelling mistake I referred to earlier.
> s/column/colon/

Thank you!

Regards,
Yann E. MORIN.

-- 
.-----------------.--------------------.------------------.--------------------.
|  Yann E. MORIN  | Real-Time Embedded | /"\ ASCII RIBBON | Erics' conspiracy: |
| +33 662 376 056 | Software  Designer | \ / CAMPAIGN     |  ___               |
| +33 223 225 172 `------------.-------:  X  AGAINST      |  \e/  There is no  |
| http://ymorin.is-a-geek.org/ | _/*\_ | / \ HTML MAIL    |   v   conspiracy.  |
'------------------------------^-------^------------------^--------------------'

^ permalink raw reply	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-01-01 20:10 ` [Buildroot] [PATCH 1/2] packages: add ability for packages to create users Yann E. MORIN
  2013-01-01 21:50   ` Samuel Martin
  2013-01-02  3:40   ` Cam Hutchison
@ 2013-01-02  3:44   ` Cam Hutchison
  2013-01-02 18:05     ` Yann E. MORIN
  2 siblings, 1 reply; 27+ messages in thread
From: Cam Hutchison @ 2013-01-02  3:44 UTC (permalink / raw)
  To: buildroot

"Yann E. MORIN" <yann.morin.1998@free.fr> writes:

>+The syntax for adding a user is a space-separated list of fields, one
>+user per-line; the fields are:

per line - no hyphen here.

>+* no field may contain a column (+:+).

Found it - this is the third spelling mistake I referred to earlier.
s/column/colon/

^ permalink raw reply	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-01-01 20:10 ` [Buildroot] [PATCH 1/2] packages: add ability for packages to create users Yann E. MORIN
  2013-01-01 21:50   ` Samuel Martin
@ 2013-01-02  3:40   ` Cam Hutchison
  2013-01-02 18:31     ` Yann E. MORIN
  2013-01-02  3:44   ` Cam Hutchison
  2 siblings, 1 reply; 27+ messages in thread
From: Cam Hutchison @ 2013-01-02  3:40 UTC (permalink / raw)
  To: buildroot

"Yann E. MORIN" <yann.morin.1998@free.fr> writes:

>+* +LIBFOO_USERS+ lists the users to create for this package, if it installs
>+  a daemon you want to run with a specific user. The syntax is similar in

"if it installs a program you want to run as a specific user"

that is, s/daemon/program/ and s/with/as/

>+- +group+ is the desired name for the user's main group.
>+- +gid+ is the desired GID for the user's main group. It must be unique,
>+  and not +0+. If set to +-1+, then a unique GID will be computed by
>+  buildroot.

I think this is saying it creates groups as well as users. If so, the
documentation should say so.

Also, how do you specifiy that you do not want a group created - such
as when you want a new user in an existing system group. I would assume
something like

foo -1 daemon -1 ...

to create user "foo" in the system "daemon" group, but the documentation
implies a new GID will be allocated.

>+- +password+ is the crypt(3)-encrypted password. If prefixed with +=+,
>+  then it is interpreted as clear-text, and will be cypt-encoded. If
>+  prefixed with +!+, then login is disabled. If set to +*+, then login
>+  is not allowed.

What is the status of the support of other encryption algorithms? e.g.
$6$salt$enc for SHA-512 encrypted passwords? This will depend on settings
in BuildRoot itself, but it would be worth making a reference here
since it is relevant to a user providing encrypted user passwords.

>+set -e
....
>+USERS_TABLE="${1}"
>+TARGET_DIR="${2}"
>+shift 2

This will fail under 'set -e' if there are less than two parameters, but fail
without any useful diagnostics. An error/usage message here would be useful.

>+#----------------------------------------------------------------------------
>+error() {
>+    local fmt="${1}"
>+    shift
>+
>+    printf "%s: " "${myname}" >&2
>+    printf "${fmt}" "${@}" >&2

I think you should put a \n in the format string here. Every call to this
function (via fail) includes a \n, so it seems redundant require each caller
to put a \n on the string. In fact, the last call to fail does not have
a \n in the string which it should, so this will also fix what would
probably become a common error.

>+}
>+fail() {
>+    error "$@"
>+    exit 1
>+}
>+
>+#----------------------------------------------------------------------------
>+get_uid() {
>+    local username="${1}"
>+
>+    grep -r -E "${username}:" "${PASSWD}" |cut -d: -f3

Why grep -r? A recursive grep with a filename (non-directory) as an argument
does not do anything different without -r. Ditto for get_ugid and get_gid.

Why grep -E? You are not using any features of extended regular expressions.
An argument could be made that you should be using grep -F.

You should also anchor ${username} to the start of the line or you may get
false matches:

  grep "^${username}:" "${PASSWD}"

ditto get_ugid and get_gid.

>+}
>+
>+#----------------------------------------------------------------------------
>+get_ugid() {
>+    local username="${1}"
>+
>+    grep -r -E "${username}:" "${PASSWD}" |cut -d: -f4
>+}
>+
>+#----------------------------------------------------------------------------
>+get_gid() {
>+    local group="${1}"
>+
>+    grep -r -E "${group}:" "${GROUP}" |cut -d: -f3
>+}
>+
>+#----------------------------------------------------------------------------
>+get_username() {
>+    local uid="${1}"
>+
>+    sed -r -e '/^([^:]+):[^:]+:'"${uid}"':.*/!d; s//\1/;' "${PASSWD}"

Is awk available in the host toolchain? If so, this would be much clearer
with awk:

  awk -F: '$3 == '"${uid}"' { print $1 }' "${PASSWD}"

ditto get_group

>+}
>+
>+#----------------------------------------------------------------------------
>+get_group() {
>+    local gid="${1}"
>+
>+    sed -r -e '/^([^:]+):[^:]+:'"${gid}"':/!d; s//\1/;' "${GROUP}"
>+}
>+
>+#----------------------------------------------------------------------------
>+get_ugroup() {
>+    local username="${1}"
>+    local ugid
>+
>+    ugid="$( get_ugid "${username}" )"
>+    if [ -n "${ugid}" ]; then
>+        get_group "${ugid}"
>+    fi
>+}
>+
>+#----------------------------------------------------------------------------
>+# Sanity-check the new user/group:
>+#   - check the gid is not already used for another group
>+#   - check the group does not already exist with another gid
>+#   - check the user does not already exist with another gid
>+#   - check the uid is not already used for another user
>+#   - check the user does not already exist with another uid
>+#   - check the user does not already exist in another group
>+check_user_validity() {
>+    local username="${1}"
>+    local uid="${2}"
>+    local group="${3}"
>+    local gid="${4}"
>+    local _uid _ugid _gid _username _group _ugroup
>+
>+    _group="$( get_group "${gid}" )"
>+    _gid="$( get_gid "${group}" )"
>+    _ugid="$( get_ugid "${username}" )"
>+    _username="$( get_username "${uid}" )"
>+    _uid="$( get_uid "${username}" )"
>+    _ugroup="$( get_ugroup "${username}" )"
>+
>+    if [ ${gid} -ge 0 ]; then

Elsewhere in this script you check uid/gid against -1, not >= 0. This
should be changed to "${gid} -eq -1" to be consistent with that and the
documentation.

>+    case "${home}" in
>+        -)  _home="/";;
>+        /)  fail "home can not be explicitly '/'\n";;
>+        /*) _home="${home}";;
>+        *)  fail "home must be an absolute path";;

This is where you missed the \n in the error message.

There is also another spelling mistake somewhere :-) I noticed three when
I first skimmed this patch; two have already been pointed out; and I did
not see the third when I went through to comment.

^ permalink raw reply	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-01-01 21:50   ` Samuel Martin
@ 2013-01-01 22:32     ` Yann E. MORIN
  2013-01-03 21:46       ` Yann E. MORIN
  0 siblings, 1 reply; 27+ messages in thread
From: Yann E. MORIN @ 2013-01-01 22:32 UTC (permalink / raw)
  To: buildroot

Smartin, All,

On Tuesday 01 January 2013 Samuel Martin wrote:
> 2013/1/1 Yann E. MORIN <yann.morin.1998@free.fr>:
> > Packages that install daemons may need those daemons to run as a non-root,
> > or an otherwise non-system (eg. 'daemon'), user.
> >
> > Add infrastructure for packages to create users, by declaring the
> > FOO_USERS variable that conatin a makedev-syntax-like description
> > of the user(s) to add.
[--SNIP--]
> > diff --git a/docs/manual/makedev-syntax.txt b/docs/manual/makedev-syntax.txt
> > index 27517b3..fffdac9 100644
> > --- a/docs/manual/makedev-syntax.txt
> > +++ b/docs/manual/makedev-syntax.txt
> You could add this in a new file...
> Why should makedev-syntax.txt contain syntax details for makedev and mkusers?

Right, I'll do.

> > +- +password+ is the crypt(3)-encrypted password. If prefixed with +=+,
> > +  then it is interpreted as clear-text, and will be cypt-encoded. If
> > +  prefixed with +!+, then login is disabled. If set to +*+, then login
> > +  is not allowed.
> Multiple prefix is allowed/supported; it could be explicitly mentioned.
> Is the prefix order important?
> 
> So, the clear-text password itself should not starts with any prefix character?

Oh! You raise a valid point here. In fact, I coded '=!' as being the same
as '!=' , which it clearly is not.

So, valid combinations are:
    *           no password, login not allowed
    XXXX        crypt-encoded password
    !XXXX       crypt-encoded password, login disabled
    =1234       clear-text password
    !=1234      clear-text password, login disabled

Thank you!

Regards,
Yann E. MORIN.

-- 
.-----------------.--------------------.------------------.--------------------.
|  Yann E. MORIN  | Real-Time Embedded | /"\ ASCII RIBBON | Erics' conspiracy: |
| +33 662 376 056 | Software  Designer | \ / CAMPAIGN     |  ___               |
| +33 223 225 172 `------------.-------:  X  AGAINST      |  \e/  There is no  |
| http://ymorin.is-a-geek.org/ | _/*\_ | / \ HTML MAIL    |   v   conspiracy.  |
'------------------------------^-------^------------------^--------------------'

^ permalink raw reply	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-01-01 20:10 ` [Buildroot] [PATCH 1/2] packages: add ability for packages to create users Yann E. MORIN
@ 2013-01-01 21:50   ` Samuel Martin
  2013-01-01 22:32     ` Yann E. MORIN
  2013-01-02  3:40   ` Cam Hutchison
  2013-01-02  3:44   ` Cam Hutchison
  2 siblings, 1 reply; 27+ messages in thread
From: Samuel Martin @ 2013-01-01 21:50 UTC (permalink / raw)
  To: buildroot

Hi Yann, all,

2013/1/1 Yann E. MORIN <yann.morin.1998@free.fr>:
> Packages that install daemons may need those daemons to run as a non-root,
> or an otherwise non-system (eg. 'daemon'), user.
>
> Add infrastructure for packages to create users, by declaring the
> FOO_USERS variable that conatin a makedev-syntax-like description
> of the user(s) to add.
>
> Signed-off-by: "Yann E. MORIN" <yann.morin.1998@free.fr>
> ---
[snip]
> @@ -252,6 +259,11 @@ information is (assuming the package name is +libfoo+) :
>    You can find some documentation for this syntax in the xref:makedev-syntax[].
>    This variable is optional.
>
> +* +LIBFOO_USERS+ lists the users to create for this package, if it installs
> +  a daemon you want to run with a specific user. The syntax is similar in
> +  spirit to the makedevs one, and is described in the xref:makeuser-syntax[].
> +  This variable is optional.
> +
>  * +LIBFOO_LICENSE+ defines the license (or licenses) under which the package
>    is released.
>    This name will appear in the manifest file produced by +make legal-info+.
> diff --git a/docs/manual/makedev-syntax.txt b/docs/manual/makedev-syntax.txt
> index 27517b3..fffdac9 100644
> --- a/docs/manual/makedev-syntax.txt
> +++ b/docs/manual/makedev-syntax.txt
You could add this in a new file...
Why should makedev-syntax.txt contain syntax details for makedev and mkusers?

> @@ -54,3 +54,68 @@ and then for device files corresponding to the partitions of
>  /dev/hda       b       640     0       0       3       1       1       1       15
>  -------------------------------------------------------------------
>
> +
> +[[makeuser-syntax]]
> +Makeuser syntax documentation
> +-----------------------------
> +
> +The syntax to create users is inspired by the makedev syntax, above, but
> +is psecific to buildroot.
s/psecific/specific/

> +
> +The syntax for adding a user is a space-separated list of fields, one
> +user per-line; the fields are:
> +
> +|=================================================================
> +|username |uid |group |gid |password |home |shell |groups |comment
> +|=================================================================
> +
> +Where:
> +
> +- +username+ is the desired user name (aka login name) for the user.
> +  It must be unique.
> +- +uid+ is the desired UID for the user. It must be unique, and not
> +  +0+. If set to +-1+, then a unique UID will be computed by buildroot.
s/buildroot/Buildroot/

> +- +group+ is the desired name for the user's main group.
> +- +gid+ is the desired GID for the user's main group. It must be unique,
> +  and not +0+. If set to +-1+, then a unique GID will be computed by
> +  buildroot.
ditto

> +- +password+ is the crypt(3)-encrypted password. If prefixed with +=+,
> +  then it is interpreted as clear-text, and will be cypt-encoded. If
s/cypt-encoded/crypt-encoded/

> +  prefixed with +!+, then login is disabled. If set to +*+, then login
> +  is not allowed.
Multiple prefix is allowed/supported; it could be explicitly mentioned.
Is the prefix order important?

So, the clear-text password itself should not starts with any prefix character?

> +- +home+ is the desired home directory for the user. If set to '-', no
> +  home directory will be created, and the user's home will be +/+.
> +  Explicitly setting +home+ to +/+ is not allowed.
> +- +shell+ is the desired shell for the user. If set to +-+, then
> +  +/bin/false+ is set as the user's shell.
> +- +groups+ is the comma-separated list of additional groups the user
> +  should be part of. If set to +-+, then the user will be a member of
> +  no additional group.
> +- +comment+ is an almost-free-form text.
> +
> +There are a few restrictions on the content of each field:
> +
> +* except for +comment+, all fields are mandatory.
> +* except for +comment+, fields may not contain spaces.
> +* no field may contain a column (+:+).
> +
> +If +home+ is not +-+, then the home directory, and all files below,
> +will belong to the user and its main group.
> +
> +Example:
> +
> +----
> +foo -1 bar -1 !=blabla /home/foo /bin/sh alpha,bravo Foo user
> +----
> +
> +This will create this user:
> +
> +- +username+ (aka login name) is: +foo+
> +- +uid+ is computed by buildroot
ditto

> +- main +group+ is: +bar+
> +- main group +gid+ is computed by buildroot
ditto

> +- clear-text +password+ is: +blabla+, will be crypt(3)-encrypted, but login is disabled.
> +- +home+ is: +/home/foo+
> +- +shell+ is: +/bin/sh+
> +- +foo+ is also a member of +groups+: +alpha+ and +bravo+
> +- +comment+ is: +Foo user+
[snip]
> --- /dev/null
> +++ b/support/scripts/mkusers
> @@ -0,0 +1,348 @@
> +#!/bin/bash
> +set -e
> +myname="${0##*/}"
> +
> +#----------------------------------------------------------------------------
> +# Configurable items
> +MIN_UID=1000
> +MAX_UID=1999
> +MIN_GID=1000
> +MAX_GID=1999
> +# No more is configurable below this point
> +#----------------------------------------------------------------------------
> +
> +#----------------------------------------------------------------------------
> +USERS_TABLE="${1}"
> +TARGET_DIR="${2}"
> +shift 2
> +PASSWD="${TARGET_DIR}/etc/passwd"
> +SHADOW="${TARGET_DIR}/etc/shadow"
> +GROUP="${TARGET_DIR}/etc/group"
> +# /etc/gsahdow is not part of the standard skeleton, so not everybody
> +# will have it, but some may hav it, and its content must be in sync
s/hav/have/


Regards,

-- 
Samuel

^ permalink raw reply	[flat|nested] 27+ messages in thread

* [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
  2013-01-01 20:10 [Buildroot] [pull request] Pull request for branch yem-package-create-user Yann E. MORIN
@ 2013-01-01 20:10 ` Yann E. MORIN
  2013-01-01 21:50   ` Samuel Martin
                     ` (2 more replies)
  0 siblings, 3 replies; 27+ messages in thread
From: Yann E. MORIN @ 2013-01-01 20:10 UTC (permalink / raw)
  To: buildroot

Packages that install daemons may need those daemons to run as a non-root,
or an otherwise non-system (eg. 'daemon'), user.

Add infrastructure for packages to create users, by declaring the
FOO_USERS variable that conatin a makedev-syntax-like description
of the user(s) to add.

Signed-off-by: "Yann E. MORIN" <yann.morin.1998@free.fr>
---
 docs/manual/adding-packages-generic.txt |   16 ++-
 docs/manual/makedev-syntax.txt          |   65 ++++++
 fs/common.mk                            |    5 +-
 package/pkg-generic.mk                  |    1 +
 support/scripts/mkusers                 |  348 +++++++++++++++++++++++++++++++
 5 files changed, 432 insertions(+), 3 deletions(-)
 create mode 100755 support/scripts/mkusers

diff --git a/docs/manual/adding-packages-generic.txt b/docs/manual/adding-packages-generic.txt
index 0759d4f..1adf424 100644
--- a/docs/manual/adding-packages-generic.txt
+++ b/docs/manual/adding-packages-generic.txt
@@ -50,7 +50,11 @@ system is based on hand-written Makefiles or shell scripts.
 34:	/bin/foo  f  4755  0  0	 -  -  -  -  -
 35: endef
 36:
-37: $(eval $(generic-package))
+37: define LIBFOO_USERS
+38: foo -1 libfoo -1 * - - - LibFoo daemon
+39: endef
+40
+41: $(eval $(generic-package))
 --------------------------------
 
 The Makefile begins on line 6 to 10 with metadata information: the
@@ -95,7 +99,10 @@ On line 29..31, we define a device-node file used by this package
 On line 33..35, we define the permissions to set to specific files
 installed by this package (+LIBFOO_PERMISSIONS+).
 
-Finally, on line 37, we call the +generic-package+ function, which
+On lines 37..39, we define a user that is used by this package (eg.
+to run a daemon as non-root).
+
+Finally, on line 41, we call the +generic-package+ function, which
 generates, according to the variables defined previously, all the
 Makefile code necessary to make your package working.
 
@@ -252,6 +259,11 @@ information is (assuming the package name is +libfoo+) :
   You can find some documentation for this syntax in the xref:makedev-syntax[].
   This variable is optional.
 
+* +LIBFOO_USERS+ lists the users to create for this package, if it installs
+  a daemon you want to run with a specific user. The syntax is similar in
+  spirit to the makedevs one, and is described in the xref:makeuser-syntax[].
+  This variable is optional.
+
 * +LIBFOO_LICENSE+ defines the license (or licenses) under which the package
   is released.
   This name will appear in the manifest file produced by +make legal-info+.
diff --git a/docs/manual/makedev-syntax.txt b/docs/manual/makedev-syntax.txt
index 27517b3..fffdac9 100644
--- a/docs/manual/makedev-syntax.txt
+++ b/docs/manual/makedev-syntax.txt
@@ -54,3 +54,68 @@ and then for device files corresponding to the partitions of
 /dev/hda	b	640	0	0	3	1	1	1	15
 -------------------------------------------------------------------
 
+
+[[makeuser-syntax]]
+Makeuser syntax documentation
+-----------------------------
+
+The syntax to create users is inspired by the makedev syntax, above, but
+is psecific to buildroot.
+
+The syntax for adding a user is a space-separated list of fields, one
+user per-line; the fields are:
+
+|=================================================================
+|username |uid |group |gid |password |home |shell |groups |comment
+|=================================================================
+
+Where:
+
+- +username+ is the desired user name (aka login name) for the user.
+  It must be unique.
+- +uid+ is the desired UID for the user. It must be unique, and not
+  +0+. If set to +-1+, then a unique UID will be computed by buildroot.
+- +group+ is the desired name for the user's main group.
+- +gid+ is the desired GID for the user's main group. It must be unique,
+  and not +0+. If set to +-1+, then a unique GID will be computed by
+  buildroot.
+- +password+ is the crypt(3)-encrypted password. If prefixed with +=+,
+  then it is interpreted as clear-text, and will be cypt-encoded. If
+  prefixed with +!+, then login is disabled. If set to +*+, then login
+  is not allowed.
+- +home+ is the desired home directory for the user. If set to '-', no
+  home directory will be created, and the user's home will be +/+.
+  Explicitly setting +home+ to +/+ is not allowed.
+- +shell+ is the desired shell for the user. If set to +-+, then
+  +/bin/false+ is set as the user's shell.
+- +groups+ is the comma-separated list of additional groups the user
+  should be part of. If set to +-+, then the user will be a member of
+  no additional group.
+- +comment+ is an almost-free-form text.
+
+There are a few restrictions on the content of each field:
+
+* except for +comment+, all fields are mandatory.
+* except for +comment+, fields may not contain spaces.
+* no field may contain a column (+:+).
+
+If +home+ is not +-+, then the home directory, and all files below,
+will belong to the user and its main group.
+
+Example:
+
+----
+foo -1 bar -1 !=blabla /home/foo /bin/sh alpha,bravo Foo user
+----
+
+This will create this user:
+
+- +username+ (aka login name) is: +foo+
+- +uid+ is computed by buildroot
+- main +group+ is: +bar+
+- main group +gid+ is computed by buildroot
+- clear-text +password+ is: +blabla+, will be crypt(3)-encrypted, but login is disabled.
+- +home+ is: +/home/foo+
+- +shell+ is: +/bin/sh+
+- +foo+ is also a member of +groups+: +alpha+ and +bravo+
+- +comment+ is: +Foo user+
diff --git a/fs/common.mk b/fs/common.mk
index b1512dd..b5a7950 100644
--- a/fs/common.mk
+++ b/fs/common.mk
@@ -35,6 +35,7 @@ FAKEROOT_SCRIPT = $(BUILD_DIR)/_fakeroot.fs
 FULL_DEVICE_TABLE = $(BUILD_DIR)/_device_table.txt
 ROOTFS_DEVICE_TABLES = $(call qstrip,$(BR2_ROOTFS_DEVICE_TABLE)) \
 	$(call qstrip,$(BR2_ROOTFS_STATIC_DEVICE_TABLE))
+USERS_TABLE = $(BUILD_DIR)/_users_table.txt
 
 define ROOTFS_TARGET_INTERNAL
 
@@ -55,11 +56,13 @@ endif
 	printf '$(subst $(sep),\n,$(PACKAGES_PERMISSIONS_TABLE))' >> $(FULL_DEVICE_TABLE)
 	echo "$(HOST_DIR)/usr/bin/makedevs -d $(FULL_DEVICE_TABLE) $(TARGET_DIR)" >> $(FAKEROOT_SCRIPT)
 endif
+	printf '$(subst $(sep),\n,$(PACKAGES_USERS))' > $(USERS_TABLE)
+	$(TOPDIR)/support/scripts/mkusers $(USERS_TABLE) $(TARGET_DIR) >> $(FAKEROOT_SCRIPT)
 	echo "$(ROOTFS_$(2)_CMD)" >> $(FAKEROOT_SCRIPT)
 	chmod a+x $(FAKEROOT_SCRIPT)
 	$(HOST_DIR)/usr/bin/fakeroot -- $(FAKEROOT_SCRIPT)
 	cp support/misc/target-dir-warning.txt $(TARGET_DIR_WARNING_FILE)
-	- at rm -f $(FAKEROOT_SCRIPT) $(FULL_DEVICE_TABLE)
+	- at rm -f $(FAKEROOT_SCRIPT) $(FULL_DEVICE_TABLE) $(USERS_TABLE)
 	$(foreach hook,$(ROOTFS_$(2)_POST_GEN_HOOKS),$(call $(hook))$(sep))
 ifeq ($$(BR2_TARGET_ROOTFS_$(2)_GZIP),y)
 	gzip -9 -c $$@ > $$@.gz
diff --git a/package/pkg-generic.mk b/package/pkg-generic.mk
index a570ad7..871544c 100644
--- a/package/pkg-generic.mk
+++ b/package/pkg-generic.mk
@@ -510,6 +510,7 @@ ifeq ($$($$($(2)_KCONFIG_VAR)),y)
 TARGETS += $(1)
 PACKAGES_PERMISSIONS_TABLE += $$($(2)_PERMISSIONS)$$(sep)
 PACKAGES_DEVICES_TABLE += $$($(2)_DEVICES)$$(sep)
+PACKAGES_USERS += $$($(2)_USERS)$$(sep)
 
 ifeq ($$($(2)_SITE_METHOD),svn)
 DL_TOOLS_DEPENDENCIES += svn
diff --git a/support/scripts/mkusers b/support/scripts/mkusers
new file mode 100755
index 0000000..9d89ff9
--- /dev/null
+++ b/support/scripts/mkusers
@@ -0,0 +1,348 @@
+#!/bin/bash
+set -e
+myname="${0##*/}"
+
+#----------------------------------------------------------------------------
+# Configurable items
+MIN_UID=1000
+MAX_UID=1999
+MIN_GID=1000
+MAX_GID=1999
+# No more is configurable below this point
+#----------------------------------------------------------------------------
+
+#----------------------------------------------------------------------------
+USERS_TABLE="${1}"
+TARGET_DIR="${2}"
+shift 2
+PASSWD="${TARGET_DIR}/etc/passwd"
+SHADOW="${TARGET_DIR}/etc/shadow"
+GROUP="${TARGET_DIR}/etc/group"
+# /etc/gsahdow is not part of the standard skeleton, so not everybody
+# will have it, but some may hav it, and its content must be in sync
+# with /etc/group, so any use of gshadow must be conditional.
+GSHADOW="${TARGET_DIR}/etc/gshadow"
+
+#----------------------------------------------------------------------------
+error() {
+    local fmt="${1}"
+    shift
+
+    printf "%s: " "${myname}" >&2
+    printf "${fmt}" "${@}" >&2
+}
+fail() {
+    error "$@"
+    exit 1
+}
+
+#----------------------------------------------------------------------------
+get_uid() {
+    local username="${1}"
+
+    grep -r -E "${username}:" "${PASSWD}" |cut -d: -f3
+}
+
+#----------------------------------------------------------------------------
+get_ugid() {
+    local username="${1}"
+
+    grep -r -E "${username}:" "${PASSWD}" |cut -d: -f4
+}
+
+#----------------------------------------------------------------------------
+get_gid() {
+    local group="${1}"
+
+    grep -r -E "${group}:" "${GROUP}" |cut -d: -f3
+}
+
+#----------------------------------------------------------------------------
+get_username() {
+    local uid="${1}"
+
+    sed -r -e '/^([^:]+):[^:]+:'"${uid}"':.*/!d; s//\1/;' "${PASSWD}"
+}
+
+#----------------------------------------------------------------------------
+get_group() {
+    local gid="${1}"
+
+    sed -r -e '/^([^:]+):[^:]+:'"${gid}"':/!d; s//\1/;' "${GROUP}"
+}
+
+#----------------------------------------------------------------------------
+get_ugroup() {
+    local username="${1}"
+    local ugid
+
+    ugid="$( get_ugid "${username}" )"
+    if [ -n "${ugid}" ]; then
+        get_group "${ugid}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+# Sanity-check the new user/group:
+#   - check the gid is not already used for another group
+#   - check the group does not already exist with another gid
+#   - check the user does not already exist with another gid
+#   - check the uid is not already used for another user
+#   - check the user does not already exist with another uid
+#   - check the user does not already exist in another group
+check_user_validity() {
+    local username="${1}"
+    local uid="${2}"
+    local group="${3}"
+    local gid="${4}"
+    local _uid _ugid _gid _username _group _ugroup
+
+    _group="$( get_group "${gid}" )"
+    _gid="$( get_gid "${group}" )"
+    _ugid="$( get_ugid "${username}" )"
+    _username="$( get_username "${uid}" )"
+    _uid="$( get_uid "${username}" )"
+    _ugroup="$( get_ugroup "${username}" )"
+
+    if [ ${gid} -ge 0 ]; then
+        # check the gid is not already used for another group
+        if [ -n "${_group}" -a "${_group}" != "${group}" ]; then
+            fail "gid is already used by group '${_group}'\n"
+        fi
+
+        # check the group does not already exists with another gid
+        if [ -n "${_gid}" -a ${_gid} -ne ${gid} ]; then
+            fail "group already exists with gid '${_gid}'\n"
+        fi
+
+        # check the user does not already exists with another gid
+        if [ -n "${_ugid}" -a ${_ugid} -ne ${gid} ]; then
+            fail "user already exists with gid '${_ugid}'\n"
+        fi
+    fi
+
+    if [ ${uid} -ge 0 ]; then
+        # check the uid is not already used for another user
+        if [ -n "${_username}" -a "${_username}" != "${username}" ]; then
+            fail "uid is already used by user '${_username}'\n"
+        fi
+
+        # check the user does not already exists with another uid
+        if [ -n "${_uid}" -a ${_uid} -ne ${uid} ]; then
+            fail "user already exists with uid '${_uid}'\n"
+        fi
+    fi
+
+    # check the user does not already exist in another group
+    if [ -n "${_ugroup}" -a "${_ugroup}" != "${group}" ]; then
+        fail "user already exists with group '${_ugroup}'\n"
+    fi
+
+    return 0
+}
+
+#----------------------------------------------------------------------------
+# Generate a unique GID for given group. If the group already exists,
+# then simply report its current GID. Otherwise, generate the lowest GID
+# that is:
+#   - not 0
+#   - comprised in [MIN_GID..MAX_GID]
+#   - not already used by a group
+generate_gid() {
+    local group="${1}"
+    local gid
+
+    gid="$( get_gid "${group}" )"
+    if [ -z "${gid}" ]; then
+        for(( gid=MIN_GID; gid<=MAX_GID; gid++ )); do
+            if [ -z "$( get_group "${gid}" )" ]; then
+                break
+            fi
+        done
+        if [ ${gid} -gt ${MAX_GID} ]; then
+            fail "can not allocate a GID for group '%s'\n" "${group}"
+        fi
+    fi
+    printf "%d\n" "${gid}"
+}
+
+#----------------------------------------------------------------------------
+# Add a group; if it does already exist, remove it first
+add_one_group() {
+    local group="${1}"
+    local gid="${2}"
+    local _f
+
+    # Generate a new GID if needed
+    if [ ${gid} -eq -1 ]; then
+        gid="$( generate_gid "${group}" )"
+    fi
+
+    # Remove any previous instance of this group
+    for _f in "${GROUP}" "${GSHADOW}"; do
+        [ -f "${_f}" ] || continue
+        sed -r -i -e '/^'"${group}"':.*/d;' "${_f}"
+    done
+
+    printf "%s:x:%d:\n" "${group}" "${gid}" >>"${GROUP}"
+    if [ -f "${GSHADOW}" ]; then
+        printf "%s:*::\n" "${group}" >>"${GSHADOW}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+# Generate a unique UID for given username. If the username already exists,
+# then simply report its current UID. Otherwise, generate the lowest UID
+# that is:
+#   - not 0
+#   - comprised in [MIN_UID..MAX_UID]
+#   - not already used by a user
+generate_uid() {
+    local username="${1}"
+    local uid
+
+    uid="$( get_uid "${username}" )"
+    if [ -z "${uid}" ]; then
+        for(( uid=MIN_UID; uid<=MAX_UID; uid++ )); do
+            if [ -z "$( get_username "${uid}" )" ]; then
+                break
+            fi
+        done
+        if [ ${uid} -gt ${MAX_UID} ]; then
+            fail "can not allocate a UID for user '%s'\n" "${username}"
+        fi
+    fi
+    printf "%d\n" "${uid}"
+}
+
+#----------------------------------------------------------------------------
+# Add given user to given group, if not already the case
+add_user_to_group() {
+    local username="${1}"
+    local group="${2}"
+    local _f
+
+    for _f in "${GROUP}" "${GSHADOW}"; do
+        [ -f "${_f}" ] || continue
+        sed -r -i -e 's/^('"${group}"':.*:)(([^:]+,)?)'"${username}"'(,[^:]+*)?$/\1\2\4/;'  \
+                  -e 's/^('"${group}"':.*)$/\1,'"${username}"'/;'                           \
+                  -e 's/,+/,/'                                                              \
+                  -e 's/:,/:/'                                                              \
+                  "${_f}"
+    done
+}
+
+#----------------------------------------------------------------------------
+# Add a user; if it does already exist, remove it first
+add_one_user() {
+    local username="${1}"
+    local uid="${2}"
+    local group="${3}"
+    local gid="${4}"
+    local passwd="${5}"
+    local home="${6}"
+    local shell="${7}"
+    local groups="${8}"
+    local comment="${9}"
+    local nb_days="$((($(date +%s)+(24*60*60-1))/(24*60*60)))"
+    local _f _group _home _shell _gid _passwd
+
+    # First, sanity-check the user
+    check_user_validity "${username}" "${uid}" "${group}" "${gid}"
+
+    # Generate a new UID if needed
+    if [ ${uid} -eq -1 ]; then
+        uid="$( generate_uid "${username}" )"
+    fi
+
+    # Remove any previous instance of this user
+    for _f in "${PASSWD}" "${SHADOW}"; do
+        sed -r -i -e '/^'"${username}"':.*/d;' "${_f}"
+    done
+
+    _gid="$( get_gid "${group}" )"
+    _shell="${shell}"
+    if [ "${shell}" = "-" ]; then
+        _shell="/bin/false"
+    fi
+    case "${home}" in
+        -)  _home="/";;
+        /)  fail "home can not be explicitly '/'\n";;
+        /*) _home="${home}";;
+        *)  fail "home must be an absolute path";;
+    esac
+    case "${passwd}" in
+        =!*|!=*)
+            _passwd='!'"$( mkpasswd -m md5 "${passwd#??}" )"
+            ;;
+        =*)
+            _passwd="$( mkpasswd -m md5 "${passwd#?}" )"
+            ;;
+        *)
+            _passwd="${passwd}"
+            ;;
+    esac
+
+    printf "%s:x:%d:%d:%s:%s:%s\n"              \
+           "${username}" "${uid}" "${_gid}"     \
+           "${comment}" "${_home}" "${_shell}"  \
+           >>"${PASSWD}"
+    printf "%s:%s:%d:0:99999:7:::\n"                \
+           "${username}" "${_passwd}" "${nb_days}"  \
+           >>"${SHADOW}"
+
+    # Add the user to its additional groups
+    if [ "${groups}" != "-" ]; then
+        for _group in ${groups//,/ }; do
+            add_user_to_group "${username}" "${_group}"
+        done
+    fi
+
+    # If the user has a home, chown it
+    # (Note: stdout goes to the fakeroot-script)
+    if [ "${home}" != "-" ]; then
+        mkdir -p "${TARGET_DIR}/${home}"
+        printf "chown -R %d:%d '%s'\n" "${uid}" "${_gid}" "${TARGET_DIR}/${home}"
+    fi
+}
+
+#----------------------------------------------------------------------------
+main() {
+    local username uid group gid passwd home shell groups comment
+
+    # Some sanity checks
+    if [ ${MIN_UID} -le 0 ]; then
+        fail "MIN_UID must be >0 (currently %d)\n" ${MIN_UID}
+    fi
+    if [ ${MIN_GID} -le 0 ]; then
+        fail "MIN_GID must be >0 (currently %d)\n" ${MIN_GID}
+    fi
+
+    # First, create all the main groups
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        add_one_group "${group}" "${gid}"
+    done <"${USERS_TABLE}"
+
+    # Then, create all the additional groups
+    # If any additional group is already a main group, we should use
+    # the gid of that main group; otherwise, we can use any gid
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        if [ "${groups}" != "-" ]; then
+            for g in ${groups//,/ }; do
+                add_one_group "${g}" -1
+            done
+        fi
+    done <"${USERS_TABLE}"
+
+    # Finally, add users
+    while read username uid group gid passwd home shell groups comment; do
+        [ -n "${username}" ] || continue    # Package with no user
+        add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
+                     "${home}" "${shell}" "${groups}" "${comment}"
+    done <"${USERS_TABLE}"
+}
+
+#----------------------------------------------------------------------------
+main "${@}"
-- 
1.7.2.5

^ permalink raw reply related	[flat|nested] 27+ messages in thread

end of thread, other threads:[~2013-04-12 17:14 UTC | newest]

Thread overview: 27+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2013-02-05 14:54 [Buildroot] [pull request v5] Pull request for branch yem-package-create-user Yann E. MORIN
2013-02-05 14:54 ` [Buildroot] [PATCH 1/2] packages: add ability for packages to create users Yann E. MORIN
2013-02-06  0:12   ` Arnout Vandecappelle
2013-02-06 22:59     ` Yann E. MORIN
2013-02-06 23:20     ` Yann E. MORIN
2013-02-08 22:02     ` Yann E. MORIN
2013-02-12  6:27       ` Arnout Vandecappelle
2013-02-05 14:54 ` [Buildroot] [PATCH 2/2] package/tvheadend: use a non-root user to run the daemon Yann E. MORIN
  -- strict thread matches above, loose matches on Subject: below --
2013-04-12 17:14 [Buildroot] [pull request v9] Pull request for branch yem-package-create-user Yann E. MORIN
2013-04-12 17:14 ` [Buildroot] [PATCH 1/2] packages: add ability for packages to create users Yann E. MORIN
2013-03-07 21:47 [Buildroot] [pull request v8] Pull request for branch yem-package-create-user Yann E. MORIN
2013-03-07 21:47 ` [Buildroot] [PATCH 1/2] packages: add ability for packages to create users Yann E. MORIN
2013-03-08 17:09   ` Yann E. MORIN
2013-03-29 14:49     ` Jeremy Rosen
2013-02-17 22:59 [Buildroot] [pull request v7 'next'] Pull request for branch yem-package-create-user Yann E. MORIN
2013-02-17 22:59 ` [Buildroot] [PATCH 1/2] packages: add ability for packages to create users Yann E. MORIN
2013-02-08 22:06 [Buildroot] [pull request v6] Pull request for branch yem-package-create-user Yann E. MORIN
2013-02-08 22:06 ` [Buildroot] [PATCH 1/2] packages: add ability for packages to create users Yann E. MORIN
2013-01-13 22:50 [Buildroot] [pull request v4] Pull request for branch yem-package-create-user Yann E. MORIN
2013-01-13 22:50 ` [Buildroot] [PATCH 1/2] packages: add ability for packages to create users Yann E. MORIN
2013-01-03 21:47 [Buildroot] [pull request v3] Pull request for branch yem-package-create-user Yann E. MORIN
2013-01-03 21:47 ` [Buildroot] [PATCH 1/2] packages: add ability for packages to create users Yann E. MORIN
2013-01-01 20:10 [Buildroot] [pull request] Pull request for branch yem-package-create-user Yann E. MORIN
2013-01-01 20:10 ` [Buildroot] [PATCH 1/2] packages: add ability for packages to create users Yann E. MORIN
2013-01-01 21:50   ` Samuel Martin
2013-01-01 22:32     ` Yann E. MORIN
2013-01-03 21:46       ` Yann E. MORIN
2013-01-02  3:40   ` Cam Hutchison
2013-01-02 18:31     ` Yann E. MORIN
2013-01-03  2:35       ` Cam Hutchison
2013-01-03 10:31         ` Thomas Petazzoni
2013-01-03 17:35           ` Yann E. MORIN
2013-01-02  3:44   ` Cam Hutchison
2013-01-02 18:05     ` Yann E. MORIN

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.