All of lore.kernel.org
 help / color / mirror / Atom feed
From: Arnout Vandecappelle <arnout@mind.be>
To: buildroot@busybox.net
Subject: [Buildroot] [PATCH 1/2] packages: add ability for packages to create users
Date: Wed, 06 Feb 2013 01:12:57 +0100	[thread overview]
Message-ID: <5111A009.10304@mind.be> (raw)
In-Reply-To: <5f1de03287fa8817aa692419c52e0997ec3e2489.1360075956.git.yann.morin.1998@free.fr>

  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

  reply	other threads:[~2013-02-06  0:12 UTC|newest]

Thread overview: 27+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 [this message]
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

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=5111A009.10304@mind.be \
    --to=arnout@mind.be \
    --cc=buildroot@busybox.net \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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.