#!/bin/bash # duser - manage user accounts on Das OS: create them with the right groups, # set passwords, lock them, add SSH keys. set -euo pipefail # shellcheck source=helpers.sh . "${DAS_HELPERS:-/usr/lib/das/helpers.sh}" # Groups of every new user, as the desktop installer gives them; only the ones # that exist on this system are used user_groups="users audio video input storage network optical scanner lp kvm plugdev bluetooth" # Administrators also get sudo, Docker (root-equivalent) and the system logs admin_groups="wheel docker socklog" uid_min=$(awk '$1 == "UID_MIN" { print $2 }' /etc/login.defs 2>/dev/null) uid_max=$(awk '$1 == "UID_MAX" { print $2 }' /etc/login.defs 2>/dev/null) uid_min=${uid_min:-1000} uid_max=${uid_max:-60000} usage() { if [[ -n $uk ]]; then cat <<'EOF' Використання: duser КОМАНДА [КОРИСТУВАЧ] [...] list користувачі: адміністратор, вхід, SSH-ключі, групи add [КОРИСТУВАЧ] створити; чого не вказано, те запитає: --admin адміністратор (sudo, docker, журнали) --key КЛЮЧ|ФАЙЛ публічний SSH-ключ --no-password без пароля, вхід лише за ключем --password-stdin пароль зі стандартного входу (для скриптів) del КОРИСТУВАЧ [--home] видалити (--home: разом із домашнім каталогом) passwd [КОРИСТУВАЧ] змінити пароль (без імені — свій) lock КОРИСТУВАЧ заборонити вхід (і за паролем, і за ключем) unlock КОРИСТУВАЧ дозволити вхід знову key КОРИСТУВАЧ [list] SSH-ключі користувача key КОРИСТУВАЧ add [КЛЮЧ|ФАЙЛ|-] додати ключ (рядок, файл .pub або stdin) key КОРИСТУВАЧ rm НОМЕР видалити ключ за номером зі списку groups КОРИСТУВАЧ [+ГРУПА|-ГРУПА...] показати групи, додати (+) чи прибрати (-); +wheel робить адміністратором Зміни груп діють із наступного входу користувача. EOF else cat <<'EOF' Usage: duser COMMAND [USER] [...] list users: administrator, login, SSH keys, groups add [USER] create one; asks for what is not given: --admin an administrator (sudo, docker, system logs) --key KEY|FILE a public SSH key --no-password no password, key login only --password-stdin read the password from stdin (for scripts) del USER [--home] delete (--home: with the home directory) passwd [USER] change the password (without a name: your own) lock USER forbid logins (by password and by key) unlock USER allow logins again key USER [list] the user's SSH keys key USER add [KEY|FILE|-] add a key (a line, a .pub file or stdin) key USER rm NUMBER remove a key by its number in the list groups USER [+GROUP|-GROUP...] show the groups, add (+) or remove (-) some; +wheel makes an administrator Group changes apply from the user's next login. EOF fi } # A regular (human) account, not a system one check_user() { local uid uid=$(id -u "$1" 2>/dev/null) || die "немає користувача $1" "no user $1" ((uid >= uid_min && uid <= uid_max)) || die "$1 — системний обліковий запис, duser його не змінює" "$1 is a system account, duser leaves it alone" } in_group() { id -nG "$1" | tr ' ' '\n' | grep -qx "$2" } # existing GROUPS...: the given groups that exist, comma-separated existing() { local g out=() for g in "$@"; do getent group "$g" >/dev/null && out+=("$g") done local IFS=, echo "${out[*]}" } # login_state USER: how the user can log in, from /etc/shadow login_state() { local hash expire today IFS=: read -r _ hash _ _ _ _ _ expire _ < <(getent shadow "$1") today=$(($(date +%s) / 86400)) if [[ -n $expire ]] && ((expire <= today)); then say заблоковано locked elif [[ $hash == \$* ]]; then say пароль password else # "!", "*" or a "!"-locked hash: SSH keys still work say "лише ключ" "key only" fi } keys_file() { echo "$(getent passwd "$1" | cut -d: -f6)/.ssh/authorized_keys" } # The keys of a file, one per line, without comments and blank lines key_lines() { [[ -r $1 ]] || return 0 grep -v '^[[:space:]]*\(#\|$\)' "$1" || true } cmd_list() { local name uid home admin keys pad "$(say КОРИСТУВАЧ USER)" 16 pad UID 6 pad "$(say АДМІН ADMIN)" 6 pad "$(say ВХІД LOGIN)" 12 pad "$(say КЛЮЧІ KEYS)" 6 say ГРУПИ GROUPS while IFS=: read -r name _ uid _ _ home _; do ((uid >= uid_min && uid <= uid_max)) || continue admin=$(say ні no) in_group "$name" wheel && admin=$(say так yes) keys=$(key_lines "$home/.ssh/authorized_keys" | wc -l) pad "$name" 16 pad "$uid" 6 pad "$admin" 6 pad "$(login_state "$name")" 12 pad "$keys" 6 id -nG "$name" | sed "s/^$name //; s/ /,/g" done < <(getent passwd | sort -t: -k3n) } # add_key USER KEY|FILE|-: appends a validated key to the user's authorized_keys add_key() { local user=$1 src=$2 key file tmp group if [[ $src == - ]]; then key=$(cat) elif [[ -f $src ]]; then key=$(cat "$src") else key=$src fi key=$(printf '%s\n' "$key" | grep -v '^[[:space:]]*$' | head -n 1) tmp=$(mktemp) printf '%s\n' "$key" >"$tmp" if ! ssh-keygen -l -f "$tmp" >/dev/null 2>&1; then rm -f "$tmp" die "це не публічний SSH-ключ (потрібен рядок на кшталт 'ssh-ed25519 AAAA... ім'я')" \ "this is not a public SSH key (expected a line like 'ssh-ed25519 AAAA... name')" fi rm -f "$tmp" file=$(keys_file "$user") # Same key type and body, whatever the comment if key_lines "$file" | awk -v k="$(awk '{ print $1, $2 }' <<<"$key")" '$1 " " $2 == k { found = 1 } END { exit !found }'; then say "$user: цей ключ уже є" "$user: this key is already there" return 0 fi group=$(id -gn "$user") install -d -m 700 -o "$user" -g "$group" "$(dirname "$file")" printf '%s\n' "$key" >>"$file" chown "$user:$group" "$file" chmod 600 "$file" say "$user: ключ додано" "$user: key added" } # set_password USER: asks on a terminal, reads one line from stdin otherwise set_password() { local password if interactive; then passwd "$1" else IFS= read -r password || die "пароль не передано" "no password given" [[ -n $password ]] || die "порожній пароль" "empty password" # -c: Void's PAM stack for chpasswd does not hash passwords itself printf '%s:%s\n' "$1" "$password" | chpasswd -c SHA512 say "$1: пароль змінено" "$1: password changed" fi } cmd_add() { local name="" admin="" key="" password=ask groups while (($#)); do case $1 in --admin) admin=1 ;; --key) key=${2:?}; shift ;; --no-password) password=none ;; --password-stdin) password=stdin ;; -*) die "невідомий параметр $1 (duser help)" "unknown option $1 (duser help)" ;; *) [[ -z $name ]] || die "зайвий аргумент $1" "extra argument $1"; name=$1 ;; esac shift done if [[ -z $name ]]; then interactive || die "вкажіть ім'я користувача" "name the user" ask name "Ім'я користувача (латиницею)" "User name" fi [[ $name =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "ім'я '$name': малі латинські літери, цифри, '_', '-', до 32 символів" \ "name '$name': lowercase Latin letters, digits, '_', '-', up to 32 characters" ! id "$name" >/dev/null 2>&1 || die "користувач $name уже є" "user $name already exists" if [[ -z $admin ]] && interactive && [[ $password != stdin ]]; then confirm "Адміністратор (sudo, docker, журнали)?" "Administrator (sudo, docker, system logs)?" && admin=1 fi if [[ -z $key ]] && interactive && [[ $password != stdin ]]; then ask key "Публічний SSH-ключ: рядок або файл .pub (Enter — без ключа)" \ "Public SSH key: a line or a .pub file (Enter for none)" fi [[ $password == ask ]] && ! interactive && password=none # shellcheck disable=SC2086 # word lists groups=$(existing $user_groups ${admin:+$admin_groups}) useradd -m -s /bin/bash -G "$groups" "$name" say "$name: створено, групи $groups" "$name: created, groups $groups" # From here on the account exists: on a failure, say so instead of leaving # it half set up silently # shellcheck disable=SC2064 # $name is fixed by now trap "[[ \$? -eq 0 ]] || warn '$name створено не повністю; виправте через duser або видаліть: duser del $name --home' \ '$name is only partly set up; fix it with duser or delete it: duser del $name --home'" EXIT [[ -n $key ]] && add_key "$name" "$key" case $password in ask | stdin) set_password "$name" ;; none) [[ -n $key ]] || warn "$name не має ні пароля, ні ключа і поки не зможе увійти: duser passwd $name" \ "$name has neither a password nor a key and cannot log in yet: duser passwd $name" ;; esac trap - EXIT } # The current user (the one who ran sudo) me() { echo "${SUDO_USER:-$(id -un)}" } cmd_del() { local name=${1:-} home="" others [[ -n $name ]] || die "вкажіть користувача" "name the user" [[ ${2:-} == --home ]] && home=1 check_user "$name" [[ $name != "$(me)" ]] || die "не можна видалити себе" "you cannot delete yourself" if in_group "$name" wheel; then others=$(getent group wheel | cut -d: -f4 | tr ',' '\n' | grep -vx "$name" || true) [[ -n $others ]] || die "$name — єдиний адміністратор" "$name is the only administrator" fi if interactive; then confirm "Видалити користувача $name?" "Delete user $name?" || return 1 if [[ -z $home ]] && confirm "Видалити й домашній каталог $(getent passwd "$name" | cut -d: -f6)?" \ "Delete the home directory $(getent passwd "$name" | cut -d: -f6) too?"; then home=1 fi fi # userdel refuses while the user has running processes, which is right. # Void creates no mail spools, so drop userdel's warning about it. local out if ! out=$(userdel ${home:+-r} "$name" 2>&1); then echo "$out" >&2 exit 1 fi grep -v 'mail spool' <<<"$out" >&2 || true say "$name: видалено" "$name: deleted" } cmd_passwd() { local name=${1:-} if [[ -z $name || $name == "$(id -un)" ]] && [[ $EUID -ne 0 ]]; then # Your own password: no sudo, passwd asks for the current one exec passwd fi need_root passwd "$@" name=${name:-$(me)} check_user "$name" set_password "$name" } cmd_lock() { local name=${1:-} [[ -n $name ]] || die "вкажіть користувача" "name the user" check_user "$name" [[ $name != "$(me)" ]] || die "не можна заблокувати себе" "you cannot lock yourself out" # -L disables the password; an expired account also stops SSH key logins usermod -L -e 1 "$name" say "$name: вхід заборонено (поточні сеанси тривають; завершити: pkill -KILL -u $name)" \ "$name: logins forbidden (open sessions go on; to end them: pkill -KILL -u $name)" } cmd_unlock() { local name=${1:-} hash [[ -n $name ]] || die "вкажіть користувача" "name the user" check_user "$name" # (usermod -e '' complains "no changes" when the account never expired) chage -E -1 "$name" # Re-enable the password only when there is one under the "!" hash=$(getent shadow "$name" | cut -d: -f2) [[ $hash == \!\$* ]] && usermod -U "$name" say "$name: вхід дозволено" "$name: logins allowed" } cmd_key() { local name=${1:-} action=${2:-list} file n i line [[ -n $name ]] || die "вкажіть користувача" "name the user" check_user "$name" file=$(keys_file "$name") case $action in list | ls) if [[ -z $(key_lines "$file") ]]; then say "$name: SSH-ключів немає" "$name: no SSH keys" return 0 fi i=0 while IFS= read -r line; do i=$((i + 1)) printf '%2d %s\n' "$i" "$(ssh-keygen -l -f - <<<"$line" 2>/dev/null || echo "? $line")" done < <(key_lines "$file") ;; add) local src=${3:-} if [[ -z $src ]]; then interactive || die "вкажіть ключ, файл або -" "give a key, a file or -" ask src "Публічний SSH-ключ: рядок або файл .pub" "Public SSH key: a line or a .pub file" fi add_key "$name" "$src" ;; rm | del) n=${3:-} [[ $n =~ ^[1-9][0-9]*$ ]] || die "вкажіть номер ключа (duser key $name)" "give the key number (duser key $name)" line=$(key_lines "$file" | sed -n "${n}p") [[ -n $line ]] || die "немає ключа номер $n" "no key number $n" # Remove that exact line, keeping comments and the other keys awk -v k="$line" '$0 != k' "$file" >"$file.new" chown --reference="$file" "$file.new" chmod 600 "$file.new" mv "$file.new" "$file" say "$name: ключ $n видалено" "$name: key $n removed" ;; *) die "невідома дія $action (list, add, rm)" "unknown action $action (list, add, rm)" ;; esac } cmd_groups() { local name=${1:-} arg g [[ -n $name ]] || die "вкажіть користувача" "name the user" check_user "$name" shift for arg in "$@"; do g=${arg:1} case $arg in +?*) getent group "$g" >/dev/null || die "немає групи $g" "no group $g" gpasswd -a "$name" "$g" >/dev/null ;; -?*) if [[ $g == wheel && $name == "$(me)" ]]; then die "не можна позбавити себе прав адміністратора" "you cannot drop your own administrator rights" fi gpasswd -d "$name" "$g" >/dev/null ;; *) die "'$arg': потрібно +ГРУПА або -ГРУПА" "'$arg': expected +GROUP or -GROUP" ;; esac done echo "$name: $(id -nG "$name" | sed "s/^$name //; s/ /, /g")" } cmd=${1:-list} [[ $# -gt 0 ]] && shift case $cmd in list | ls) # /etc/shadow is root-only need_root list cmd_list ;; add | new) need_root add "$@"; cmd_add "$@" ;; del | rm | remove) need_root del "$@"; cmd_del "$@" ;; passwd | password) cmd_passwd "$@" ;; lock) need_root lock "$@"; cmd_lock "$@" ;; unlock) need_root unlock "$@"; cmd_unlock "$@" ;; key | keys) need_root key "$@"; cmd_key "$@" ;; groups | group) need_root groups "$@"; cmd_groups "$@" ;; -h | --help | help) usage ;; *) usage >&2 exit 1 ;; esac