mkusers 17 KB

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