Sumber yang diterbitkan
Unduh ZIP sumber
CH-J Server Manager
Jelajahi direktori dan file untuk rilis aplikasi tertentu.
CH-J / Server Manager
377 file
core-20261019T111616Z-160a00bfcddc
Ditambahkan: · Dibuat:
1
# CH-J Server Manager - fixed remote helper for the chj.users capability (POSIX sh).2
# Core runs this text with "sh -c SCRIPT chj-users OPERATION ARGS...". Plugins cannot supply code.3
# Arguments are validated by Core and again here. Secrets and file contents never appear in argv:4
# they arrive as base64 stdin frames ("CHJ<TAG>:<base64>") only after the READY marker.5
set -u6
umask 0777
LC_ALL=C; export LC_ALL8
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; export PATH9
SSHD_MAIN=/etc/ssh/sshd_config10
SSHD_DROPIN=/etc/ssh/sshd_config.d11
SSHD_MANAGED=$SSHD_DROPIN/01-chj-users.conf12
STATE_DIR=/var/lib/chj-server-manager/users13
FRAME=14
op=${1:-}15
[ $# -gt 0 ] && shift16
printf '\036CHJ_READY\n'18
fail() { printf '\036CHJ_ERR=%s\n' "$1"; [ $# -gt 1 ] && printf '%s\n' "$2" | head -c 2000 >&2; exit 3; }19
section() { printf '\036CHJ_SECTION=%s\n' "$1"; }20
oneline() { tr '\r\n|\036' ' ' | cut -c1-400; }21
step() { printf '\036CHJ_STEP=%s|%s|%s\n' "$1" "$2" "$(printf '%s' "${3:-}" | oneline)"; }22
is_root() { [ "$(id -u)" = 0 ]; }23
need_root() { is_root || fail PRIVILEGE_REQUIRED "Root privileges are required."; }24
valid_name() { case $1 in ''|[!a-z_]*|*[!a-z0-9_-]*) return 1 ;; esac; [ ${#1} -le 32 ]; }25
valid_addr() { case $1 in ''|*[!0-9a-fA-F:.]*) return 1 ;; esac; [ ${#1} -le 45 ]; }26
valid_path() {27
case $1 in /*) ;; *) return 1 ;; esac28
case $1 in *[!A-Za-z0-9._/@+-]*|*/../*|*/..|*/./*|*//*) return 1 ;; esac29
[ ${#1} -le 255 ]30
}31
have() { command -v "$1" >/dev/null 2>&1; }32
uid_min() {33
value=$(awk '$1=="UID_MIN"{print $2; exit}' /etc/login.defs 2>/dev/null)34
case $value in ''|*[!0-9]*) value=1000 ;; esac35
printf '%s' "$value"36
}37
b64d() {38
if printf 'YQ==' | base64 -d >/dev/null 2>&1; then base64 -d39
elif printf 'YQ==' | openssl base64 -d -A >/dev/null 2>&1; then openssl base64 -d -A40
else return 127; fi41
}42
b64e() { base64 | tr -d '\r\n'; }43
sha256_of() {44
if [ -f "$1" ]; then45
if have sha256sum; then sha256sum < "$1" | cut -c1-64; else openssl dgst -sha256 < "$1" | sed 's/^.*= *//'; fi46
else printf 'absent'; fi47
}48
read_frame() {49
FRAME=50
while IFS= read -r line; do51
case $line in "CHJ$1:"*) FRAME=${line#"CHJ$1:"}; return 0 ;; esac52
done53
return 154
}55
passwd_entry() { getent passwd "$1" 2>/dev/null | head -n 1; }56
user_uid() { passwd_entry "$1" | cut -d: -f3; }57
user_home() { passwd_entry "$1" | cut -d: -f6; }58
group_members() { getent group "$1" 2>/dev/null | cut -d: -f4 | tr ',' '\n'; }59
is_member() { group_members "$2" | grep -qx "$1"; }61
# Destructive account changes are refused for root, system accounts and the sudo caller.62
protect() {63
valid_name "$1" || fail NAME_INVALID "Invalid account name."64
uid=$(user_uid "$1")65
[ -n "$uid" ] || fail USER_NOT_FOUND "The account does not exist."66
[ "$uid" -ne 0 ] || fail PROTECTED_USER "The root account is protected."67
[ "$uid" -ge "$(uid_min)" ] && [ "$uid" -ne 65534 ] || fail PROTECTED_USER "System accounts are protected."68
[ "$1" != "${SUDO_USER:-}" ] || fail PROTECTED_USER "The account of the active SSH session is protected."69
}71
# Runs a command as the target account so file permissions, not root, govern access.72
# BusyBox getent (Alpine) has no shadow database; root can read /etc/shadow directly. Only Core-side states leave the server.73
shadow_db() {74
out=$(getent shadow 2>/dev/null)75
if [ -n "$out" ]; then printf '%s\n' "$out"; else cat /etc/shadow 2>/dev/null; fi76
}77
run_as() {78
target=$1; shift79
if ! is_root; then80
[ "$(id -un)" = "$target" ] || return 12581
"$@"; return $?82
fi83
if have runuser; then runuser -u "$target" -- "$@"84
# BusyBox setpriv (Alpine) has no --reuid; only the util-linux variant is used.85
elif have setpriv && setpriv --help 2>&1 | grep -q -e '--reuid'; then setpriv --reuid="$(id -u "$target")" --regid="$(id -g "$target")" --init-groups -- "$@"86
else87
# BusyBox su (Alpine) and util-linux su hand extra arguments to the shell differently, so the command is passed88
# as one single-quoted string instead of positional parameters.89
cmd=exec90
for arg in "$@"; do cmd="$cmd '$(printf '%s' "$arg" | sed "s/'/'\\\\''/g")'"; done91
su -s /bin/sh -c "$cmd" "$target"92
fi93
}95
sudo_summary() {96
have sudo || { printf 'unavailable'; return; }97
sudo -n -l -U "$1" 2>/dev/null | awk '98
/is not allowed to run sudo/ { none=1 }99
/may run the following commands/ { rules=1; next }100
rules && /^[ \t]+\(/ {101
line=$0; nopw=(line ~ /NOPASSWD:/)102
sub(/^[ \t]+\([^)]*\)[ \t]*/, "", line)103
while (line ~ /^[A-Z_]+:[ \t]*/) sub(/^[A-Z_]+:[ \t]*/, "", line)104
gsub(/[ \t]+$/, "", line)105
if (line == "ALL") { full=1; if (nopw) fullnopw=1 } else any=1106
}107
END { if (full) print (fullnopw ? "full-nopasswd" : "full"); else if (any) print "limited"; else print "none" }'108
}109
sudoers_group_rule() {110
for file in /etc/sudoers /etc/sudoers.d/*; do111
[ -f "$file" ] || continue112
grep -hE "^[[:space:]]*%$1[[:space:]]" "$file" 2>/dev/null113
done | grep -q .114
}115
admin_group() {116
for group in sudo wheel admin; do117
getent group "$group" >/dev/null 2>&1 && sudoers_group_rule "$group" && { printf '%s' "$group"; return 0; }118
done119
return 1120
}122
sshd_bin() {123
if have sshd; then command -v sshd124
elif [ -x /usr/sbin/sshd ]; then printf '/usr/sbin/sshd'; fi125
}126
# "ok" only when sshd_config includes sshd_config.d before its first Match block.127
layout_state() {128
[ -f "$SSHD_MAIN" ] || { printf 'missing-main'; return; }129
awk '{ l=tolower($0); sub(/^[ \t]+/, "", l) }130
l ~ /^include[ \t]/ && l ~ /sshd_config\.d\/\*/ && !inc { inc=NR }131
l ~ /^match[ \t]/ && !m { m=NR }132
END { if (inc && (!m || inc < m)) print "ok"; else if (inc) print "include-after-match"; else print "include-missing" }' "$SSHD_MAIN" 2>/dev/null || printf 'unreadable'133
}134
service_state() {135
if have systemctl && [ -d /run/systemd/system ]; then136
for unit in ssh.service sshd.service; do137
[ "$(systemctl is-active "$unit" 2>/dev/null)" = active ] && { printf 'systemd:%s:active' "$unit"; return; }138
done139
for unit in ssh.socket sshd.socket; do140
[ "$(systemctl is-active "$unit" 2>/dev/null)" = active ] && { printf 'systemd:%s:active' "$unit"; return; }141
done142
printf 'systemd:none:inactive'; return143
fi144
# OpenRC (Alpine) also writes /run/sshd.pid, so it is checked before the bare pid file.145
if have rc-service; then146
rc-service sshd status >/dev/null 2>&1 && { printf 'openrc:sshd:active'; return; }147
fi148
for file in /run/sshd.pid /var/run/sshd.pid; do149
[ -r "$file" ] || continue150
pid=$(head -n 1 "$file" 2>/dev/null)151
case $pid in ''|*[!0-9]*) continue ;; esac152
if kill -0 "$pid" 2>/dev/null; then printf 'pidfile:%s:active' "$file"; else printf 'pidfile:%s:unknown' "$file"; fi153
return154
done155
printf 'unknown:none:unknown'156
}157
# After SIGHUP sshd re-executes; some builds come back under a new PID and rewrite the pid file.158
# Success = a live sshd listed in the pid file within 10 s.159
sshd_running_after_reload() {160
tries=0161
while [ "$tries" -lt 20 ]; do162
for file in /run/sshd.pid /var/run/sshd.pid; do163
[ -r "$file" ] || continue164
pid=$(head -n 1 "$file" 2>/dev/null)165
case $pid in ''|*[!0-9]*) continue ;; esac166
kill -0 "$pid" 2>/dev/null && return 0167
done168
sleep 0.5 2>/dev/null || sleep 1169
tries=$((tries + 1))170
done171
return 1172
}173
reload_sshd() {174
state=$(service_state); kind=${state%%:*}; rest=${state#*:}; unit=${rest%%:*}175
case $kind in176
systemd)177
case $unit in178
*.socket) printf 'socket-activated'; return 0 ;;179
none) printf 'sshd service is not active'; return 1 ;;180
esac181
systemctl reload "$unit" 2>&1 ;;182
pidfile)183
pid=$(head -n 1 "$unit"); kill -HUP "$pid" 2>&1 || return 1184
sleep 1; sshd_running_after_reload || { printf 'sshd stopped after reload'; return 1; } ;;185
openrc)186
rc-service sshd reload 2>&1 || return 1187
sleep 1; sshd_running_after_reload || rc-service sshd status >/dev/null 2>&1 || { printf 'sshd stopped after reload'; return 1; } ;;188
*) printf 'sshd service manager was not recognised'; return 1 ;;189
esac190
}191
effective() {192
# $1 user, $2 address; prints only the authentication and access keywords Core evaluates.193
"$SSHD" -T -C "user=$1,host=$2,addr=$2" 2>&1 | grep -E '^(passwordauthentication|kbdinteractiveauthentication|challengeresponseauthentication|pubkeyauthentication|authenticationmethods|permitrootlogin|denyusers|allowusers|denygroups|allowgroups|authorizedkeysfile|authorizedkeyscommand|addressfamily|listenaddress|port|usepam|strictmodes) |^[^a-z]' | head -n 300194
}195
expand_keys_path() {196
awk -v s="$1" -v h="$2" -v u="$3" -v id="$4" 'BEGIN {197
out=""; n=length(s)198
for (i=1; i<=n; i++) {199
c=substr(s,i,1)200
if (c=="%" && i<n) { d=substr(s,i+1,1); i++201
if (d=="%") out=out "%"; else if (d=="h") out=out h; else if (d=="u") out=out u; else if (d=="U") out=out id; else { print "UNSUPPORTED"; exit }202
} else out=out c203
}204
if (substr(out,1,1)!="/") out=h "/" out205
print out }'206
}207
file_meta() {208
if [ -L "$1" ]; then printf 'symlink'209
elif [ -e "$1" ]; then stat -c '%U:%G:%a:%F' "$1" 2>/dev/null || printf 'unknown'210
else printf 'absent'; fi211
}213
print_managed() {214
section managed215
if [ -L "$SSHD_MANAGED" ]; then printf 'unsafe=1\n'216
elif [ -f "$SSHD_MANAGED" ] && [ -r "$SSHD_MANAGED" ]; then217
printf 'sha256=%s\ndata=' "$(sha256_of "$SSHD_MANAGED")"; b64e < "$SSHD_MANAGED"; printf '\n'218
elif [ -e "$SSHD_MANAGED" ]; then printf 'unreadable=1\n'219
else printf 'sha256=absent\n'; fi220
}222
READ_KEYS='p=$1223
if [ -L "$p" ]; then printf "type=symlink\n"; exit 0; fi224
if [ ! -e "$p" ]; then if [ -d "${p%/*}" ]; then printf "type=absent\n"; else printf "type=absent-dir\n"; fi; exit 0; fi225
if [ ! -f "$p" ]; then printf "type=other\n"; exit 0; fi226
if [ ! -r "$p" ]; then printf "type=unreadable\n"; exit 0; fi227
size=$(wc -c < "$p" | tr -d " ")228
printf "type=file\nsize=%s\n" "$size"229
[ "$size" -le 262144 ] || { printf "tooLarge=1\n"; exit 0; }230
printf "data="; base64 < "$p" | tr -d "\r\n"; printf "\n"'232
# Executed as the key owner. Exit codes are mapped by Core.233
WRITE_KEYS='set -u; target=$1; expected=$2; dir=${target%/*}234
[ ! -L "$dir" ] || exit 21235
if [ ! -e "$dir" ]; then mkdir -m 700 -- "$dir" || exit 22; fi236
[ -d "$dir" ] || exit 23237
[ ! -L "$target" ] || exit 24238
if [ -e "$target" ] && [ ! -f "$target" ]; then exit 25; fi239
if [ -f "$target" ]; then240
if command -v sha256sum >/dev/null 2>&1; then current=$(sha256sum < "$target" | cut -c1-64); else current=$(openssl dgst -sha256 < "$target" | sed "s/^.*= *//"); fi241
else current=absent; fi242
[ "$current" = "$expected" ] || exit 26243
tmp=$(mktemp "$dir/.authorized_keys.chj-XXXXXX") || exit 27244
if ! cat > "$tmp"; then rm -f -- "$tmp"; exit 28; fi245
chmod 600 "$tmp" || { rm -f -- "$tmp"; exit 28; }246
mv -f -- "$tmp" "$target" || { rm -f -- "$tmp"; exit 29; }'248
op_probe() {249
printf 'user=%s\nuid=%s\n' "$(id -un)" "$(id -u)"250
printf 'groups=%s\n' "$(id -Gn 2>/dev/null | tr ' ' ',')"251
printf 'client=%s\n' "${SSH_CLIENT%% *}"252
if have sudo; then253
out=$(sudo -n -p '' true 2>&1 </dev/null); code=$?254
printf 'sudoInstalled=1\nsudoNonInteractive=%s\nsudoMessage=%s\n' "$code" "$(printf '%s' "$out" | head -n 1 | oneline)"255
else256
printf 'sudoInstalled=0\n'257
fi258
}260
op_inventory() {261
section meta262
printf 'whoami=%s\nuid=%s\nuidMin=%s\n' "$(id -un)" "$(id -u)" "$(uid_min)"263
printf 'os=%s\n' "$(sed -n 's/^PRETTY_NAME=//p' /etc/os-release 2>/dev/null | head -n 1 | tr -d '"' | oneline)"264
printf 'osId=%s\n' "$(sed -n 's/^ID=//p' /etc/os-release 2>/dev/null | head -n 1 | tr -d '"' | oneline)"265
for tool in useradd usermod userdel gpasswd chpasswd runuser setpriv sudo systemctl pgrep base64; do266
have "$tool" && printf 'tool=%s\n' "$tool"267
done268
SSHD=$(sshd_bin); [ -n "$SSHD" ] && printf 'tool=sshd\n'269
printf 'layout=%s\n' "$(layout_state)"270
printf 'managedPath=%s\n' "$SSHD_MANAGED"271
section passwd; getent passwd | head -n 5000272
section group; getent group | head -n 5000273
section shells; grep -E '^/' /etc/shells 2>/dev/null274
print_managed275
if is_root; then276
section admingroups277
for group in sudo wheel admin; do278
getent group "$group" >/dev/null 2>&1 && sudoers_group_rule "$group" && printf '%s\n' "$group"279
done280
section shadow281
# Password hashes never leave the server: only a coarse state is printed.282
shadow_db | awk -F: '{283
h=$2; s="set"284
if (h=="") s="empty"285
else if (h=="!" || h=="!!" || h=="!*" || h=="*" || h=="*LK*") s="none"286
else if (substr(h,1,1)=="!") s="locked"287
print $1 ":" s ":" $3 ":" $8 }'288
section sudo289
min=$(uid_min)290
getent passwd | while IFS=: read -r name _ uid _; do291
case $uid in ''|*[!0-9]*) continue ;; esac292
if [ "$uid" -eq 0 ] || { [ "$uid" -ge "$min" ] && [ "$uid" -ne 65534 ]; } || is_member "$name" sudo || is_member "$name" wheel || is_member "$name" admin; then293
printf '%s:%s\n' "$name" "$(sudo_summary "$name")"294
fi295
done | head -n 500296
fi297
section end298
}300
op_detail() {301
[ $# -eq 3 ] || fail ARGUMENTS_INVALID "Invalid arguments."302
name=$1; addr4=$2; addr6=$3303
valid_name "$name" || fail NAME_INVALID "Invalid account name."304
{ [ -z "$addr4" ] || valid_addr "$addr4"; } && { [ -z "$addr6" ] || valid_addr "$addr6"; } || fail ARGUMENTS_INVALID "Invalid address."305
entry=$(passwd_entry "$name"); [ -n "$entry" ] || fail USER_NOT_FOUND "The account does not exist."306
uid=$(printf '%s' "$entry" | cut -d: -f3); home=$(printf '%s' "$entry" | cut -d: -f6)307
section account308
printf 'passwd=%s\n' "$entry"309
printf 'uidMin=%s\n' "$(uid_min)"310
printf 'groups=%s\n' "$(id -Gn "$name" 2>/dev/null | tr ' ' ',')"311
if have pgrep; then printf 'processes=%s\n' "$(pgrep -u "$name" 2>/dev/null | wc -l | tr -d ' ')"; fi312
printf 'homeMeta=%s\n' "$(file_meta "$home")"313
if is_root; then314
printf 'shadow=%s\n' "$(shadow_db | awk -F: -v n="$name" '$1==n' | awk -F: '{315
h=$2; s="set"; if (h=="") s="empty"; else if (h=="!" || h=="!!" || h=="!*" || h=="*" || h=="*LK*") s="none"; else if (substr(h,1,1)=="!") s="locked"316
print s ":" $3 ":" $8 }')"317
printf 'sudo=%s\n' "$(sudo_summary "$name")"318
fi319
section ssh320
SSHD=$(sshd_bin)321
akf=".ssh/authorized_keys .ssh/authorized_keys2"322
if [ -z "$SSHD" ]; then printf 'sshd=missing\n'323
else324
printf 'sshd=%s\nversion=%s\n' "$SSHD" "$("$SSHD" -V 2>&1 | head -n 1 | oneline)"325
printf 'layout=%s\nservice=%s\n' "$(layout_state)" "$(service_state)"326
if is_root; then327
out=$("$SSHD" -t 2>&1); code=$?328
printf 'syntax=%s\nsyntaxMessage=%s\n' "$code" "$(printf '%s' "$out" | head -n 3 | oneline)"329
if [ -n "$addr4" ]; then section effective4; effective "$name" "$addr4"; fi330
if [ -n "$addr6" ]; then section effective6; effective "$name" "$addr6"; fi331
configured=$(effective "$name" "${addr4:-${addr6:-192.0.2.10}}" | sed -n 's/^authorizedkeysfile //p' | head -n 1)332
[ -n "$configured" ] && akf=$configured333
fi334
fi335
print_managed336
section keys337
printf 'home=%s\n' "$home"338
set -f339
count=0340
for spec in $akf; do341
count=$((count + 1)); [ "$count" -le 4 ] || break342
[ "$spec" != none ] || continue343
path=$(expand_keys_path "$spec" "$home" "$name" "$uid")344
printf '\036CHJ_KEYFILE=%s\n' "$path"345
printf 'spec=%s\n' "$spec"346
case $path in UNSUPPORTED) printf 'type=unsupported\n'; continue ;; esac347
printf 'meta=%s\ndirMeta=%s\n' "$(file_meta "$path")" "$(file_meta "${path%/*}")"348
run_as "$name" sh -c "$READ_KEYS" chj-keys "$path" 2>/dev/null || printf 'type=unreadable\n'349
done350
set +f351
section end352
}354
op_keys_write() {355
[ $# -eq 3 ] || fail ARGUMENTS_INVALID "Invalid arguments."356
name=$1; path=$2; expected=$3357
valid_name "$name" || fail NAME_INVALID "Invalid account name."358
valid_path "$path" || fail KEYS_PATH_UNSUPPORTED "The authorized_keys path is not supported."359
case $expected in absent|[0-9a-f]*) ;; *) fail ARGUMENTS_INVALID "Invalid checksum." ;; esac360
uid=$(user_uid "$name"); [ -n "$uid" ] || fail USER_NOT_FOUND "The account does not exist."361
home=$(user_home "$name")362
valid_path "$home" && [ "$home" != / ] || fail KEYS_PATH_UNSUPPORTED "The home directory is not usable."363
case $path in "$home"/*) ;; *) fail KEYS_PATH_UNSUPPORTED "Only key files inside the home directory are managed." ;; esac364
read_frame DATA || fail PROTOCOL_ERROR "Key data was not received."365
work=$(mktemp -d) || fail TEMPORARY_FAILED "Temporary directory could not be created."366
trap 'rm -rf -- "$work"' EXIT367
printf '%s' "$FRAME" | b64d > "$work/data" || fail PROTOCOL_ERROR "Key data could not be decoded."368
run_as "$name" sh -c "$WRITE_KEYS" chj-keys "$path" "$expected" < "$work/data"369
code=$?370
case $code in371
0) ;;372
21|24) fail KEYS_SYMLINK "The key file or its directory is a symbolic link; it was not changed." ;;373
22|23) fail KEYS_DIRECTORY_FAILED "The .ssh directory could not be created or is not a directory." ;;374
25) fail KEYS_NOT_REGULAR "The key file is not a regular file." ;;375
26) fail KEYS_CHANGED "The key file changed after it was read. Reload and try again." ;;376
125) fail PRIVILEGE_REQUIRED "Root privileges are required for keys of another account." ;;377
*) fail KEYS_WRITE_FAILED "The key file could not be written (exit $code)." ;;378
esac379
if is_root && have restorecon; then restorecon -F "${path%/*}" "$path" >/dev/null 2>&1 || true; fi380
printf 'sha256=%s\nmeta=%s\ndirMeta=%s\n' "$(run_as "$name" sh -c 'if command -v sha256sum >/dev/null 2>&1; then sha256sum < "$1" | cut -c1-64; else openssl dgst -sha256 < "$1" | sed "s/^.*= *//"; fi' chj-keys "$path")" "$(file_meta "$path")" "$(file_meta "${path%/*}")"381
printf '\036CHJ_RESULT=ok\n'382
}384
check_shell() {385
valid_path "$1" || fail SHELL_INVALID "Invalid shell path."386
[ -x "$1" ] || fail SHELL_INVALID "The shell does not exist or is not executable."387
case ${1##*/} in nologin|false) return 0 ;; esac388
grep -qxF "$1" /etc/shells 2>/dev/null || fail SHELL_INVALID "The shell is not listed in /etc/shells."389
}390
set_password() {391
have chpasswd || { ERRMSG="chpasswd is not available"; return 127; }392
read_frame SECRET || { ERRMSG="password was not received"; return 2; }393
secret=$(printf '%s' "$FRAME" | b64d) || { FRAME=; ERRMSG="password could not be decoded"; return 3; }394
FRAME=395
[ -n "$secret" ] || { ERRMSG="empty password"; return 4; }396
ERRMSG=$(printf '%s:%s\n' "$1" "$secret" | chpasswd 2>&1); code=$?397
secret=398
return $code399
}400
grant_sudo() {401
group=$(admin_group) || { ERRMSG="No sudo, wheel or admin group is referenced by sudoers; sudoers was not changed."; return 2; }402
ERRMSG=$(usermod -a -G "$group" -- "$1" 2>&1) || return 1403
ERRMSG="group $group"404
}405
revoke_sudo() {406
removed=407
for group in sudo wheel admin; do408
is_member "$1" "$group" || continue409
if have gpasswd; then ERRMSG=$(gpasswd -d "$1" "$group" 2>&1) || return 1410
elif have deluser; then ERRMSG=$(deluser "$1" "$group" 2>&1) || return 1411
else ERRMSG="gpasswd is not available"; return 127; fi412
removed="$removed $group"413
done414
ERRMSG="removed from:${removed:- none}"415
}417
op_create() {418
[ $# -eq 9 ] || fail ARGUMENTS_INVALID "Invalid arguments."419
name=$1; shell=$2; home=$3; mkhome=$4; primary=$5; groups=$6; comment=$7; admin=$8; withpw=$9420
need_root421
valid_name "$name" || fail NAME_INVALID "Invalid account name."422
have useradd || fail TOOL_MISSING "useradd is not available on this server."423
[ -z "$(passwd_entry "$name")" ] || fail USER_EXISTS "The account already exists."424
if [ -z "$primary" ] && getent group "$name" >/dev/null 2>&1; then fail GROUP_EXISTS "A group with this name already exists; select a primary group."; fi425
check_shell "$shell"426
if [ -n "$home" ]; then valid_path "$home" || fail HOME_INVALID "Invalid home directory."; fi427
if [ -n "$home" ] && [ "$mkhome" = 1 ] && [ -e "$home" ]; then fail HOME_EXISTS "The home directory already exists."; fi428
if [ -n "$primary" ]; then valid_name "$primary" && getent group "$primary" >/dev/null 2>&1 || fail GROUP_NOT_FOUND "Primary group does not exist."; fi429
for group in $(printf '%s' "$groups" | tr ',' ' '); do430
valid_name "$group" && getent group "$group" >/dev/null 2>&1 || fail GROUP_NOT_FOUND "Group $group does not exist."431
done432
set -- -s "$shell"433
if [ "$mkhome" = 1 ]; then set -- "$@" -m; else set -- "$@" -M; fi434
[ -n "$home" ] && set -- "$@" -d "$home"435
if [ -n "$primary" ]; then set -- "$@" -g "$primary"; else set -- "$@" -U; fi436
[ -n "$groups" ] && set -- "$@" -G "$groups"437
[ -n "$comment" ] && set -- "$@" -c "$comment"438
out=$(useradd "$@" -- "$name" 2>&1) || fail USERADD_FAILED "$out"439
step create ok "$out"440
result=ok441
if [ "$withpw" = 1 ]; then442
if set_password "$name"; then step password ok; else step password failed "$ERRMSG"; result=partial; fi443
fi444
if [ "$admin" = 1 ]; then445
if grant_sudo "$name"; then step sudo ok "$ERRMSG"; else step sudo failed "$ERRMSG"; result=partial; fi446
fi447
printf '\036CHJ_RESULT=%s\n' "$result"448
}450
op_account() {451
[ $# -ge 2 ] || fail ARGUMENTS_INVALID "Invalid arguments."452
action=$1; name=$2; value=${3:-}; flag=${4:-0}453
need_root454
protect "$name"455
have usermod || fail TOOL_MISSING "usermod is not available on this server."456
case $action in457
lock) out=$(usermod -L -- "$name" 2>&1) || fail USERMOD_FAILED "$out" ;;458
unlock) out=$(usermod -U -- "$name" 2>&1) || fail USERMOD_FAILED "$out" ;;459
expire) out=$(usermod -e 1 -- "$name" 2>&1) || fail USERMOD_FAILED "$out" ;;460
unexpire) out=$(usermod -e '' -- "$name" 2>&1) || fail USERMOD_FAILED "$out" ;;461
password) set_password "$name" || fail PASSWORD_FAILED "$ERRMSG" ;;462
shell) check_shell "$value"; out=$(usermod -s "$value" -- "$name" 2>&1) || fail USERMOD_FAILED "$out" ;;463
comment) out=$(usermod -c "$value" -- "$name" 2>&1) || fail USERMOD_FAILED "$out" ;;464
home)465
valid_path "$value" && [ "$value" != / ] || fail HOME_INVALID "Invalid home directory."466
if [ "$flag" = 1 ]; then467
[ ! -e "$value" ] || fail HOME_EXISTS "The target directory already exists."468
out=$(usermod -d "$value" -m -- "$name" 2>&1) || fail USERMOD_FAILED "$out"469
else out=$(usermod -d "$value" -- "$name" 2>&1) || fail USERMOD_FAILED "$out"; fi ;;470
primary)471
valid_name "$value" && getent group "$value" >/dev/null 2>&1 || fail GROUP_NOT_FOUND "The group does not exist."472
out=$(usermod -g "$value" -- "$name" 2>&1) || fail USERMOD_FAILED "$out" ;;473
group-add)474
valid_name "$value" && getent group "$value" >/dev/null 2>&1 || fail GROUP_NOT_FOUND "The group does not exist."475
out=$(usermod -a -G "$value" -- "$name" 2>&1) || fail USERMOD_FAILED "$out" ;;476
group-remove)477
valid_name "$value" || fail GROUP_NOT_FOUND "Invalid group."478
is_member "$name" "$value" || fail GROUP_NOT_MEMBER "The account is not a supplementary member of this group."479
if have gpasswd; then out=$(gpasswd -d "$name" "$value" 2>&1) || fail GROUP_CHANGE_FAILED "$out"480
elif have deluser; then out=$(deluser "$name" "$value" 2>&1) || fail GROUP_CHANGE_FAILED "$out"481
else fail TOOL_MISSING "gpasswd is not available on this server."; fi ;;482
sudo-grant) grant_sudo "$name" || fail SUDO_GROUP_UNAVAILABLE "$ERRMSG"; step sudo ok "$ERRMSG" ;;483
sudo-revoke) revoke_sudo "$name" || fail GROUP_CHANGE_FAILED "$ERRMSG"; step sudo ok "$ERRMSG" ;;484
*) fail ACTION_INVALID "Unsupported account action." ;;485
esac486
[ "$action" = sudo-grant ] || [ "$action" = sudo-revoke ] || step "$action" ok487
if [ "$action" = sudo-grant ] || [ "$action" = sudo-revoke ]; then printf '\036CHJ_SUDO=%s\n' "$(sudo_summary "$name")"; fi488
printf '\036CHJ_RESULT=ok\n'489
}491
op_delete() {492
[ $# -eq 2 ] || fail ARGUMENTS_INVALID "Invalid arguments."493
name=$1; remove_home=$2494
need_root495
protect "$name"496
have userdel || fail TOOL_MISSING "userdel is not available on this server."497
if have pgrep && pgrep -u "$name" >/dev/null 2>&1; then fail USER_HAS_PROCESSES "The account still has running processes."; fi498
if [ "$remove_home" = 1 ]; then out=$(userdel -r -- "$name" 2>&1); code=$?; else out=$(userdel -- "$name" 2>&1); code=$?; fi499
[ -z "$(passwd_entry "$name")" ] || fail USERDEL_FAILED "$out"500
if [ "$code" -eq 0 ]; then step delete ok; printf '\036CHJ_RESULT=ok\n'501
else step delete ok; step home failed "$out"; printf '\036CHJ_RESULT=partial\n'; fi502
}504
# Staged and confirmed change of the CH-J managed sshd drop-in. Core decides via stdin:505
# COMMIT reloads sshd, CONFIRM keeps it; ROLLBACK, EOF or the watchdog restores the previous file.506
op_ssh_apply() {507
[ $# -ge 2 ] || fail ARGUMENTS_INVALID "Invalid arguments."508
expected=$1; timeout=$2; shift 2509
case $expected in absent|[0-9a-f]*) ;; *) fail ARGUMENTS_INVALID "Invalid checksum." ;; esac510
case $timeout in ''|*[!0-9]*) fail ARGUMENTS_INVALID "Invalid timeout." ;; esac511
for tuple in "$@"; do valid_name "${tuple%%|*}" && valid_addr "${tuple#*|}" || fail ARGUMENTS_INVALID "Invalid verification target."; done512
need_root513
SSHD=$(sshd_bin); [ -n "$SSHD" ] || fail SSHD_MISSING "sshd was not found."514
[ "$(layout_state)" = ok ] || fail SSH_LAYOUT_UNSUPPORTED "sshd_config does not include sshd_config.d before its first Match block."515
[ -d "$SSHD_DROPIN" ] && [ ! -L "$SSHD_DROPIN" ] || fail SSH_LAYOUT_UNSUPPORTED "sshd_config.d is missing or is a symbolic link."516
if [ -e "$SSHD_MANAGED" ] || [ -L "$SSHD_MANAGED" ]; then517
[ -f "$SSHD_MANAGED" ] && [ ! -L "$SSHD_MANAGED" ] || fail SSH_MANAGED_FILE_UNSAFE "The managed file is not a regular file."518
fi519
[ "$(sha256_of "$SSHD_MANAGED")" = "$expected" ] || fail SSH_POLICY_CHANGED "The managed SSH policy changed after it was read."520
out=$("$SSHD" -t 2>&1) || fail SSH_CONFIG_ALREADY_INVALID "$out"521
read_frame DATA || fail PROTOCOL_ERROR "Policy data was not received."522
mkdir -p "$STATE_DIR" && chmod 700 "$STATE_DIR" || fail STATE_FAILED "State directory could not be created."523
work=$(mktemp -d "$STATE_DIR/apply.XXXXXX") || fail STATE_FAILED "Work directory could not be created."524
printf '%s' "$FRAME" | b64d > "$work/new" || { rm -rf -- "$work"; fail PROTOCOL_ERROR "Policy data could not be decoded."; }525
stamp=$(date -u +%Y%m%dT%H%M%SZ)526
if [ -f "$SSHD_MANAGED" ]; then had=1; backup="$STATE_DIR/sshd-managed.$stamp.conf.bak"; cp -p "$SSHD_MANAGED" "$backup" || fail STATE_FAILED "Backup failed."527
else had=0; backup="$STATE_DIR/sshd-managed.$stamp.absent"; : > "$backup"; fi528
ls -1t "$STATE_DIR"/sshd-managed.* 2>/dev/null | tail -n +11 | while IFS= read -r old; do rm -f -- "$old"; done529
restore() {530
if [ "$had" = 1 ]; then cp -p "$backup" "$SSHD_DROPIN/.chj-users.restore" && mv -f "$SSHD_DROPIN/.chj-users.restore" "$SSHD_MANAGED"531
else rm -f -- "$SSHD_MANAGED"; fi532
}533
claim() { mv "$work/pending" "$work/claimed" 2>/dev/null; }534
for tuple in "$@"; do printf '\036CHJ_BEFORE=%s\n' "$tuple"; effective "${tuple%%|*}" "${tuple#*|}"; done535
if [ -s "$work/new" ]; then536
cp "$work/new" "$SSHD_DROPIN/.chj-users.tmp" && chmod 644 "$SSHD_DROPIN/.chj-users.tmp" && chown 0:0 "$SSHD_DROPIN/.chj-users.tmp" \537
&& mv -f "$SSHD_DROPIN/.chj-users.tmp" "$SSHD_MANAGED" || { rm -f -- "$SSHD_DROPIN/.chj-users.tmp"; restore; fail SSH_WRITE_FAILED "The managed file could not be written."; }538
else rm -f -- "$SSHD_MANAGED"; fi539
if ! out=$("$SSHD" -t 2>&1); then restore; rm -rf -- "$work"; fail SSH_CONFIG_INVALID "$out"; fi540
for tuple in "$@"; do printf '\036CHJ_AFTER=%s\n' "$tuple"; effective "${tuple%%|*}" "${tuple#*|}"; done541
: > "$work/pending"542
( trap '' HUP INT543
sleep "$timeout"544
if mv "$work/pending" "$work/claimed" 2>/dev/null; then545
restore546
[ -e "$work/reloaded" ] && reload_sshd >/dev/null 2>&1547
printf 'watchdog rollback %s\n' "$stamp" > "$STATE_DIR/last-watchdog-rollback"548
fi549
rm -rf -- "$work" ) </dev/null >/dev/null 2>&1 &550
printf '\036CHJ_STAGED\n'551
IFS= read -r decision || decision=EOF552
if [ "$decision" != COMMIT ]; then553
claim && restore554
printf '\036CHJ_ROLLED_BACK=%s\n' "$decision"; exit 0555
fi556
if ! out=$(reload_sshd); then557
claim && restore558
"$SSHD" -t >/dev/null 2>&1 && reload_sshd >/dev/null 2>&1559
fail SSH_RELOAD_FAILED "$out"560
fi561
: > "$work/reloaded"562
printf '\036CHJ_RELOADED=%s|%s\n' "$(service_state)" "$(printf '%s' "$out" | oneline)"563
IFS= read -r decision || decision=EOF564
if [ "$decision" = CONFIRM ] && claim; then565
printf '\036CHJ_CONFIRMED=%s\n' "$backup"; exit 0566
fi567
if claim; then restore; reload_sshd >/dev/null 2>&1; printf '\036CHJ_ROLLED_BACK=%s\n' "$decision"568
else printf '\036CHJ_ROLLED_BACK=watchdog\n'; fi569
exit 0570
}572
case $op in573
probe) op_probe ;;574
inventory) op_inventory ;;575
detail) op_detail "$@" ;;576
keys-write) op_keys_write "$@" ;;577
create) op_create "$@" ;;578
account) op_account "$@" ;;579
delete) op_delete "$@" ;;580
ssh-apply) op_ssh_apply "$@" ;;581
*) fail ACTION_INVALID "Unsupported operation." ;;582
esacSHA-256
6bb7e2b1e50b9200da072610701d55cc804a5e20dab76d44857b611b9e1b1cd8CH-J Proprietary Software License 1.14 · Kode sumber disediakan berdasarkan CH-J Proprietary Software License 1.14. Ketersediaannya tidak mengubah ketentuan lisensi atau memberikan hak tambahan.
SHA-256 arsip
a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a