| # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| # @ECLASS: user.eclass |
| # @MAINTAINER: |
| # The Chromium OS Authors. <chromium-os-dev@chromium.org> |
| # @BLURB: user management in ebuilds |
| # @DESCRIPTION: |
| # Replaces the upstream mechanism of managing users and groups with one that |
| # manages the database in ${ROOT}, changing the sysroot database |
| # only when the caller creates the user/group during setup. |
| |
| # Before we manipulate users at all, we want to make sure that |
| # passwd/group/shadow is initialized in the first place. That's |
| # what baselayout does. |
| # |
| # We should consider providing a virtual to abstract away this dependency. |
| # This would allow CrOS builds to completely specify all users and groups, |
| # instead of accepting the assumption (expressed in baselayout, currently) |
| # that every build wants groups like wheel, tty and so forth. |
| if [ "${PN}" != "baselayout" ]; then |
| DEPEND="sys-apps/baselayout" |
| RDEPEND="sys-apps/baselayout" |
| fi |
| |
| # @FUNCTION: _is_cros_device |
| # @INTERNAL |
| # @USAGE: |
| # @DESCRIPTION: |
| # Used to figure out if we're running on a Chromium OS device. |
| _is_cros_device() { |
| grep -qs ^CHROMEOS_RELEASE_ /etc/lsb-release |
| } |
| |
| # @FUNCTION: _assert_pkg_ebuild_phase |
| # @INTERNAL |
| # @USAGE: <calling func name> |
| _assert_pkg_ebuild_phase() { |
| case ${EBUILD_PHASE} in |
| setup|preinst|postinst) ;; |
| *) |
| eerror "'$1()' called from '${EBUILD_PHASE}' phase which is not OK:" |
| eerror "You may only call from pkg_{setup,preinst,postinst} functions." |
| eerror "Package fails at QA and at life. Please file a bug." |
| die "Bad package! $1 is only for use in some pkg_* functions!" |
| esac |
| } |
| |
| # Array of paths to existing accounts DBs in overlays that apply for the |
| # current board. In order of decreasing overlay priority. |
| ACCOUNTS_DIRS=() |
| |
| # @FUNCTION: _find_accounts_dirs |
| # @INTERNAL |
| # @USAGE: |
| # @DESCRIPTION: |
| # Looks for accounts DB under all valid overlays for the current board and |
| # populates ACCOUNTS_DIRS with fully-qualified paths to all the ones it finds. |
| # Values are cached, so multiple calls will return quickly without updating |
| # global ACCOUNTS_DIRS array. |
| _find_accounts_dirs() { |
| [[ ${#ACCOUNTS_DIRS[@]} -gt 0 ]] && return |
| |
| # Load the cache from disk. We don't want to use the env because that |
| # will be saved at build time and used when merging binpkgs. Instead, |
| # we need it to be generated at binpkg time too. |
| local cache="${T}/_accounts_dir_cache.list" |
| if [[ -e ${cache} ]]; then |
| local dir |
| while read -d $'\0' -r dir; do |
| ACCOUNTS_DIRS+=("${dir}") |
| done <"${cache}" |
| return |
| fi |
| |
| local overlay |
| for overlay in $(_call_portageq get_repos "${ROOT:-/}") ; do |
| local overlay_dir=$(_call_portageq get_repo_path "${ROOT:-/}" "${overlay}") |
| local accounts_dir="${overlay_dir}/profiles/base/accounts" |
| if [[ -d "${accounts_dir}" ]] ; then |
| einfo "Adding ${accounts_dir} to user/group search path." |
| ACCOUNTS_DIRS+=("${accounts_dir}") |
| fi |
| done |
| |
| printf '%s\0' "${ACCOUNTS_DIRS[@]}" >"${cache}" |
| } |
| |
| # @FUNCTION: _call_portageq |
| # @INTERNAL |
| # @USAGE: <portageq command> [<arg> ...] |
| _call_portageq() { |
| echo $(env -i PATH="${PATH}" PORTAGE_USERNAME="${PORTAGE_USERNAME}" PORTAGE_CONFIGROOT="${PORTAGE_CONFIGROOT}" portageq "$@") |
| } |
| |
| # @FUNCTION: _find_acct_template |
| # @INTERNAL |
| # @USAGE: <db> <key> |
| # @DESCRIPTION: |
| # Searches existing account dbs in overlay-inheritance order for <key> in <db>. |
| _find_acct_template() { |
| local db=$1 key=$2 |
| [[ $# -ne 2 ]] && die "usage: ${FUNCNAME} <db> <key>" |
| local accounts_dir |
| for accounts_dir in "${ACCOUNTS_DIRS[@]}" ; do |
| local template="${accounts_dir}/${db}/${key}" |
| [[ -e "${template}" ]] && echo "${template}" && break |
| done |
| } |
| |
| # @FUNCTION: _read_db_entry |
| # @INTERNAL |
| # @USAGE: <template> <key> |
| # @DESCRIPTION: |
| # Read the value from the template database. |
| _read_db_entry() { |
| local template=$1 key=$2 |
| awk -F':' -v key="${key}" '$1 == key { print $2 }' "${template}" |
| } |
| |
| # @FUNCTION: _get_value_for_user |
| # @INTERNAL |
| # @USAGE: <user> <key> |
| # @DESCRIPTION: |
| # Gets value from appropriate account definition file. |
| _get_value_for_user() { |
| local user=$1 key=$2 |
| [[ $# -ne 2 ]] && die "usage: ${FUNCNAME} <user> <key>" |
| [[ ${#ACCOUNTS_DIRS[@]} -eq 0 ]] && die "Must populate ACCOUNTS_DIRS!" |
| |
| case ${key} in |
| user|password|uid|gid|gecos|home|shell|defunct) ;; |
| *) die "sorry, '${key}' is not a field in the passwd db." ;; |
| esac |
| |
| local template=$(_find_acct_template user "${user}") |
| [[ -z "${template}" ]] && die "No entry for ${user} in any overlay." |
| _read_db_entry "${template}" "${key}" |
| } |
| |
| # @FUNCTION: _get_value_for_group |
| # @INTERNAL |
| # @USAGE: <group> <key> |
| # @DESCRIPTION: |
| # Gets value from appropriate account definition file. |
| _get_value_for_group() { |
| local group=$1 key=$2 |
| [[ $# -ne 2 ]] && die "usage: ${FUNCNAME} <group> <key>" |
| [[ ${#ACCOUNTS_DIRS[@]} -eq 0 ]] && die "Must populate ACCOUNTS_DIRS!" |
| |
| case ${key} in |
| group|password|gid|users|defunct) ;; |
| *) die "sorry, '${key}' is not a field in the group db." ;; |
| esac |
| |
| local template=$(_find_acct_template group "${group}") |
| [[ -z "${template}" ]] && die "No entry for ${group} in any overlay." |
| _read_db_entry "${template}" "${key}" |
| } |
| |
| # @FUNCTION: _assert_fields_in_sync |
| # @INTERNAL |
| # @USAGE: <user|group> <account> <keys> |
| # @DESCRIPTION: |
| # Walks all the overlays and makes sure that the keys have the same values in |
| # all of them. This is useful for making sure uids/gids don't change in case |
| # the account name has a collision. |
| _assert_fields_in_sync() { |
| local db=$1 acct=$2 keys=( "${@:3}" ) |
| local key dir old_dir |
| for key in "${keys[@]}"; do |
| local value old_value="" |
| for dir in "${ACCOUNTS_DIRS[@]}"; do |
| local template="${dir}/${db}/${acct}" |
| if [[ -e "${template}" ]]; then |
| value=$(_read_db_entry "${template}" "${key}") |
| if [[ "${old_value:=${value}}" != "${value}" ]]; then |
| eerror "${db} account '${acct}' has conflicting ${key} values." |
| eerror "${template}: ${key} = ${value}" |
| eerror "${old_dir}/${db}/${acct}: ${key} = ${old_value}" |
| die "${key} must be kept in sync across overlays" |
| fi |
| old_dir="${dir}" |
| fi |
| done |
| done |
| } |
| |
| # @FUNCTION: _portable_grab_lock |
| # @INTERNAL |
| # @USAGE: <lockfile> |
| # @DESCRIPTION: |
| # Grabs a lock on <lockfile> in a race-free, portable manner. |
| # We need to use this mechanism in order to be compatible with the shadow utils |
| # (groupadd, useradd, etc). |
| _portable_grab_lock() { |
| local lockfile=$1 |
| local lockfile_1="${lockfile}.${BASHPID}" |
| local timeout=$(( 60 * 5 )) # 5 minute timeout |
| |
| touch "${lockfile_1}" |
| until ln "${lockfile_1}" "${lockfile}" &> /dev/null; do |
| sleep 1 |
| [[ $(( timeout-- )) -le 0 ]] && die "Timeout while trying to lock ${lockfile}" |
| [[ $(( timeout % 10 )) -eq 0 ]] && einfo "Waiting for lock on ${dbfile}" |
| done |
| rm "${lockfile_1}" || die "Failed to lock ${lockfile}." |
| } |
| |
| # @FUNCTION: _write_entry_to_db() |
| # @INTERNAL |
| # @USAGE: <entry> <database> <root> |
| # @DESCRIPTION: |
| # Writes an entry to the specified database under the specified root. |
| _write_entry_to_db() { |
| local entry=$1 db=$2 root=$3 |
| |
| [[ $# -ne 3 ]] && die "usage: _write_entry_to_db <entry> <database> <root>" |
| |
| case ${db} in |
| passwd|group) ;; |
| *) die "sorry, database '${db}' not supported." ;; |
| esac |
| |
| local dbfile=$(readlink -e "${root}/etc/${db}") |
| [[ ! -e "${dbfile}" ]] && die "${db} under ${root} does not exist." |
| if [[ ! -w "${dbfile}" ]] ; then |
| ewarn "Unable to modify ${db} under ${root} due to read-only mount." |
| return 1 |
| fi |
| # Use the same lock file as the shadow utils. |
| local lockfile="${dbfile}.lock" |
| |
| _portable_grab_lock "${lockfile}" |
| |
| # Need to check if the acct exists while we hold the lock, in case |
| # another ebuild added it in the meantime. |
| local key=$(awk -F':' '{ print $1 }' <<<"${entry}") |
| local existing_entry=$(egetent --nolock "${db}" "${key}" "${root}") |
| if [[ -z ${existing_entry} ]] ; then |
| echo "${entry}" >> "${dbfile}" || die "Could not write ${entry} to ${dbfile}." |
| else |
| einfo "'${entry}' superceded by '${existing_entry}'" |
| fi |
| |
| rm "${lockfile}" || die "Failed to release lock on ${lockfile}." |
| return 0 |
| } |
| |
| # @FUNCTION: egetent |
| # @USAGE: [--nolock] <database> <key> [root] |
| # @DESCRIPTION: |
| # Provides getent-like functionality for databases under [root]. Defaults to ${ROOT}. |
| # |
| # Supported databases: group passwd |
| egetent() { |
| local use_lock=true |
| [[ $1 == "--nolock" ]] && use_lock=false && shift |
| [[ $# -ne 2 && $# -ne 3 ]] && die "usage: egetent <database> <key> [root]" |
| |
| local db=$1 key=$2 root=${3:-"${ROOT}"} |
| |
| case ${db} in |
| passwd|group) ;; |
| *) die "sorry, database '${db}' not yet supported; file a bug" ;; |
| esac |
| |
| local dbfile=$(readlink -e "${root}/etc/${db}") |
| [[ ! -e "${dbfile}" ]] && die "${db} under ${root} does not exist." |
| [[ ! -w "${dbfile}" ]] && use_lock=false # File can't change anyway! |
| |
| local lockfile="${dbfile}.lock" |
| ${use_lock} && _portable_grab_lock "${lockfile}" |
| |
| awk -F':' -v key="${key}" \ |
| '($1 == key || $3 == key) { print }' \ |
| "${dbfile}" 2>/dev/null |
| |
| if ${use_lock} ; then |
| rm "${lockfile}" || die "Failed to release lock on ${lockfile}." |
| fi |
| } |
| |
| # @FUNCTION: enewuser |
| # @USAGE: <user> [uid] [shell] [homedir] [groups] |
| # @DESCRIPTION: |
| # Same as enewgroup, you are not required to understand how to properly add |
| # a user to the system. The only required parameter is the username. |
| # Default uid is (pass -1 for this) next available, default shell is |
| # /bin/false, default homedir is /dev/null, and there are no default groups. |
| enewuser() { |
| _assert_pkg_ebuild_phase ${FUNCNAME} |
| |
| # get the username |
| local euser=$1; shift |
| if [[ -z ${euser} ]] ; then |
| eerror "No username specified !" |
| die "Cannot call enewuser without a username" |
| fi |
| |
| # Lets see if the username already exists in ${ROOT} or in the system. |
| local is_in_root=false |
| [[ -n "$(egetent passwd ${euser})" ]] && is_in_root=true |
| local is_in_system=false |
| [[ -n "$(egetent passwd ${euser} /)" ]] && is_in_system=true |
| local should_be_in_system=false |
| [[ "${EBUILD_PHASE}" == "setup" ]] && should_be_in_system=true |
| |
| if "${is_in_root}" && (! "${should_be_in_system}" || "${is_in_system}") ; then |
| return 0 |
| fi |
| |
| # We can't support creating accounts on the system yet. |
| # https://crbug.com/402673 |
| if _is_cros_device; then |
| ewarn "Skipping user '${euser}' creation due to https://crbug.com/402673" |
| return 0 |
| fi |
| |
| # Locate all applicable accounts profiles. |
| local ACCOUNTS_DIRS |
| _find_accounts_dirs |
| if [[ ${#ACCOUNTS_DIRS[@]} -eq 0 ]] ; then |
| ewarn "No user/group data files present. Skipping." |
| return 0 |
| fi |
| |
| # Ensure username exists in profile. |
| if [[ -z $(_get_value_for_user "${euser}" user) ]] ; then |
| die "'${euser}' does not exist in profile!" |
| elif [[ -n $(_get_value_for_user "${euser}" defunct) ]] ; then |
| die "'${euser}' was used previously and is now disallowed." |
| fi |
| _assert_fields_in_sync user "${euser}" user uid gid |
| einfo "Adding user '${euser}' to your system ..." |
| |
| # Handle uid. Passing no UID is functionally equivalent to passing -1. |
| local provided_uid=$(_get_value_for_user "${euser}" uid) |
| local euid=$1; shift |
| if [[ "${PORTAGE_REPO_NAME}" == "portage-stable" ]] ; then |
| # If caller is from portage-stable, ignore specified UID. |
| if [[ ${euid:--1} != "-1" ]] ; then |
| einfo "Ignoring requested UID ${euid} in portage-stable ebuilds." |
| fi |
| euid='' |
| fi |
| if [[ -z ${euid} ]] ; then |
| euid=-1 |
| elif [[ ${euid} -lt -1 ]] ; then |
| eerror "Userid given but is not greater than 0 !" |
| die "${euid} is not a valid UID." |
| fi |
| # Now, ${euid} is set and >= -1. |
| if [[ -n ${provided_uid} ]] ; then |
| # If profile has UID and caller specified '' or -1, use profile. |
| # If profile has UID and caller specified different, barf. |
| # If profile has UID and caller specified same, OK. |
| if [[ ${euid} == -1 ]] ; then |
| euid=${provided_uid} |
| elif [[ ${euid} != ${provided_uid} ]] ; then |
| eerror "Userid differs from the profile!" |
| die "${euid} != ${provided_uid} from profile." |
| # else...they're already equal, so do nothing. |
| fi |
| else |
| # If profile has no UID and caller did not specify, barf. |
| if [[ ${euid} == -1 ]] ; then |
| die "No UID specified in profile!" |
| fi |
| # If profile has no entry w/UID and caller specified one, OK. |
| fi |
| |
| einfo " - Userid: ${euid}" |
| |
| # See if there's a provided gid and use it if so. |
| local provided_gid=$(_get_value_for_user "${euser}" gid) |
| local egid=${provided_gid:-${euid}} |
| einfo " - Groupid: ${egid}" |
| |
| # handle shell |
| local eshell=$1; shift |
| if [[ -n ${eshell} && ${eshell} != "-1" ]] ; then |
| # We might need to relax this for portage-stable if there |
| # are any packages that we want to allow to set a custom shell. |
| eerror "Do not specify ${eshell} yourself, use -1" |
| die "Pass '-1' as the shell parameter" |
| else |
| eshell=$(_get_value_for_user "${euser}" shell) |
| ${eshell:=/bin/false} |
| fi |
| if [[ ${eshell} != */false && ${eshell} != */nologin ]] ; then |
| if [[ ! -e ${ROOT}${eshell} ]] ; then |
| eerror "A shell was specified but it does not exist !" |
| die "${eshell} does not exist in ${ROOT}" |
| fi |
| fi |
| einfo " - Shell: ${eshell}" |
| |
| # handle homedir |
| local ehome=$1; shift |
| if [[ ${ehome:--1} != "-1" ]] ; then |
| if [[ "${PORTAGE_REPO_NAME}" != "portage-stable" ]] ; then |
| die "Pass -1 as the home directory" |
| else |
| # If caller is from portage-stable, ignore specified homedir. |
| einfo "Ignoring requested homedir ${ehome} in portage-stable ebuilds." |
| ehome='' |
| fi |
| fi |
| if [[ -z ${ehome} || ${ehome} == "-1" ]] ; then |
| ehome=$(_get_value_for_user "${euser}" home) |
| fi |
| einfo " - Home: ${ehome}" |
| |
| # Grab groups for later handling. |
| local egroups=$1; shift |
| |
| # Check groups. |
| local g egroups_arr |
| IFS="," read -r -a egroups_arr <<<"${egroups}" |
| shift |
| for g in "${egroups_arr[@]}" ; do |
| enewgroup "${g}" |
| done |
| einfo " - Groups: ${egroups:-(none)}" |
| |
| local comment |
| if [[ $# -gt 0 ]] ; then |
| die "extra arguments no longer supported; please file a bug." |
| else |
| comment=$(_get_value_for_user "${euser}" gecos) |
| einfo " - GECOS: ${comment}" |
| fi |
| |
| local epassword=$(_get_value_for_user "${euser}" password) |
| : ${epassword:="!"} |
| local entry="${euser}:${epassword}:${euid}:${egid}:${comment}:${ehome}:${eshell}" |
| if ! "${is_in_system}" && "${should_be_in_system}" ; then |
| _write_entry_to_db "${entry}" passwd / || die "Must be able to add users during setup." |
| fi |
| if ! "${is_in_root}" ; then |
| if _write_entry_to_db "${entry}" passwd "${ROOT}" ; then |
| if [[ ! -e ${ROOT}/${ehome} ]] ; then |
| einfo " - Creating ${ehome} in ${ROOT}" |
| mkdir -p "${ROOT}/${ehome}" |
| chown "${euser}" "${ROOT}/${ehome}" |
| chmod 755 "${ROOT}/${ehome}" |
| fi |
| fi |
| fi |
| } |
| |
| # @FUNCTION: enewgroup |
| # @USAGE: <group> [gid] |
| # @DESCRIPTION: |
| # This function does not require you to understand how to properly add a |
| # group to the system. Just give it a group name to add and enewgroup will |
| # do the rest. You may specify the gid for the group or allow the group to |
| # allocate the next available one. |
| enewgroup() { |
| _assert_pkg_ebuild_phase ${FUNCNAME} |
| |
| # Get the group. |
| local egroup=$1; shift |
| if [[ -z ${egroup} ]] ; then |
| eerror "No group specified !" |
| die "Cannot call enewgroup without a group" |
| fi |
| |
| # Lets see if the group already exists in ${ROOT} or in the system. |
| local is_in_root=false |
| [[ -n "$(egetent group ${egroup})" ]] && is_in_root=true |
| local is_in_system=false |
| [[ -n "$(egetent group ${egroup} /)" ]] && is_in_system=true |
| local should_be_in_system=false |
| [[ "${EBUILD_PHASE}" == "setup" ]] && should_be_in_system=true |
| |
| if "${is_in_root}" && (! "${should_be_in_system}" || "${is_in_system}") ; then |
| return 0 |
| fi |
| |
| # We can't support creating accounts on the system yet. |
| # https://crbug.com/402673 |
| if _is_cros_device; then |
| ewarn "Skipping group '${egroup}' creation due to https://crbug.com/402673" |
| return 0 |
| fi |
| |
| # Locate all applicable accounts profiles. |
| local ACCOUNTS_DIRS |
| _find_accounts_dirs |
| if [[ ${#ACCOUNTS_DIRS[@]} -eq 0 ]] ; then |
| ewarn "No user/group data files present. Skipping." |
| return 0 |
| fi |
| # Ensure group exists in profile. |
| if [[ -z $(_get_value_for_group "${egroup}" group) ]] ; then |
| die "Config for ${egroup} not present in profile!" |
| elif [[ -n $(_get_value_for_group "${egroup}" defunct) ]] ; then |
| die "'${egroup}' was used previously and is now disallowed." |
| fi |
| _assert_fields_in_sync group "${egroup}" group gid |
| einfo "Adding group '${egroup}' to your system ..." |
| |
| # handle gid |
| local provided_gid=$(_get_value_for_group "${egroup}" gid) |
| local egid=$1; shift |
| if [[ "${PORTAGE_REPO_NAME}" == "portage-stable" ]] ; then |
| # If caller is from portage-stable, ignore specified GID. |
| if [[ ${egid:--1} != "-1" ]] ; then |
| einfo "Ignoring requested GID ${egid} in portage-stable ebuilds." |
| fi |
| egid='' |
| fi |
| if [[ -z ${egid} ]] ; then |
| # If caller specified nothing and profile has GID, use profile. |
| # If caller specified nothing and profile has no GID, barf. |
| if [[ ! -z ${provided_gid} ]] ; then |
| egid=${provided_gid} |
| else |
| die "No gid provided in PROFILE or in args!" |
| fi |
| else |
| if [[ ${egid} -lt 0 ]] ; then |
| eerror "Groupid given but is not greater than 0 !" |
| die "${egid} is not a valid GID" |
| fi |
| |
| # If caller specified GID and profile has no GID, OK. |
| # If caller specified GID and profile has entry with same, OK. |
| if [[ -z ${provided_gid} || ${egid} -eq ${provided_gid} ]] ; then |
| provided_gid=${egid} |
| fi |
| |
| # If caller specified GID but profile has different, barf. |
| if [[ ${egid} -ne ${provided_gid} ]] ; then |
| eerror "${egid} conflicts with provided ${provided_gid}!" |
| die "${egid} conflicts with provided ${provided_gid}!" |
| fi |
| fi |
| einfo " - Groupid: ${egid}" |
| |
| # Handle extra. |
| if [[ $# -gt 0 ]] ; then |
| die "extra arguments no longer supported; please file a bug" |
| fi |
| |
| # Allow group passwords, if profile asks for it. |
| local epassword=$(_get_value_for_group "${egroup}" password) |
| : ${epassword:="!"} |
| einfo " - Password entry: ${epassword}" |
| |
| # Pre-populate group with users. |
| local eusers=$(_get_value_for_group "${egroup}" users) |
| einfo " - User list: ${eusers}" |
| |
| # Add the group. |
| local entry="${egroup}:${epassword}:${egid}:${eusers}" |
| if ! "${is_in_system}" && "${should_be_in_system}" ; then |
| _write_entry_to_db "${entry}" group / || die "Must be able to add groups during setup." |
| fi |
| if ! "${is_in_root}" ; then |
| _write_entry_to_db "${entry}" group "${ROOT}" |
| fi |
| einfo "Done with group: '${egroup}'." |
| } |
| |
| # @FUNCTION: egethome |
| # @USAGE: <user> |
| # @DESCRIPTION: |
| # Gets the home directory for the specified user. |
| egethome() { |
| [[ $# -eq 1 ]] || die "usage: egethome <user>" |
| egetent passwd "$1" | cut -d: -f6 |
| } |
| |
| # @FUNCTION: egetshell |
| # @USAGE: <user> |
| # @DESCRIPTION: |
| # Gets the shell for the specified user. |
| egetshell() { |
| [[ $# -eq 1 ]] || die "usage: egetshell <user>" |
| egetent passwd "$1" | cut -d: -f7 |
| } |