1#!/usr/bin/env bash
2set -e
3myname="${0##*/}"
4
5#----------------------------------------------------------------------------
6# Configurable items
7FIRST_USER_UID=1000
8LAST_USER_UID=1999
9FIRST_USER_GID=1000
10LAST_USER_GID=1999
11# use names from /etc/adduser.conf
12FIRST_SYSTEM_UID=100
13LAST_SYSTEM_UID=999
14FIRST_SYSTEM_GID=100
15LAST_SYSTEM_GID=999
16# argument to automatically crease system/user id
17AUTO_SYSTEM_ID=-1
18AUTO_USER_ID=-2
19
20# No more is configurable below this point
21#----------------------------------------------------------------------------
22
23#----------------------------------------------------------------------------
24error() {
25    local fmt="${1}"
26    shift
27
28    printf "%s: " "${myname}" >&2
29    # shellcheck disable=SC2059  # fmt is the format passed to error()
30    printf "${fmt}" "${@}" >&2
31}
32fail() {
33    error "$@"
34    exit 1
35}
36
37#----------------------------------------------------------------------------
38if [ ${#} -ne 2 ]; then
39    fail "usage: %s USERS_TABLE TARGET_DIR\n"
40fi
41USERS_TABLE="${1}"
42TARGET_DIR="${2}"
43shift 2
44PASSWD="${TARGET_DIR}/etc/passwd"
45SHADOW="${TARGET_DIR}/etc/shadow"
46GROUP="${TARGET_DIR}/etc/group"
47# /etc/gshadow is not part of the standard skeleton, so not everybody
48# will have it, but some may have it, and its content must be in sync
49# with /etc/group, so any use of gshadow must be conditional.
50GSHADOW="${TARGET_DIR}/etc/gshadow"
51
52# We can't simply source ${BR2_CONFIG} as it may contains constructs
53# such as:
54#    BR2_DEFCONFIG="$(CONFIG_DIR)/defconfig"
55# which when sourced from a shell script will eventually try to execute
56# a command named 'CONFIG_DIR', which is plain wrong for virtually every
57# systems out there.
58# So, we have to scan that file instead. Sigh... :-(
59PASSWD_METHOD="$( sed -r -e '/^BR2_TARGET_GENERIC_PASSWD_METHOD="(.*)"$/!d;'    \
60                         -e 's//\1/;'                                           \
61                         "${BR2_CONFIG}"                                        \
62                )"
63
64#----------------------------------------------------------------------------
65get_uid() {
66    local username="${1}"
67
68    awk -F: -v username="${username}"                           \
69        '$1 == username { printf( "%d\n", $3 ); }' "${PASSWD}"
70}
71
72#----------------------------------------------------------------------------
73get_ugid() {
74    local username="${1}"
75
76    awk -F: -v username="${username}"                          \
77        '$1 == username { printf( "%d\n", $4 ); }' "${PASSWD}"
78}
79
80#----------------------------------------------------------------------------
81get_gid() {
82    local group="${1}"
83
84    awk -F: -v group="${group}"                             \
85        '$1 == group { printf( "%d\n", $3 ); }' "${GROUP}"
86}
87
88#----------------------------------------------------------------------------
89get_members() {
90    local group="${1}"
91
92    awk -F: -v group="${group}"                             \
93        '$1 == group { printf( "%s\n", $4 ); }' "${GROUP}"
94}
95
96#----------------------------------------------------------------------------
97get_username() {
98    local uid="${1}"
99
100    awk -F: -v uid="${uid}"                                 \
101        '$3 == uid { printf( "%s\n", $1 ); }' "${PASSWD}"
102}
103
104#----------------------------------------------------------------------------
105get_group() {
106    local gid="${1}"
107
108    awk -F: -v gid="${gid}"                             \
109        '$3 == gid { printf( "%s\n", $1 ); }' "${GROUP}"
110}
111
112#----------------------------------------------------------------------------
113get_ugroup() {
114    local username="${1}"
115    local ugid
116
117    ugid="$( get_ugid "${username}" )"
118    if [ -n "${ugid}" ]; then
119        get_group "${ugid}"
120    fi
121}
122
123#----------------------------------------------------------------------------
124# Sanity-check the new user/group:
125#   - check the gid is not already used for another group
126#   - check the group does not already exist with another gid
127#   - check the user does not already exist with another gid
128#   - check the uid is not already used for another user
129#   - check the user does not already exist with another uid
130#   - check the user does not already exist in another group
131check_user_validity() {
132    local username="${1}"
133    local uid="${2}"
134    local group="${3}"
135    local gid="${4}"
136    local _uid _ugid _gid _username _group _ugroup
137
138    _group="$( get_group "${gid}" )"
139    _gid="$( get_gid "${group}" )"
140    _ugid="$( get_ugid "${username}" )"
141    _username="$( get_username "${uid}" )"
142    _uid="$( get_uid "${username}" )"
143    _ugroup="$( get_ugroup "${username}" )"
144
145    if [ "${username}" = "root" ]; then
146        fail "invalid username '%s\n'" "${username}"
147    fi
148
149    # shellcheck disable=SC2086  # gid is a non-empty int
150    # shellcheck disable=SC2166  # [ .. -o .. ] works well in this case
151    if [ ${gid} -lt -2 -o ${gid} -eq 0 ]; then
152        fail "invalid gid '%d' for '%s'\n" ${gid} "${username}"
153    elif [ ${gid} -ge 0 ]; then
154        # check the gid is not already used for another group
155        if [ -n "${_group}" -a "${_group}" != "${group}" ]; then
156            fail "gid '%d' for '%s' is already used by group '%s'\n" \
157                 ${gid} "${username}" "${_group}"
158        fi
159
160        # check the group does not already exists with another gid
161        # Need to split the check in two, otherwise '[' complains it
162        # is missing arguments when _gid is empty
163        if [ -n "${_gid}" ] && [ ${_gid} -ne ${gid} ]; then
164            fail "group '%s' for '%s' already exists with gid '%d' (wants '%d')\n" \
165                 "${group}" "${username}" ${_gid} ${gid}
166        fi
167
168        # check the user does not already exists with another gid
169        # Need to split the check in two, otherwise '[' complains it
170        # is missing arguments when _ugid is empty
171        if [ -n "${_ugid}" ] && [ ${_ugid} -ne ${gid} ]; then
172            fail "user '%s' already exists with gid '%d' (wants '%d')\n" \
173                 "${username}" ${_ugid} ${gid}
174        fi
175    fi
176
177    # shellcheck disable=SC2086  # uid is a non-empty int
178    # shellcheck disable=SC2166  # [ .. -o .. ] works well in this case
179    if [ ${uid} -lt -2 -o ${uid} -eq 0 ]; then
180        fail "invalid uid '%d' for '%s'\n" ${uid} "${username}"
181    elif [ ${uid} -ge 0 ]; then
182        # check the uid is not already used for another user
183        if [ -n "${_username}" -a "${_username}" != "${username}" ]; then
184            fail "uid '%d' for '%s' already used by user '%s'\n" \
185                 ${uid} "${username}" "${_username}"
186        fi
187
188        # check the user does not already exists with another uid
189        # Need to split the check in two, otherwise '[' complains it
190        # is missing arguments when _uid is empty
191        if [ -n "${_uid}" ] && [ ${_uid} -ne ${uid} ]; then
192            fail "user '%s' already exists with uid '%d' (wants '%d')\n" \
193                 "${username}" ${_uid} ${uid}
194        fi
195    fi
196
197    # check the user does not already exist in another group
198    # shellcheck disable=SC2166  # [ .. -a .. ] works well in this case
199    if [ -n "${_ugroup}" -a "${_ugroup}" != "${group}" ]; then
200        fail "user '%s' already exists with group '%s' (wants '%s')\n" \
201             "${username}" "${_ugroup}" "${group}"
202    fi
203
204    return 0
205}
206
207#----------------------------------------------------------------------------
208# Generate a unique GID for given group. If the group already exists,
209# then simply report its current GID. Otherwise, generate the lowest GID
210# that is:
211#   - not 0
212#   - comprised in [$2..$3]
213#   - not already used by a group
214generate_gid() {
215    local group="${1}"
216    local mingid="${2}"
217    local maxgid="${3}"
218    local gid
219
220    gid="$( get_gid "${group}" )"
221    if [ -z "${gid}" ]; then
222        for(( gid=mingid; gid<=maxgid; gid++ )); do
223            if [ -z "$( get_group "${gid}" )" ]; then
224                break
225            fi
226        done
227        # shellcheck disable=SC2086  # gid and maxgid are non-empty ints
228        if [ ${gid} -gt ${maxgid} ]; then
229            fail "can not allocate a GID for group '%s'\n" "${group}"
230        fi
231    fi
232    printf "%d\n" "${gid}"
233}
234
235#----------------------------------------------------------------------------
236# Add a group; if it does already exist, remove it first
237add_one_group() {
238    local group="${1}"
239    local gid="${2}"
240    local members
241
242    # Generate a new GID if needed
243    # shellcheck disable=SC2086  # gid is a non-empty int
244    if [ ${gid} -eq ${AUTO_USER_ID} ]; then
245        gid="$( generate_gid "${group}" $FIRST_USER_GID $LAST_USER_GID )"
246    elif [ ${gid} -eq ${AUTO_SYSTEM_ID} ]; then
247        gid="$( generate_gid "${group}" $FIRST_SYSTEM_GID $LAST_SYSTEM_GID )"
248    fi
249
250    members=$(get_members "$group")
251    # Remove any previous instance of this group, and re-add the new one
252    sed -i --follow-symlinks -e '/^'"${group}"':.*/d;' "${GROUP}"
253    printf "%s:x:%d:%s\n" "${group}" "${gid}" "${members}" >>"${GROUP}"
254
255    # Ditto for /etc/gshadow if it exists
256    if [ -f "${GSHADOW}" ]; then
257        sed -i --follow-symlinks -e '/^'"${group}"':.*/d;' "${GSHADOW}"
258        printf "%s:*::\n" "${group}" >>"${GSHADOW}"
259    fi
260}
261
262#----------------------------------------------------------------------------
263# Generate a unique UID for given username. If the username already exists,
264# then simply report its current UID. Otherwise, generate the lowest UID
265# that is:
266#   - not 0
267#   - comprised in [$2..$3]
268#   - not already used by a user
269generate_uid() {
270    local username="${1}"
271    local minuid="${2}"
272    local maxuid="${3}"
273
274    local uid
275
276    uid="$( get_uid "${username}" )"
277    if [ -z "${uid}" ]; then
278        for(( uid=minuid; uid<=maxuid; uid++ )); do
279            if [ -z "$( get_username "${uid}" )" ]; then
280                break
281            fi
282        done
283        # shellcheck disable=SC2086  # uid is a non-empty int
284        if [ ${uid} -gt ${maxuid} ]; then
285            fail "can not allocate a UID for user '%s'\n" "${username}"
286        fi
287    fi
288    printf "%d\n" "${uid}"
289}
290
291#----------------------------------------------------------------------------
292# Add given user to given group, if not already the case
293add_user_to_group() {
294    local username="${1}"
295    local group="${2}"
296    local _f
297
298    for _f in "${GROUP}" "${GSHADOW}"; do
299        [ -f "${_f}" ] || continue
300        sed -r -i --follow-symlinks \
301                  -e 's/^('"${group}"':.*:)(([^:]+,)?)'"${username}"'(,[^:]+*)?$/\1\2\4/;'  \
302                  -e 's/^('"${group}"':.*)$/\1,'"${username}"'/;'                           \
303                  -e 's/,+/,/'                                                              \
304                  -e 's/:,/:/'                                                              \
305                  "${_f}"
306    done
307}
308
309#----------------------------------------------------------------------------
310# Encode a password
311encode_password() {
312    local passwd="${1}"
313
314    mkpasswd -m "${PASSWD_METHOD}" "${passwd}"
315}
316
317#----------------------------------------------------------------------------
318# Add a user; if it does already exist, remove it first
319add_one_user() {
320    local username="${1}"
321    local uid="${2}"
322    local group="${3}"
323    local gid="${4}"
324    local passwd="${5}"
325    local home="${6}"
326    local shell="${7}"
327    local groups="${8}"
328    local comment="${9}"
329    local _f _group _home _shell _gid _passwd
330
331    # First, sanity-check the user
332    check_user_validity "${username}" "${uid}" "${group}" "${gid}"
333
334    # Generate a new UID if needed
335    # shellcheck disable=SC2086  # uid is a non-empty int
336    if [ ${uid} -eq ${AUTO_USER_ID} ]; then
337        uid="$( generate_uid "${username}" $FIRST_USER_UID $LAST_USER_UID )"
338    elif [ ${uid} -eq ${AUTO_SYSTEM_ID} ]; then
339        uid="$( generate_uid "${username}" $FIRST_SYSTEM_UID $LAST_SYSTEM_UID )"
340    fi
341
342    # Remove any previous instance of this user
343    for _f in "${PASSWD}" "${SHADOW}"; do
344        sed -r -i --follow-symlinks -e '/^'"${username}"':.*/d;' "${_f}"
345    done
346
347    _gid="$( get_gid "${group}" )"
348    _shell="${shell}"
349    if [ "${shell}" = "-" ]; then
350        _shell="/bin/false"
351    fi
352    case "${home}" in
353        -)  _home="/";;
354        /)  fail "home can not explicitly be '/'\n";;
355        /*) _home="${home}";;
356        *)  fail "home must be an absolute path\n";;
357    esac
358    case "${passwd}" in
359        -)
360            _passwd=""
361            ;;
362        !=*)
363            _passwd='!'"$( encode_password "${passwd#!=}" )"
364            ;;
365        =*)
366            _passwd="$( encode_password "${passwd#=}" )"
367            ;;
368        *)
369            _passwd="${passwd}"
370            ;;
371    esac
372
373    printf "%s:x:%d:%d:%s:%s:%s\n"              \
374           "${username}" "${uid}" "${_gid}"     \
375           "${comment}" "${_home}" "${_shell}"  \
376           >>"${PASSWD}"
377    printf "%s:%s:::::::\n"      \
378           "${username}" "${_passwd}"   \
379           >>"${SHADOW}"
380
381    # Add the user to its additional groups
382    if [ "${groups}" != "-" ]; then
383        for _group in ${groups//,/ }; do
384            add_user_to_group "${username}" "${_group}"
385        done
386    fi
387
388    # If the user has a home, chown it
389    # (Note: stdout goes to the fakeroot-script)
390    if [ "${home}" != "-" ]; then
391        mkdir -p "${TARGET_DIR}/${home}"
392        printf "chown -h -R %d:%d '%s'\n" "${uid}" "${_gid}" "${TARGET_DIR}/${home}"
393    fi
394}
395
396#----------------------------------------------------------------------------
397main() {
398    local username uid group gid passwd home shell groups comment
399    local line
400    local auto_id
401    local -a ENTRIES
402
403    # Some sanity checks
404    if [ ${FIRST_USER_UID} -le 0 ]; then
405        fail "FIRST_USER_UID must be >0 (currently %d)\n" ${FIRST_USER_UID}
406    fi
407    if [ ${FIRST_USER_GID} -le 0 ]; then
408        fail "FIRST_USER_GID must be >0 (currently %d)\n" ${FIRST_USER_GID}
409    fi
410
411    # Read in all the file in memory, exclude empty lines and comments
412    while read -r line; do
413        ENTRIES+=( "${line}" )
414    done < <( sed -r -e 's/#.*//; /^[[:space:]]*$/d;' "${USERS_TABLE}" )
415
416    # We first create groups whose gid is positive, and then we create groups
417    # whose gid is automatic, so that, if a group is defined both with
418    # a specified gid and an automatic gid, we ensure the specified gid is
419    # used, rather than a different automatic gid is computed.
420
421    # First, create all the main groups which gid is *not* automatic
422    for line in "${ENTRIES[@]}"; do
423        read -r username uid group gid passwd home shell groups comment <<<"${line}"
424        # shellcheck disable=SC2086  # gid is a non-empty int
425        [ ${gid} -ge 0 ] || continue    # Automatic gid
426        add_one_group "${group}" "${gid}"
427    done
428
429    # Then, create all the main groups which gid *is* automatic
430    for line in "${ENTRIES[@]}"; do
431        read -r username uid group gid passwd home shell groups comment <<<"${line}"
432        # shellcheck disable=SC2086  # gid is a non-empty int
433        [ ${gid} -lt 0 ] || continue    # Non-automatic gid
434        add_one_group "${group}" "${gid}"
435    done
436
437    # Then, create all the additional groups
438    # If any additional group is already a main group, we should use
439    # the gid of that main group; otherwise, we can use any gid - a
440    # system gid if the uid is a system user (<= LAST_SYSTEM_UID),
441    # otherwise a user gid.
442    for line in "${ENTRIES[@]}"; do
443        read -r username uid group gid passwd home shell groups comment <<<"${line}"
444        if [ "${groups}" != "-" ]; then
445            # shellcheck disable=SC2086  # uid is a non-empty int
446            if [ ${uid} -le 0 ]; then
447                auto_id=${uid}
448            elif [ ${uid} -le ${LAST_SYSTEM_UID} ]; then
449                auto_id=${AUTO_SYSTEM_ID}
450            else
451                auto_id=${AUTO_USER_ID}
452            fi
453            for g in ${groups//,/ }; do
454                add_one_group "${g}" ${auto_id}
455            done
456        fi
457    done
458
459    # When adding users, we do as for groups, in case two packages create
460    # the same user, one with an automatic uid, the other with a specified
461    # uid, to ensure the specified uid is used, rather than an incompatible
462    # uid be generated.
463
464    # Now, add users whose uid is *not* automatic
465    for line in "${ENTRIES[@]}"; do
466        read -r username uid group gid passwd home shell groups comment <<<"${line}"
467        [ "${username}" != "-" ] || continue # Magic string to skip user creation
468        # shellcheck disable=SC2086  # uid is a non-empty int
469        [ ${uid} -ge 0         ] || continue # Automatic uid
470        add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
471                     "${home}" "${shell}" "${groups}" "${comment}"
472    done
473
474    # Finally, add users whose uid *is* automatic
475    for line in "${ENTRIES[@]}"; do
476        read -r username uid group gid passwd home shell groups comment <<<"${line}"
477        [ "${username}" != "-" ] || continue # Magic string to skip user creation
478        # shellcheck disable=SC2086  # uid is a non-empty int
479        [ ${uid} -lt 0        ] || continue # Non-automatic uid
480        add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
481                     "${home}" "${shell}" "${groups}" "${comment}"
482    done
483}
484
485#----------------------------------------------------------------------------
486main "${@}"
487