#!/bin/sh # sysadminctl — macOS user management stub for Darling # # Translates macOS sysadminctl commands to direct /etc/passwd file operations. # Only implements the subset needed by the Nix installer: # # sysadminctl -addUser [-UID ] [-GID ] # [-home ] [-shell ] [-fullName ] # sysadminctl -deleteUser # # This stub operates on the Darling prefix's /etc/passwd file, NOT the host's. # # See: plan/07-phase5-daemon.md (Task 5.1) set -eu PASSWD_FILE="/etc/passwd" # ── Helpers ───────────────────────────────────────────────────────────────── errcho() { echo "$@" >&2 } usage() { errcho "Usage: sysadminctl -addUser [-UID ] [-GID ]" errcho " [-home ] [-shell ] [-fullName ]" errcho " sysadminctl -deleteUser " errcho "" errcho "Options:" errcho " -addUser Create a new user" errcho " -deleteUser Delete an existing user" errcho " -UID User ID (numeric)" errcho " -GID Primary group ID (numeric)" errcho " -home Home directory (default: /var/empty)" errcho " -shell Login shell (default: /usr/bin/false)" errcho " -fullName Full name / GECOS field" errcho " -password Password (ignored — always set to locked)" errcho " -adminUser Admin user for authentication (ignored)" errcho " -adminPassword Admin password (ignored)" exit 64 } # user_exists — exit 0 if the user exists in /etc/passwd user_exists() { grep -q "^${1}:" "$PASSWD_FILE" 2>/dev/null } # uid_exists — exit 0 if the UID is already taken uid_exists() { awk -F: -v uid="$1" '$3 == uid { found=1; exit } END { exit !found }' "$PASSWD_FILE" 2>/dev/null } # get_uid_owner — print the username that owns this UID get_uid_owner() { awk -F: -v uid="$1" '$3 == uid { print $1; exit }' "$PASSWD_FILE" } # next_uid — find the next available UID >= 300 (Nix build user range) next_uid() { awk -F: '{ if ($3 >= 300 && $3 > max) max = $3 } END { print (max ? max + 1 : 300) }' "$PASSWD_FILE" } # ── Argument parsing ──────────────────────────────────────────────────────── if [ "$#" -lt 2 ]; then usage fi ACTION="" USERNAME="" case "$1" in -addUser) ACTION="add" USERNAME="$2" shift 2 ;; -deleteUser) ACTION="delete" USERNAME="$2" shift 2 ;; -h|--help|-help) usage ;; *) errcho "sysadminctl: unknown option '$1'" usage ;; esac # Validate username (alphanumeric, underscore, dash, dot; max 256 chars) case "$USERNAME" in "") errcho "sysadminctl: username must not be empty" exit 1 ;; *[!a-zA-Z0-9_.-]*) errcho "sysadminctl: invalid username '$USERNAME' (allowed: a-z A-Z 0-9 _ . -)" exit 1 ;; esac if [ "${#USERNAME}" -gt 256 ]; then errcho "sysadminctl: username too long (max 256 characters)" exit 1 fi # ── addUser ───────────────────────────────────────────────────────────────── if [ "$ACTION" = "add" ]; then UID_VAL="" GID_VAL="" HOME_DIR="/var/empty" SHELL="/usr/bin/false" GECOS="" while [ "$#" -gt 0 ]; do case "$1" in -UID) [ "$#" -ge 2 ] || { errcho "sysadminctl: -UID requires an argument"; exit 1; } UID_VAL="$2" shift 2 ;; -GID) [ "$#" -ge 2 ] || { errcho "sysadminctl: -GID requires an argument"; exit 1; } GID_VAL="$2" shift 2 ;; -home) [ "$#" -ge 2 ] || { errcho "sysadminctl: -home requires an argument"; exit 1; } HOME_DIR="$2" shift 2 ;; -shell) [ "$#" -ge 2 ] || { errcho "sysadminctl: -shell requires an argument"; exit 1; } SHELL="$2" shift 2 ;; -fullName) [ "$#" -ge 2 ] || { errcho "sysadminctl: -fullName requires an argument"; exit 1; } GECOS="$2" shift 2 ;; -password) # Ignored — we always set the password field to 'x' (locked) [ "$#" -ge 2 ] || { errcho "sysadminctl: -password requires an argument"; exit 1; } shift 2 ;; -adminUser|-admin) # Ignored — no authentication needed in a Darling prefix [ "$#" -ge 2 ] || { errcho "sysadminctl: $1 requires an argument"; exit 1; } shift 2 ;; -adminPassword) # Ignored — no authentication needed in a Darling prefix [ "$#" -ge 2 ] || { errcho "sysadminctl: -adminPassword requires an argument"; exit 1; } shift 2 ;; -roleAccount) # Boolean flag used by newer Nix installers — no argument shift ;; *) errcho "sysadminctl: unknown option '$1'" usage ;; esac done # Idempotent: if user already exists, succeed silently if user_exists "$USERNAME"; then echo "User '$USERNAME' already exists" exit 0 fi # Assign UID if not specified if [ -z "$UID_VAL" ]; then UID_VAL=$(next_uid) fi # Validate UID is numeric case "$UID_VAL" in *[!0-9]*) errcho "sysadminctl: UID must be numeric, got '$UID_VAL'" exit 1 ;; esac # Check for UID conflicts if uid_exists "$UID_VAL"; then existing=$(get_uid_owner "$UID_VAL") errcho "sysadminctl: UID $UID_VAL already in use by user '$existing'" exit 1 fi # Default GID to 0 (wheel/root) if not specified if [ -z "$GID_VAL" ]; then GID_VAL=0 fi # Validate GID is numeric case "$GID_VAL" in *[!0-9]*) errcho "sysadminctl: GID must be numeric, got '$GID_VAL'" exit 1 ;; esac # Ensure /etc/passwd exists if [ ! -f "$PASSWD_FILE" ]; then errcho "sysadminctl: $PASSWD_FILE does not exist" exit 1 fi # Ensure the home directory's parent exists (but don't create the home # directory itself — Nix build users use /var/empty which should already # exist). HOME_PARENT=$(dirname "$HOME_DIR") if [ ! -d "$HOME_PARENT" ] && [ "$HOME_PARENT" != "/" ]; then mkdir -p "$HOME_PARENT" 2>/dev/null || true fi # Create the passwd entry # Format: name:password:UID:GID:GECOS:home:shell echo "${USERNAME}:x:${UID_VAL}:${GID_VAL}:${GECOS}:${HOME_DIR}:${SHELL}" >> "$PASSWD_FILE" echo "Created user '$USERNAME' (UID=$UID_VAL, GID=$GID_VAL, home=$HOME_DIR, shell=$SHELL)" exit 0 fi # ── deleteUser ────────────────────────────────────────────────────────────── if [ "$ACTION" = "delete" ]; then # Consume any remaining flags (some callers pass -secure, etc.) while [ "$#" -gt 0 ]; do case "$1" in -secure|-keepHome|-adminUser|-adminPassword) # Ignored — these are macOS-specific options if [ "$1" = "-adminUser" ] || [ "$1" = "-adminPassword" ]; then shift 2 2>/dev/null || shift else shift fi ;; *) errcho "sysadminctl: unknown option '$1' (ignoring)" shift ;; esac done # Idempotent: if user doesn't exist, succeed silently if ! user_exists "$USERNAME"; then echo "User '$USERNAME' does not exist" exit 0 fi # Remove the user from /etc/passwd TMPFILE=$(mktemp "${PASSWD_FILE}.XXXXXX") grep -v "^${USERNAME}:" "$PASSWD_FILE" > "$TMPFILE" || true mv "$TMPFILE" "$PASSWD_FILE" echo "Deleted user '$USERNAME'" exit 0 fi # Should not reach here errcho "sysadminctl: internal error — unknown action '$ACTION'" exit 1