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