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