diff mbox

[1/2] packages: add ability for packages to create users

Message ID 0b0fa3ae335483e6402e2c15465cacdb2d51b0d7.1357070939.git.yann.morin.1998@free.fr
State Changes Requested
Headers show

Commit Message

Yann E. MORIN Jan. 1, 2013, 8:10 p.m. UTC
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

Comments

Samuel Martin Jan. 1, 2013, 9:50 p.m. UTC | #1
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,
Yann E. MORIN Jan. 1, 2013, 10:32 p.m. UTC | #2
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.
Cam Hutchison Jan. 2, 2013, 3:40 a.m. UTC | #3
"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.
Cam Hutchison Jan. 2, 2013, 3:44 a.m. UTC | #4
"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/
Yann E. MORIN Jan. 2, 2013, 6:05 p.m. UTC | #5
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 Jan. 2, 2013, 6:31 p.m. UTC | #6
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.
Cam Hutchison Jan. 3, 2013, 2:35 a.m. UTC | #7
"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.
Thomas Petazzoni Jan. 3, 2013, 10:31 a.m. UTC | #8
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
Yann E. MORIN Jan. 3, 2013, 5:35 p.m. UTC | #9
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 Jan. 3, 2013, 9:46 p.m. UTC | #10
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.
diff mbox

Patch

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)
-	-@rm -f $(FAKEROOT_SCRIPT) $(FULL_DEVICE_TABLE)
+	-@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 "${@}"