# Copyright 2026 Nadeŭka # Distributed under the terms of the GNU General Public License v2 # @ECLASS: sysuser.eclass # @MAINTAINER: # Nadeŭka # @SUPPORTED_EAPIS: 8 9 # @BLURB: Eclass used to create and maintain a single user entry # @DESCRIPTION: # Creates, updates and (on removal) locks a single system user, installs a # matching sysusers.d entry, and optionally deploys skeleton files or SSH keys. # # Works with zero configuration: the user, its primary group, and the dependency # on that group are derived from ${PN}. # @EXAMPLE: # EAPI=8 # inherit sysuser if [[ ! -v _SYSUSER_ECLASS ]]; then _SYSUSER_ECLASS=1 case ${EAPI} in 8|9) ;; *) die "${ECLASS}: EAPI ${EAPI:-0} not supported" ;; esac inherit pkgvar sysidentity # @ECLASS_VARIABLE: SYSUSER_NAME # @DESCRIPTION: # The name of the user. Defaults to ${PN}. pkgvar_declare SYSUSER_NAME "${PN}" # @ECLASS_VARIABLE: SYSUSER_ID # @REQUIRED # @DESCRIPTION: # Preferred UID for the new user. Defaults to -1 (dynamic allocation). pkgvar_declare SYSUSER_ID -1 # @ECLASS_VARIABLE: SYSUSER_ID_FORCE # @DEFAULT_UNSET # @DESCRIPTION: # If set to 1, require the user to have the exact specified UID. pkgvar_declare SYSUSER_ID_FORCE '' # @ECLASS_VARIABLE: SYSUSER_PASSWD_HASH # @DEFAULT_UNSET # @DESCRIPTION: # Encrypted password hash for the user. Defaults to unset (locked account). pkgvar_declare SYSUSER_PASSWD_HASH '' # @ECLASS_VARIABLE: SYSUSER_LOCK # @DESCRIPTION: # If 1, an already existing user is updated to match the ebuild and locked on # removal. Any other value leaves existing users untouched. Defaults to 1. pkgvar_declare SYSUSER_LOCK 1 # @ECLASS_VARIABLE: SYSUSER_GROUP # @DESCRIPTION: # Primary group for the user. Defaults to ${SYSUSER_NAME}. pkgvar_declare SYSUSER_GROUP "${SYSUSER_NAME}" # @ECLASS_VARIABLE: SYSUSER_GROUPS # @DESCRIPTION: # Array of supplementary groups the user should belong to. Defaults to empty array. # The package-scoped override may be an array or a whitespace-separated string, # and _ADD appends further groups. pkgvar_declare -a SYSUSER_GROUPS # @ECLASS_VARIABLE: SYSUSER_GROUPS_FORCE # @DESCRIPTION: # If 1, strictly enforce supplementary group memberships on update, removing # unlisted groups. Defaults to the value of SYSUSER_LOCK. pkgvar_declare SYSUSER_GROUPS_FORCE "${SYSUSER_LOCK}" # @ECLASS_VARIABLE: SYSUSER_COMMENT # @DEFAULT_UNSET # @DESCRIPTION: # The comment (GECOS) to use for the user. Defaults to DESCRIPTION, evaluated # when the phases run, so a DESCRIPTION set after inherit is honoured. pkgvar_declare SYSUSER_COMMENT '' # @ECLASS_VARIABLE: SYSUSER_SHELL # @DESCRIPTION: # The login shell to use for the user. Defaults to /sbin/nologin. pkgvar_declare SYSUSER_SHELL /sbin/nologin # @ECLASS_VARIABLE: SYSUSER_HOME # @DESCRIPTION: # The home directory for the user. Defaults to /dev/null. pkgvar_declare SYSUSER_HOME /dev/null # @ECLASS_VARIABLE: SYSUSER_HOME_USER # @DESCRIPTION: # User owner for the home directory. Defaults to ${SYSUSER_NAME}. pkgvar_declare SYSUSER_HOME_USER "${SYSUSER_NAME}" # @ECLASS_VARIABLE: SYSUSER_HOME_GROUP # @DESCRIPTION: # Group owner for the home directory. Defaults to ${SYSUSER_GROUP}. pkgvar_declare SYSUSER_HOME_GROUP "${SYSUSER_GROUP}" # @ECLASS_VARIABLE: SYSUSER_HOME_PERMS # @DESCRIPTION: # Permissions for the home directory in chmod form. Defaults to 0755. pkgvar_declare SYSUSER_HOME_PERMS 0755 # @ECLASS_VARIABLE: SYSUSER_HOME_CREATE # @DESCRIPTION: # If 1, ensure home directory is created in the image. Defaults to 1 if HOME != /dev/null. [[ ${SYSUSER_HOME} == /dev/null ]] && _v=0 || _v=1 pkgvar_declare SYSUSER_HOME_CREATE "${_v}" unset _v # @ECLASS_VARIABLE: SYSUSER_HOME_SKEL # @DEFAULT_UNSET # @DESCRIPTION: # Optional path to a skeleton directory whose contents will be copied into the home directory. pkgvar_declare SYSUSER_HOME_SKEL '' # @ECLASS_VARIABLE: SYSUSER_SSH_TRUSTED_CLIENT_IDS # @DEFAULT_UNSET # @DESCRIPTION: # Array or string containing SSH public keys to install into ~/.ssh/authorized_keys. pkgvar_declare SYSUSER_SSH_TRUSTED_CLIENT_IDS '' # @ECLASS_VARIABLE: SYSUSER_AUTODEPS # @DESCRIPTION: # If 1, sysuser_adddeps is called automatically on inherit. Defaults to 1. pkgvar_declare SYSUSER_AUTODEPS 1 : "${DESCRIPTION:="System user: ${SYSUSER_NAME}"}" # @ECLASS_VARIABLE: SYSUSER_GROUP_CATEGORY # @DESCRIPTION: # Category of the group packages in RDEPEND. Defaults to ${CATEGORY%-user}-group # for categories ending in -user, res-group otherwise. # @ECLASS_VARIABLE: SYSUSER_USER_CATEGORY # @DESCRIPTION: # Category of the SYSUSER_HOME_USER package in RDEPEND. Defaults to ${CATEGORY}. # @FUNCTION: sysuser_adddeps # @DESCRIPTION: # Appends to RDEPEND the packages for SYSUSER_GROUP, SYSUSER_GROUPS, and home owners. sysuser_adddeps() { debug-print-function ${FUNCNAME} "$@" local gcat=res-group ucat=${SYSUSER_USER_CATEGORY:-${CATEGORY}} [[ ${CATEGORY} == *-user ]] && gcat=${CATEGORY%-user}-group gcat=${SYSUSER_GROUP_CATEGORY:-${gcat}} local -a groups=( "${SYSUSER_GROUP}" "${SYSUSER_GROUPS[@]}" ) [[ -n ${SYSUSER_HOME_GROUP} ]] && ! has "${SYSUSER_HOME_GROUP}" "${groups[@]}" \ && groups+=( "${SYSUSER_HOME_GROUP}" ) RDEPEND+=${groups[*]/#/ ${gcat}/} [[ -n ${SYSUSER_HOME_USER} && ${SYSUSER_HOME_USER} != "${SYSUSER_NAME}" ]] \ && RDEPEND+=" ${ucat}/${SYSUSER_HOME_USER}" } if sysidentity_istrue "${SYSUSER_AUTODEPS}"; then sysuser_adddeps fi # @FUNCTION: sysuser_comment # @INTERNAL # @DESCRIPTION: # Settles the default SYSUSER_COMMENT from DESCRIPTION. sysuser_comment() { : "${SYSUSER_COMMENT:=${DESCRIPTION//[:,=]/;}}" } # @FUNCTION: sysuser_opts # @INTERNAL # @DESCRIPTION: # Derives standard useradd/usermod options from current settings into _SYSUSER_OPTS. sysuser_opts() { sysuser_comment local IFS=, _SYSUSER_OPTS=( --comment "${SYSUSER_COMMENT}" --shell "${SYSUSER_SHELL}" --gid "${SYSUSER_GROUP}" --groups "${SYSUSER_GROUPS[*]}" ) } # @FUNCTION: sysuser_has_home # @INTERNAL # @DESCRIPTION: # Returns 0 if the home directory is to be created in the image. sysuser_has_home() { sysidentity_istrue "${SYSUSER_HOME_CREATE}" && [[ ${SYSUSER_HOME} != /dev/null ]] } # @FUNCTION: sysuser_sshkeys # @INTERNAL # @DESCRIPTION: # Collects the non-empty entries of SYSUSER_SSH_TRUSTED_CLIENT_IDS (array or # string) into _SYSUSER_SSH_KEYS. sysuser_sshkeys() { local k _SYSUSER_SSH_KEYS=() for k in "${SYSUSER_SSH_TRUSTED_CLIENT_IDS[@]}"; do [[ -z ${k} ]] || _SYSUSER_SSH_KEYS+=( "${k}" ) done } sysuser_pkg_pretend() { debug-print-function ${FUNCNAME} "$@" sysidentity_checkid passwd "${SYSUSER_NAME}" "${SYSUSER_ID}" "${SYSUSER_ID_FORCE}" } sysuser_src_install() { debug-print-function ${FUNCNAME} "$@" sysuser_comment [[ ${SYSUSER_COMMENT} != *[:,=]* ]] \ || die "Invalid characters in user comment: '${SYSUSER_COMMENT//[^:,=]}'" if sysuser_has_home; then keepdir "${SYSUSER_HOME}" if [[ -n ${SYSUSER_HOME_SKEL} ]]; then [[ -d ${SYSUSER_HOME_SKEL} ]] || die "SYSUSER_HOME_SKEL='${SYSUSER_HOME_SKEL}' is not a directory!" insinto "${SYSUSER_HOME}" doins -r "${SYSUSER_HOME_SKEL}"/* fi sysuser_sshkeys if (( ${#_SYSUSER_SSH_KEYS[@]} )); then insinto "${SYSUSER_HOME}/.ssh" newins - authorized_keys < <(printf '%s\n' "${_SYSUSER_SSH_KEYS[@]}") fi fi local g sysidentity_newsysusers - "${CATEGORY}-${SYSUSER_NAME}.conf" < <( printf 'u\t%q\t%q\t%q\t%q\t%q\n' \ "${SYSUSER_NAME}" \ "${SYSUSER_ID/#-*/-}:${SYSUSER_GROUP}" \ "${SYSUSER_COMMENT}" \ "${SYSUSER_HOME}" \ "${SYSUSER_SHELL/#-*/-}" for g in "${SYSUSER_GROUPS[@]}"; do printf 'm\t%q\t%q\n' "${SYSUSER_NAME}" "${g}" done ) } sysuser_pkg_preinst() { debug-print-function ${FUNCNAME} "$@" unset _SYSUSER_ADDED sysidentity_islock || return 0 sysuser_opts sysidentity_add passwd "${SYSUSER_NAME}" "${SYSUSER_ID}" "${SYSUSER_PASSWD_HASH}" \ --no-create-home --no-user-group --home-dir "${SYSUSER_HOME}" "${_SYSUSER_OPTS[@]}" \ && _SYSUSER_ADDED=1 sysuser_has_home || return 0 sysidentity_applyperms "${SYSUSER_HOME}" "${SYSUSER_HOME_USER}" "${SYSUSER_HOME_GROUP}" "${SYSUSER_HOME_PERMS}" sysuser_sshkeys local ssh=${ED}/${SYSUSER_HOME#/}/.ssh if (( ${#_SYSUSER_SSH_KEYS[@]} )) && [[ -d ${ssh} ]]; then fperms 0700 "${SYSUSER_HOME}/.ssh" [[ ! -f ${ssh}/authorized_keys ]] || fperms 0600 "${SYSUSER_HOME}/.ssh/authorized_keys" fi } sysuser_pkg_postinst() { debug-print-function ${FUNCNAME} "$@" [[ -n ${_SYSUSER_ADDED} ]] && return 0 sysidentity_islock || return 0 if ! sysidentity_istrue "${SYSUSER_LOCK}"; then ewarn "User ${SYSUSER_NAME} already exists; Not touching existing user" ewarn "due to SYSUSER_LOCK=${SYSUSER_LOCK}." return 0 fi sysuser_opts local -a opts=( --home "${SYSUSER_HOME}" "${_SYSUSER_OPTS[@]}" --expiredate "" ) sysidentity_istrue "${SYSUSER_GROUPS_FORCE}" || opts+=( --append ) [[ $(sysidentity_field shadow "${SYSUSER_NAME}" 2) == '!'?* ]] && opts+=( --unlock ) sysidentity_isaltroot && opts+=( --prefix "${ROOT}" ) local old_groups g del_groups='' old_groups=$(sysidentity_usergroups "${SYSUSER_NAME}") for g in ${old_groups//,/ }; do [[ ${g} == "${SYSUSER_GROUP}" ]] || has "${g}" "${SYSUSER_GROUPS[@]}" \ || del_groups+="${del_groups:+, }${g}" done if [[ -n ${del_groups} ]]; then if sysidentity_istrue "${SYSUSER_GROUPS_FORCE}"; then ewarn "Removing user ${SYSUSER_NAME} from group(s): ${del_groups}" ewarn "To retain the user's group membership in the local system" ewarn "config, override with ${PKGVAR_PREFIX}SYSUSER_GROUPS in make.conf." else elog "Retaining existing group membership: ${del_groups}" fi fi elog "Updating user ${SYSUSER_NAME}" sysidentity_exec usermod "${opts[@]}" "${SYSUSER_NAME}" local status=$? case ${status} in 0) ;; 8) eerror "Failed to update user ${SYSUSER_NAME}" eerror "This user currently has one or more running processes." eerror "Please update this user manually with the following command:" eerror " usermod ${opts[*]@Q} ${SYSUSER_NAME}" ;; *) die "usermod failed with status ${status}" ;; esac } sysuser_pkg_prerm() { debug-print-function ${FUNCNAME} "$@" sysidentity_lockuser "${SYSUSER_NAME}" "${SYSUSER_ID}" "${SYSUSER_LOCK}" } fi EXPORT_FUNCTIONS pkg_pretend src_install pkg_preinst pkg_postinst pkg_prerm