user.eclass: Introduce eget{user,group}name
[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: egetusername
355 # @USAGE: <uid>
356 # @DESCRIPTION:
357 # Gets the username for given UID.
358 egetusername() {
359         [[ $# -eq 1 ]] || die "usage: egetusername <uid>"
360
361         id -u -n "$1"
362 }
363
364 # @FUNCTION: egetgroupname
365 # @USAGE: <gid>
366 # @DESCRIPTION:
367 # Gets the group name for given GID.
368 egetgroupname() {
369         [[ $# -eq 1 ]] || die "usage: egetgroupname <gid>"
370
371         id -g -n "$1"
372 }
373
374 # @FUNCTION: egethome
375 # @USAGE: <user>
376 # @DESCRIPTION:
377 # Gets the home directory for the specified user.
378 egethome() {
379         local pos
380
381         [[ $# -eq 1 ]] || die "usage: egethome <user>"
382
383         case ${CHOST} in
384         *-freebsd*|*-dragonfly*)
385                 pos=9
386                 ;;
387         *)      # Linux, NetBSD, OpenBSD, etc...
388                 pos=6
389                 ;;
390         esac
391
392         egetent passwd "$1" | cut -d: -f${pos}
393 }
394
395 # @FUNCTION: egetshell
396 # @USAGE: <user>
397 # @DESCRIPTION:
398 # Gets the shell for the specified user.
399 egetshell() {
400         local pos
401
402         [[ $# -eq 1 ]] || die "usage: egetshell <user>"
403
404         case ${CHOST} in
405         *-freebsd*|*-dragonfly*)
406                 pos=10
407                 ;;
408         *)      # Linux, NetBSD, OpenBSD, etc...
409                 pos=7
410                 ;;
411         esac
412
413         egetent passwd "$1" | cut -d: -f${pos}
414 }
415
416 # @FUNCTION: esethome
417 # @USAGE: <user> <homedir>
418 # @DESCRIPTION:
419 # Update the home directory in a platform-agnostic way.
420 # Required parameters is the username and the new home directory.
421 # Specify -1 if you want to set home to the enewuser default
422 # of /dev/null.
423 # If the new home directory does not exist, it is created.
424 # Any previously existing home directory is NOT moved.
425 esethome() {
426         _assert_pkg_ebuild_phase ${FUNCNAME}
427
428         # get the username
429         local euser=$1; shift
430         if [[ -z ${euser} ]] ; then
431                 eerror "No username specified !"
432                 die "Cannot call esethome without a username"
433         fi
434
435         # lets see if the username already exists
436         if [[ -z $(egetent passwd "${euser}") ]] ; then
437                 ewarn "User does not exist, cannot set home dir -- skipping."
438                 return 1
439         fi
440
441         # handle homedir
442         local ehome=$1; shift
443         if [[ -z ${ehome} ]] ; then
444                 eerror "No home directory specified !"
445                 die "Cannot call esethome without a home directory or '-1'"
446         fi
447
448         if [[ ${ehome} == "-1" ]] ; then
449                 ehome="/dev/null"
450         fi
451
452         # exit with no message if home dir is up to date
453         if [[ $(egethome "${euser}") == ${ehome} ]]; then
454                 return 0
455         fi
456
457         einfo "Updating home for user '${euser}' ..."
458         einfo " - Home: ${ehome}"
459
460         # ensure home directory exists, otherwise update will fail
461         if [[ ! -e ${ROOT}/${ehome} ]] ; then
462                 einfo " - Creating ${ehome} in ${ROOT}"
463                 mkdir -p "${ROOT}/${ehome}"
464                 chown "${euser}" "${ROOT}/${ehome}"
465                 chmod 755 "${ROOT}/${ehome}"
466         fi
467
468         # update the home directory
469         case ${CHOST} in
470         *-freebsd*|*-dragonfly*)
471                 pw usermod "${euser}" -d "${ehome}" && return 0
472                 [[ $? == 8 ]] && eerror "${euser} is in use, cannot update home"
473                 eerror "There was an error when attempting to update the home directory for ${euser}"
474                 eerror "Please update it manually on your system:"
475                 eerror "\t pw usermod \"${euser}\" -d \"${ehome}\""
476                 ;;
477
478         *)
479                 usermod -d "${ehome}" "${euser}" && return 0
480                 [[ $? == 8 ]] && eerror "${euser} is in use, cannot update home"
481                 eerror "There was an error when attempting to update the home directory for ${euser}"
482                 eerror "Please update it manually on your system (as root):"
483                 eerror "\t usermod -d \"${ehome}\" \"${euser}\""
484                 ;;
485         esac
486 }
487
488 # @FUNCTION: esetshell
489 # @USAGE: <user> <shell>
490 # @DESCRIPTION:
491 # Update the shell in a platform-agnostic way.
492 # Required parameters is the username and the new shell.
493 # Specify -1 if you want to set shell to platform-specific nologin.
494 esetshell() {
495         _assert_pkg_ebuild_phase ${FUNCNAME}
496
497         # get the username
498         local euser=$1; shift
499         if [[ -z ${euser} ]] ; then
500                 eerror "No username specified !"
501                 die "Cannot call esetshell without a username"
502         fi
503
504         # lets see if the username already exists
505         if [[ -z $(egetent passwd "${euser}") ]] ; then
506                 ewarn "User does not exist, cannot set shell -- skipping."
507                 return 1
508         fi
509
510         # handle shell
511         local eshell=$1; shift
512         if [[ -z ${eshell} ]] ; then
513                 eerror "No shell specified !"
514                 die "Cannot call esetshell without a shell or '-1'"
515         fi
516
517         if [[ ${eshell} == "-1" ]] ; then
518                 eshell=$(user_get_nologin)
519         fi
520
521         # exit with no message if shell is up to date
522         if [[ $(egetshell "${euser}") == ${eshell} ]]; then
523                 return 0
524         fi
525
526         einfo "Updating shell for user '${euser}' ..."
527         einfo " - Shell: ${eshell}"
528
529         # update the shell
530         case ${CHOST} in
531         *-freebsd*|*-dragonfly*)
532                 pw usermod "${euser}" -s "${eshell}" && return 0
533                 [[ $? == 8 ]] && eerror "${euser} is in use, cannot update shell"
534                 eerror "There was an error when attempting to update the shell for ${euser}"
535                 eerror "Please update it manually on your system:"
536                 eerror "\t pw usermod \"${euser}\" -s \"${eshell}\""
537                 ;;
538
539         *)
540                 usermod -s "${eshell}" "${euser}" && return 0
541                 [[ $? == 8 ]] && eerror "${euser} is in use, cannot update shell"
542                 eerror "There was an error when attempting to update the shell for ${euser}"
543                 eerror "Please update it manually on your system (as root):"
544                 eerror "\t usermod -s \"${eshell}\" \"${euser}\""
545                 ;;
546         esac
547 }
548
549 fi