Operators become a declared fact, not an accumulation of adduser runs: a line-based, bash-parseable users file (no YAML, no jq — a rig box has neither) names each user, their roles, and their keys, and apply converges the box to exactly that. Roles map to groups (admin→rig-admin with full NOPASSWD sudo, rig→rig sudo for the rig binary only, box→incus with no sudo — box's setup-host owns Incus, rig only asserts the group). Every password stays locked always; the SSH key at the door is the authentication. A user dropped from the file is found via the /etc/rig/users ledger and locked, never deleted — deleting frees the uid and rots attribution. The sudoers drop-in lands only after visudo -c passes, because a bad file under sudoers.d takes down all of sudo. Class never gates apply (#26: a shared root login is unattributable, so operators belong on every class); the marker only colors what root SSH does next. The whole file is validated in one pass before the root check, every error named with its line, so refusals are provable in the non-root harness through the sourced parser. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
65 lines
2.3 KiB
Bash
Executable file
65 lines
2.3 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# rig users status — what this box's operator accounts actually are, read from
|
|
# the machine itself: roles derived from REAL group membership (not the
|
|
# ledger's memory of an apply), key counts from authorized_keys, lock state
|
|
# from shadow. Reads only — no network, no writes.
|
|
set -euo pipefail
|
|
|
|
log() { printf 'rig-users: %s\n' "$*"; }
|
|
warn() { printf 'rig-users: WARNING: %s\n' "$*" >&2; }
|
|
die() { printf 'rig-users: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; }
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
usage: rig users status
|
|
|
|
Per rig-managed user (the /etc/rig/users ledger): roles derived from the
|
|
groups the user is ACTUALLY in (rig-admin -> admin, rig -> rig, incus -> box),
|
|
the authorized_keys count, and whether the account is locked or active.
|
|
Reads the box only — no network, no writes. Run as root (shadow is read).
|
|
EOF
|
|
}
|
|
|
|
# --- args (validated before the root check, so errors are testable) ---------
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
-h|--help) usage; exit 0 ;;
|
|
*) die "unknown flag: $1" 2 ;;
|
|
esac
|
|
done
|
|
|
|
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
|
|
|
LEDGER=/etc/rig/users
|
|
if [ ! -r "$LEDGER" ]; then
|
|
log "no rig-managed users (no $LEDGER yet — rig users apply creates it)"
|
|
exit 0
|
|
fi
|
|
|
|
while IFS= read -r u; do
|
|
[ -n "$u" ] || continue
|
|
if ! id -u "$u" >/dev/null 2>&1; then
|
|
# In the ledger but off the box: someone deleted by hand what rig only
|
|
# ever locks. Say so rather than crash or silently skip.
|
|
warn "$u: in the ledger but not on the box (rig never deletes — removed by hand?)"
|
|
continue
|
|
fi
|
|
groups=" $(id -nG "$u") "
|
|
roles=""
|
|
case "$groups" in *" rig-admin "*) roles="admin" ;; esac
|
|
case "$groups" in *" rig "*) roles="${roles:+$roles,}rig" ;; esac
|
|
case "$groups" in *" incus "*) roles="${roles:+$roles,}box" ;; esac
|
|
[ -n "$roles" ] || roles="none"
|
|
home="$(getent passwd "$u" | cut -d: -f6)"
|
|
keys=0
|
|
if [ -r "$home/.ssh/authorized_keys" ]; then
|
|
keys="$(grep -c . "$home/.ssh/authorized_keys" || true)"
|
|
fi
|
|
# Field 2 of `passwd -S` is the lock flag; locked is apply's resting state
|
|
# for a user dropped from the file, so it is the fact worth surfacing.
|
|
state=active
|
|
case "$(passwd -S "$u" 2>/dev/null | awk '{print $2}')" in
|
|
L|LK) state=locked ;;
|
|
esac
|
|
log "$u roles=$roles keys=$keys $state"
|
|
done < "$LEDGER"
|