b70698356a3ab85bd9839c441cf42da47c00a1c0
[gentoo.git] / eclass / user.eclass
1 # Copyright 1999-2020 Gentoo Authors
2 # Distributed under the terms of the GNU General Public License v2
3
4 # @ECLASS: user.eclass
5 # @MAINTAINER:
6 # base-system@gentoo.org (Linux)
7 # Michał Górny <mgorny@gentoo.org> (NetBSD)
8 # @BLURB: user management in ebuilds
9 # @DESCRIPTION:
10 # The user eclass contains a suite of functions that allow ebuilds
11 # to quickly make sure users in the installed system are sane.
12
13 if [[ -z ${_USER_ECLASS} ]]; then
14 _USER_ECLASS=1
15
16 inherit user-info
17
18 # @FUNCTION: _assert_pkg_ebuild_phase
19 # @INTERNAL
20 # @USAGE: <calling func name>
21 _assert_pkg_ebuild_phase() {
22         case ${EBUILD_PHASE} in
23         setup|preinst|postinst|prerm|postrm) ;;
24         *)
25                 eerror "'$1()' called from '${EBUILD_PHASE}' phase which is not OK:"
26                 eerror "You may only call from pkg_{setup,{pre,post}{inst,rm}} functions."
27                 eerror "Package fails at QA and at life.  Please file a bug."
28                 die "Bad package!  $1 is only for use in some pkg_* functions!"
29         esac
30 }
31
32 # @FUNCTION: user_get_nologin
33 # @INTERNAL
34 # @DESCRIPTION:
35 # Find an appropriate 'nologin' shell for the platform, and output
36 # its path.
37 user_get_nologin() {
38         local eshell
39
40         for eshell in /sbin/nologin /usr/sbin/nologin /bin/false /usr/bin/false /dev/null ; do
41                 [[ -x ${ROOT}${eshell} ]] && break
42         done
43
44         if [[ ${eshell} == "/dev/null" ]] ; then
45                 ewarn "Unable to identify the shell to use, proceeding with userland default."
46                 case ${USERLAND} in
47                         GNU)    eshell="/bin/false" ;;
48                         BSD)    eshell="/sbin/nologin" ;;
49                         Darwin) eshell="/usr/sbin/nologin" ;;
50                         *) die "Unable to identify the default shell for userland ${USERLAND}"
51                 esac
52         fi
53
54         echo "${eshell}"
55 }
56
57 # @FUNCTION: enewuser
58 # @USAGE: <user> [-F] [-M] [uid] [shell] [homedir] [groups]
59 # @DESCRIPTION:
60 # Same as enewgroup, you are not required to understand how to properly add
61 # a user to the system.  The only required parameter is the username.
62 # Default uid is (pass -1 for this) next available, default shell is
63 # /bin/false, default homedir is /dev/null, and there are no default groups.
64 #
65 # If -F is passed, enewuser will always enforce specified UID and fail if it
66 # can not be assigned.
67 # If -M is passed, enewuser does not create the home directory if it does not
68 # exist.
69 enewuser() {
70         if [[ ${EUID} != 0 ]] ; then
71                 ewarn "Insufficient privileges to execute ${FUNCNAME[0]}"
72                 return 0
73         fi
74         _assert_pkg_ebuild_phase ${FUNCNAME}
75
76         local create_home=1 force_uid=
77         while [[ $1 == -* ]]; do
78                 case $1 in
79                         -F) force_uid=1;;
80                         -M) create_home=;;
81                         *) die "${FUNCNAME}: invalid option ${1}";;
82                 esac
83                 shift
84         done
85
86         # get the username
87         local euser=$1; shift
88         if [[ -z ${euser} ]] ; then
89                 eerror "No username specified !"
90                 die "Cannot call enewuser without a username"
91         fi
92
93         # lets see if the username already exists
94         if [[ -n $(egetent passwd "${euser}") ]] ; then
95                 return 0
96         fi
97         elog "Adding user '${euser}' to your system ..."
98
99         # options to pass to useradd
100         local opts=()
101
102         # handle uid
103         local euid=$1; shift
104         if [[ -n ${euid} && ${euid} != -1 ]] ; then
105                 if [[ ${euid} -gt 0 ]] ; then
106                         if [[ -n $(egetent passwd ${euid}) ]] ; then
107                                 [[ -n ${force_uid} ]] && die "${FUNCNAME}: UID ${euid} already taken"
108                                 euid="next"
109                         fi
110                 else
111                         eerror "Userid given but is not greater than 0 !"
112                         die "${euid} is not a valid UID"
113                 fi
114         else
115                 [[ -n ${force_uid} ]] && die "${FUNCNAME}: -F with uid==-1 makes no sense"
116                 euid="next"
117         fi
118         if [[ ${euid} == "next" ]] ; then
119                 for ((euid = 999; euid >= 101; euid--)); do
120                         [[ -z $(egetent passwd ${euid}) ]] && break
121                 done
122                 [[ ${euid} -ge 101 ]] || die "${FUNCNAME}: no free UID found"
123         fi
124         opts+=( -u ${euid} )
125         elog " - Userid: ${euid}"
126
127         # handle shell
128         local eshell=$1; shift
129         if [[ ! -z ${eshell} ]] && [[ ${eshell} != "-1" ]] ; then
130                 if [[ ! -e ${ROOT}${eshell} ]] ; then
131                         eerror "A shell was specified but it does not exist !"
132                         die "${eshell} does not exist in ${ROOT}"
133                 fi
134                 if [[ ${eshell} == */false || ${eshell} == */nologin ]] ; then
135                         eerror "Do not specify ${eshell} yourself, use -1"
136                         die "Pass '-1' as the shell parameter"
137                 fi
138         else
139                 eshell=$(user_get_nologin)
140         fi
141         elog " - Shell: ${eshell}"
142         opts+=( -s "${eshell}" )
143
144         # handle homedir
145         local ehome=$1; shift
146         if [[ -z ${ehome} ]] || [[ ${ehome} == "-1" ]] ; then
147                 ehome="/dev/null"
148         fi
149         elog " - Home: ${ehome}"
150         opts+=( -d "${ehome}" )
151
152         # handle groups
153         local egroups=$1; shift
154         local g egroups_arr
155         IFS="," read -r -a egroups_arr <<<"${egroups}"
156         if [[ ${#egroups_arr[@]} -gt 0 ]] ; then
157                 local defgroup exgroups
158                 for g in "${egroups_arr[@]}" ; do
159                         if [[ -z $(egetent group "${g}") ]] ; then
160                                 eerror "You must add group ${g} to the system first"
161                                 die "${g} is not a valid GID"
162                         fi
163                         if [[ -z ${defgroup} ]] ; then
164                                 defgroup=${g}
165                         else
166                                 exgroups+=",${g}"
167                         fi
168                 done
169                 opts+=( -g "${defgroup}" )
170                 if [[ ! -z ${exgroups} ]] ; then
171                         opts+=( -G "${exgroups:1}" )
172                 fi
173         fi
174         elog " - Groups: ${egroups:-(none)}"
175
176         # handle extra args
177         if [[ $# -gt 0 ]] ; then
178                 die "extra arguments no longer supported; please file a bug"
179         else
180                 local comment="added by portage for ${PN}"
181                 opts+=( -c "${comment}" )
182                 elog " - GECOS: ${comment}"
183         fi
184
185         # add the user
186         case ${CHOST} in
187         *-freebsd*|*-dragonfly*)
188                 pw useradd "${euser}" "${opts[@]}" || die
189                 ;;
190
191         *-netbsd*)
192                 useradd "${opts[@]}" "${euser}" || die
193                 ;;
194
195         *-openbsd*)
196                 # all ops the same, except the -g vs -g/-G ...
197                 useradd -u ${euid} -s "${eshell}" \
198                         -d "${ehome}" -g "${egroups}" "${euser}" || die
199                 ;;
200
201         *)
202                 useradd -M -N -r "${opts[@]}" "${euser}" || die
203                 ;;
204         esac
205
206         if [[ -n ${create_home} && ! -e ${ROOT}/${ehome} ]] ; then
207                 elog " - Creating ${ehome} in ${ROOT}"
208                 mkdir -p "${ROOT}/${ehome}"
209                 chown "${euser}" "${ROOT}/${ehome}"
210                 chmod 755 "${ROOT}/${ehome}"
211         fi
212 }
213
214 # @FUNCTION: enewgroup
215 # @USAGE: <group> [gid]
216 # @DESCRIPTION:
217 # This function does not require you to understand how to properly add a
218 # group to the system.  Just give it a group name to add and enewgroup will
219 # do the rest.  You may specify the gid for the group or allow the group to
220 # allocate the next available one.
221 #
222 # If -F is passed, enewgroup will always enforce specified GID and fail if it
223 # can not be assigned.
224 enewgroup() {
225         if [[ ${EUID} != 0 ]] ; then
226                 ewarn "Insufficient privileges to execute ${FUNCNAME[0]}"
227                 return 0
228         fi
229         _assert_pkg_ebuild_phase ${FUNCNAME}
230
231         local force_gid=
232         while [[ $1 == -* ]]; do
233                 case $1 in
234                         -F) force_gid=1;;
235                         *) die "${FUNCNAME}: invalid option ${1}";;
236                 esac
237                 shift
238         done
239
240         # get the group
241         local egroup=$1; shift
242         if [[ -z ${egroup} ]] ; then
243                 eerror "No group specified !"
244                 die "Cannot call enewgroup without a group"
245         fi
246
247         # see if group already exists
248         if [[ -n $(egetent group "${egroup}") ]] ; then
249                 return 0
250         fi
251         elog "Adding group '${egroup}' to your system ..."
252
253         # handle gid
254         local egid=$1; shift
255         if [[ -n ${egid} && ${egid} != -1 ]] ; then
256                 if [[ ${egid} -gt 0 ]] ; then
257                         if [[ -n $(egetent group ${egid}) ]] ; then
258                                 [[ -n ${force_gid} ]] && die "${FUNCNAME}: GID ${egid} already taken"
259                                 egid="next available; requested gid taken"
260                         fi
261                 else
262                         eerror "Groupid given but is not greater than 0 !"
263                         die "${egid} is not a valid GID"
264                 fi
265         else
266                 [[ -n ${force_gid} ]] && die "${FUNCNAME}: -F with gid==-1 makes no sense"
267                 egid="next available"
268         fi
269         elog " - Groupid: ${egid}"
270
271         # handle extra
272         if [[ $# -gt 0 ]] ; then
273                 die "extra arguments no longer supported; please file a bug"
274         fi
275
276         # Some targets need to find the next available GID manually
277         _enewgroup_next_gid() {
278                 if [[ ${egid} == *[!0-9]* ]] ; then
279                         # Non numeric
280                         for ((egid = 999; egid >= 101; egid--)) ; do
281                                 [[ -z $(egetent group ${egid}) ]] && break
282                         done
283                         [[ ${egid} -ge 101 ]] || die "${FUNCNAME}: no free GID found"
284                 fi
285         }
286
287         # add the group
288         case ${CHOST} in
289         *-freebsd*|*-dragonfly*)
290                 _enewgroup_next_gid
291                 pw groupadd "${egroup}" -g ${egid} || die
292                 ;;
293
294         *-netbsd*)
295                 _enewgroup_next_gid
296                 groupadd -g ${egid} "${egroup}" || die
297                 ;;
298
299         *)
300                 local opts
301                 if [[ ${egid} == *[!0-9]* ]] ; then
302                         # Non numeric; let groupadd figure out a GID for us
303                         opts=""
304                 else
305                         opts="-g ${egid}"
306                 fi
307                 # We specify -r so that we get a GID in the system range from login.defs
308                 groupadd -r ${opts} "${egroup}" || die
309                 ;;
310         esac
311 }
312
313 # @FUNCTION: esethome
314 # @USAGE: <user> <homedir>
315 # @DESCRIPTION:
316 # Update the home directory in a platform-agnostic way.
317 # Required parameters is the username and the new home directory.
318 # Specify -1 if you want to set home to the enewuser default
319 # of /dev/null.
320 # If the new home directory does not exist, it is created.
321 # Any previously existing home directory is NOT moved.
322 esethome() {
323         _assert_pkg_ebuild_phase ${FUNCNAME}
324
325         # get the username
326         local euser=$1; shift
327         if [[ -z ${euser} ]] ; then
328                 eerror "No username specified !"
329                 die "Cannot call esethome without a username"
330         fi
331
332         # lets see if the username already exists
333         if [[ -z $(egetent passwd "${euser}") ]] ; then
334                 ewarn "User does not exist, cannot set home dir -- skipping."
335                 return 1
336         fi
337
338         # handle homedir
339         local ehome=$1; shift
340         if [[ -z ${ehome} ]] ; then
341                 eerror "No home directory specified !"
342                 die "Cannot call esethome without a home directory or '-1'"
343         fi
344
345         if [[ ${ehome} == "-1" ]] ; then
346                 ehome="/dev/null"
347         fi
348
349         # exit with no message if home dir is up to date
350         if [[ $(egethome "${euser}") == ${ehome} ]]; then
351                 return 0
352         fi
353
354         elog "Updating home for user '${euser}' ..."
355         elog " - Home: ${ehome}"
356
357         # ensure home directory exists, otherwise update will fail
358         if [[ ! -e ${ROOT}/${ehome} ]] ; then
359                 elog " - Creating ${ehome} in ${ROOT}"
360                 mkdir -p "${ROOT}/${ehome}"
361                 chown "${euser}" "${ROOT}/${ehome}"
362                 chmod 755 "${ROOT}/${ehome}"
363         fi
364
365         # update the home directory
366         case ${CHOST} in
367         *-freebsd*|*-dragonfly*)
368                 pw usermod "${euser}" -d "${ehome}" && return 0
369                 [[ $? == 8 ]] && eerror "${euser} is in use, cannot update home"
370                 eerror "There was an error when attempting to update the home directory for ${euser}"
371                 eerror "Please update it manually on your system:"
372                 eerror "\t pw usermod \"${euser}\" -d \"${ehome}\""
373                 ;;
374
375         *)
376                 usermod -d "${ehome}" "${euser}" && return 0
377                 [[ $? == 8 ]] && eerror "${euser} is in use, cannot update home"
378                 eerror "There was an error when attempting to update the home directory for ${euser}"
379                 eerror "Please update it manually on your system (as root):"
380                 eerror "\t usermod -d \"${ehome}\" \"${euser}\""
381                 ;;
382         esac
383 }
384
385 # @FUNCTION: esetshell
386 # @USAGE: <user> <shell>
387 # @DESCRIPTION:
388 # Update the shell in a platform-agnostic way.
389 # Required parameters is the username and the new shell.
390 # Specify -1 if you want to set shell to platform-specific nologin.
391 esetshell() {
392         _assert_pkg_ebuild_phase ${FUNCNAME}
393
394         # get the username
395         local euser=$1; shift
396         if [[ -z ${euser} ]] ; then
397                 eerror "No username specified !"
398                 die "Cannot call esetshell without a username"
399         fi
400
401         # lets see if the username already exists
402         if [[ -z $(egetent passwd "${euser}") ]] ; then
403                 ewarn "User does not exist, cannot set shell -- skipping."
404                 return 1
405         fi
406
407         # handle shell
408         local eshell=$1; shift
409         if [[ -z ${eshell} ]] ; then
410                 eerror "No shell specified !"
411                 die "Cannot call esetshell without a shell or '-1'"
412         fi
413
414         if [[ ${eshell} == "-1" ]] ; then
415                 eshell=$(user_get_nologin)
416         fi
417
418         # exit with no message if shell is up to date
419         if [[ $(egetshell "${euser}") == ${eshell} ]]; then
420                 return 0
421         fi
422
423         elog "Updating shell for user '${euser}' ..."
424         elog " - Shell: ${eshell}"
425
426         # update the shell
427         case ${CHOST} in
428         *-freebsd*|*-dragonfly*)
429                 pw usermod "${euser}" -s "${eshell}" && return 0
430                 [[ $? == 8 ]] && eerror "${euser} is in use, cannot update shell"
431                 eerror "There was an error when attempting to update the shell for ${euser}"
432                 eerror "Please update it manually on your system:"
433                 eerror "\t pw usermod \"${euser}\" -s \"${eshell}\""
434                 ;;
435
436         *)
437                 usermod -s "${eshell}" "${euser}" && return 0
438                 [[ $? == 8 ]] && eerror "${euser} is in use, cannot update shell"
439                 eerror "There was an error when attempting to update the shell for ${euser}"
440                 eerror "Please update it manually on your system (as root):"
441                 eerror "\t usermod -s \"${eshell}\" \"${euser}\""
442                 ;;
443         esac
444 }
445
446 # @FUNCTION: esetcomment
447 # @USAGE: <user> <comment>
448 # @DESCRIPTION:
449 # Update the comment field in a platform-agnostic way.
450 # Required parameters is the username and the new comment.
451 esetcomment() {
452         _assert_pkg_ebuild_phase ${FUNCNAME}
453
454         # get the username
455         local euser=$1; shift
456         if [[ -z ${euser} ]] ; then
457                 eerror "No username specified !"
458                 die "Cannot call esetcomment without a username"
459         fi
460
461         # lets see if the username already exists
462         if [[ -z $(egetent passwd "${euser}") ]] ; then
463                 ewarn "User does not exist, cannot set comment -- skipping."
464                 return 1
465         fi
466
467         # handle comment
468         local ecomment=$1; shift
469         if [[ -z ${ecomment} ]] ; then
470                 eerror "No comment specified !"
471                 die "Cannot call esetcomment without a comment"
472         fi
473
474         # exit with no message if comment is up to date
475         if [[ $(egetcomment "${euser}") == ${ecomment} ]]; then
476                 return 0
477         fi
478
479         elog "Updating comment for user '${euser}' ..."
480         elog " - Comment: ${ecomment}"
481
482         # update the comment
483         case ${CHOST} in
484         *-freebsd*|*-dragonfly*)
485                 pw usermod "${euser}" -c "${ecomment}" && return 0
486                 [[ $? == 8 ]] && eerror "${euser} is in use, cannot update comment"
487                 eerror "There was an error when attempting to update the comment for ${euser}"
488                 eerror "Please update it manually on your system:"
489                 eerror "\t pw usermod \"${euser}\" -c \"${ecomment}\""
490                 ;;
491
492         *)
493                 usermod -c "${ecomment}" "${euser}" && return 0
494                 [[ $? == 8 ]] && eerror "${euser} is in use, cannot update comment"
495                 eerror "There was an error when attempting to update the comment for ${euser}"
496                 eerror "Please update it manually on your system (as root):"
497                 eerror "\t usermod -c \"${ecomment}\" \"${euser}\""
498                 ;;
499         esac
500 }
501
502 # @FUNCTION: esetgroups
503 # @USAGE: <user> <groups>
504 # @DESCRIPTION:
505 # Update the group field in a platform-agnostic way.
506 # Required parameters is the username and the new list of groups,
507 # primary group first.
508 esetgroups() {
509         _assert_pkg_ebuild_phase ${FUNCNAME}
510
511         [[ ${#} -eq 2 ]] || die "Usage: ${FUNCNAME} <user> <groups>"
512
513         # get the username
514         local euser=$1; shift
515
516         # lets see if the username already exists
517         if [[ -z $(egetent passwd "${euser}") ]] ; then
518                 ewarn "User does not exist, cannot set group -- skipping."
519                 return 1
520         fi
521
522         # handle group
523         local egroups=$1; shift
524
525         local g egroups_arr=()
526         IFS="," read -r -a egroups_arr <<<"${egroups}"
527         [[ ${#egroups_arr[@]} -gt 0 ]] || die "${FUNCNAME}: no groups specified"
528
529         for g in "${egroups_arr[@]}" ; do
530                 if [[ -z $(egetent group "${g}") ]] ; then
531                         eerror "You must add group ${g} to the system first"
532                         die "${g} is not a valid GID"
533                 fi
534         done
535
536         local defgroup=${egroups_arr[0]} exgroups_arr=()
537         # sort supplementary groups to make comparison possible
538         readarray -t exgroups_arr < <(printf '%s\n' "${egroups_arr[@]:1}" | sort)
539         local exgroups=${exgroups_arr[*]}
540         exgroups=${exgroups// /,}
541         egroups=${defgroup}${exgroups:+,${exgroups}}
542
543         # exit with no message if group membership is up to date
544         if [[ $(egetgroups "${euser}") == ${egroups} ]]; then
545                 return 0
546         fi
547
548         local opts=( -g "${defgroup}" -G "${exgroups}" )
549         elog "Updating groups for user '${euser}' ..."
550         elog " - Groups: ${egroups}"
551
552         # update the group
553         case ${CHOST} in
554         *-freebsd*|*-dragonfly*)
555                 pw usermod "${euser}" "${opts[@]}" && return 0
556                 [[ $? == 8 ]] && eerror "${euser} is in use, cannot update groups"
557                 eerror "There was an error when attempting to update the groups for ${euser}"
558                 eerror "Please update it manually on your system:"
559                 eerror "\t pw usermod \"${euser}\" ${opts[*]}"
560                 ;;
561
562         *)
563                 usermod "${opts[@]}" "${euser}" && return 0
564                 [[ $? == 8 ]] && eerror "${euser} is in use, cannot update groups"
565                 eerror "There was an error when attempting to update the groups for ${euser}"
566                 eerror "Please update it manually on your system (as root):"
567                 eerror "\t usermod ${opts[*]} \"${euser}\""
568                 ;;
569         esac
570 }
571
572 fi