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