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