Stage 7: command-line helpers dsv, duser, dfw, dsys (0.5.0)
- das-helpers replaces the dsv package and adds shared functions in /usr/lib/das/helpers.sh (bilingual messages, sudo re-exec, prompts). - dsv new creates a runit service (command, user, directory, environment, svlogd log) and checks it stays up; dsv rm deletes one not from a package. - duser: list, add (installer groups, --admin, key, password), del, passwd, lock/unlock (key logins too), key add|list|rm, groups +/-. - dfw: allow/deny/status/reload for the tcp_ports and udp_ports sets of the inet das table, kept in /etc/nftables.d/dfw.nft and checked with nft -c. - dsys: info, hostname, timezone, locale, keymap. - xtools-minimal on both images. - nftables.conf no longer suggests restarting the nftables service: its finish script flushes the whole ruleset, fail2ban's and Docker's included. - das-server-install 0.2.0 mentions the helpers when it finishes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Executable
+396
@@ -0,0 +1,396 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user