this repo has no description
1#!/bin/sh
2# sysadminctl — macOS user management stub for Darling
3#
4# Translates macOS sysadminctl commands to direct /etc/passwd file operations.
5# Only implements the subset needed by the Nix installer:
6#
7# sysadminctl -addUser <name> [-UID <uid>] [-GID <gid>]
8# [-home <dir>] [-shell <shell>] [-fullName <name>]
9# sysadminctl -deleteUser <name>
10#
11# This stub operates on the Darling prefix's /etc/passwd file, NOT the host's.
12#
13# See: plan/07-phase5-daemon.md (Task 5.1)
14
15set -eu
16
17PASSWD_FILE="/etc/passwd"
18
19# ── Helpers ─────────────────────────────────────────────────────────────────
20
21errcho() {
22 echo "$@" >&2
23}
24
25usage() {
26 errcho "Usage: sysadminctl -addUser <username> [-UID <uid>] [-GID <gid>]"
27 errcho " [-home <homedir>] [-shell <shell>] [-fullName <gecos>]"
28 errcho " sysadminctl -deleteUser <username>"
29 errcho ""
30 errcho "Options:"
31 errcho " -addUser <name> Create a new user"
32 errcho " -deleteUser <name> Delete an existing user"
33 errcho " -UID <uid> User ID (numeric)"
34 errcho " -GID <gid> Primary group ID (numeric)"
35 errcho " -home <dir> Home directory (default: /var/empty)"
36 errcho " -shell <shell> Login shell (default: /usr/bin/false)"
37 errcho " -fullName <gecos> Full name / GECOS field"
38 errcho " -password <pass> Password (ignored — always set to locked)"
39 errcho " -adminUser <user> Admin user for authentication (ignored)"
40 errcho " -adminPassword <pw> Admin password (ignored)"
41 exit 64
42}
43
44# user_exists <name> — exit 0 if the user exists in /etc/passwd
45user_exists() {
46 grep -q "^${1}:" "$PASSWD_FILE" 2>/dev/null
47}
48
49# uid_exists <uid> — exit 0 if the UID is already taken
50uid_exists() {
51 awk -F: -v uid="$1" '$3 == uid { found=1; exit } END { exit !found }' "$PASSWD_FILE" 2>/dev/null
52}
53
54# get_uid_owner <uid> — print the username that owns this UID
55get_uid_owner() {
56 awk -F: -v uid="$1" '$3 == uid { print $1; exit }' "$PASSWD_FILE"
57}
58
59# next_uid — find the next available UID >= 300 (Nix build user range)
60next_uid() {
61 awk -F: '{ if ($3 >= 300 && $3 > max) max = $3 } END { print (max ? max + 1 : 300) }' "$PASSWD_FILE"
62}
63
64# ── Argument parsing ────────────────────────────────────────────────────────
65
66if [ "$#" -lt 2 ]; then
67 usage
68fi
69
70ACTION=""
71USERNAME=""
72
73case "$1" in
74 -addUser)
75 ACTION="add"
76 USERNAME="$2"
77 shift 2
78 ;;
79 -deleteUser)
80 ACTION="delete"
81 USERNAME="$2"
82 shift 2
83 ;;
84 -h|--help|-help)
85 usage
86 ;;
87 *)
88 errcho "sysadminctl: unknown option '$1'"
89 usage
90 ;;
91esac
92
93# Validate username (alphanumeric, underscore, dash, dot; max 256 chars)
94case "$USERNAME" in
95 "")
96 errcho "sysadminctl: username must not be empty"
97 exit 1
98 ;;
99 *[!a-zA-Z0-9_.-]*)
100 errcho "sysadminctl: invalid username '$USERNAME' (allowed: a-z A-Z 0-9 _ . -)"
101 exit 1
102 ;;
103esac
104
105if [ "${#USERNAME}" -gt 256 ]; then
106 errcho "sysadminctl: username too long (max 256 characters)"
107 exit 1
108fi
109
110# ── addUser ─────────────────────────────────────────────────────────────────
111
112if [ "$ACTION" = "add" ]; then
113 UID_VAL=""
114 GID_VAL=""
115 HOME_DIR="/var/empty"
116 SHELL="/usr/bin/false"
117 GECOS=""
118
119 while [ "$#" -gt 0 ]; do
120 case "$1" in
121 -UID)
122 [ "$#" -ge 2 ] || { errcho "sysadminctl: -UID requires an argument"; exit 1; }
123 UID_VAL="$2"
124 shift 2
125 ;;
126 -GID)
127 [ "$#" -ge 2 ] || { errcho "sysadminctl: -GID requires an argument"; exit 1; }
128 GID_VAL="$2"
129 shift 2
130 ;;
131 -home)
132 [ "$#" -ge 2 ] || { errcho "sysadminctl: -home requires an argument"; exit 1; }
133 HOME_DIR="$2"
134 shift 2
135 ;;
136 -shell)
137 [ "$#" -ge 2 ] || { errcho "sysadminctl: -shell requires an argument"; exit 1; }
138 SHELL="$2"
139 shift 2
140 ;;
141 -fullName)
142 [ "$#" -ge 2 ] || { errcho "sysadminctl: -fullName requires an argument"; exit 1; }
143 GECOS="$2"
144 shift 2
145 ;;
146 -password)
147 # Ignored — we always set the password field to 'x' (locked)
148 [ "$#" -ge 2 ] || { errcho "sysadminctl: -password requires an argument"; exit 1; }
149 shift 2
150 ;;
151 -adminUser|-admin)
152 # Ignored — no authentication needed in a Darling prefix
153 [ "$#" -ge 2 ] || { errcho "sysadminctl: $1 requires an argument"; exit 1; }
154 shift 2
155 ;;
156 -adminPassword)
157 # Ignored — no authentication needed in a Darling prefix
158 [ "$#" -ge 2 ] || { errcho "sysadminctl: -adminPassword requires an argument"; exit 1; }
159 shift 2
160 ;;
161 -roleAccount)
162 # Boolean flag used by newer Nix installers — no argument
163 shift
164 ;;
165 *)
166 errcho "sysadminctl: unknown option '$1'"
167 usage
168 ;;
169 esac
170 done
171
172 # Idempotent: if user already exists, succeed silently
173 if user_exists "$USERNAME"; then
174 echo "User '$USERNAME' already exists"
175 exit 0
176 fi
177
178 # Assign UID if not specified
179 if [ -z "$UID_VAL" ]; then
180 UID_VAL=$(next_uid)
181 fi
182
183 # Validate UID is numeric
184 case "$UID_VAL" in
185 *[!0-9]*)
186 errcho "sysadminctl: UID must be numeric, got '$UID_VAL'"
187 exit 1
188 ;;
189 esac
190
191 # Check for UID conflicts
192 if uid_exists "$UID_VAL"; then
193 existing=$(get_uid_owner "$UID_VAL")
194 errcho "sysadminctl: UID $UID_VAL already in use by user '$existing'"
195 exit 1
196 fi
197
198 # Default GID to 0 (wheel/root) if not specified
199 if [ -z "$GID_VAL" ]; then
200 GID_VAL=0
201 fi
202
203 # Validate GID is numeric
204 case "$GID_VAL" in
205 *[!0-9]*)
206 errcho "sysadminctl: GID must be numeric, got '$GID_VAL'"
207 exit 1
208 ;;
209 esac
210
211 # Ensure /etc/passwd exists
212 if [ ! -f "$PASSWD_FILE" ]; then
213 errcho "sysadminctl: $PASSWD_FILE does not exist"
214 exit 1
215 fi
216
217 # Ensure the home directory's parent exists (but don't create the home
218 # directory itself — Nix build users use /var/empty which should already
219 # exist).
220 HOME_PARENT=$(dirname "$HOME_DIR")
221 if [ ! -d "$HOME_PARENT" ] && [ "$HOME_PARENT" != "/" ]; then
222 mkdir -p "$HOME_PARENT" 2>/dev/null || true
223 fi
224
225 # Create the passwd entry
226 # Format: name:password:UID:GID:GECOS:home:shell
227 echo "${USERNAME}:x:${UID_VAL}:${GID_VAL}:${GECOS}:${HOME_DIR}:${SHELL}" >> "$PASSWD_FILE"
228
229 echo "Created user '$USERNAME' (UID=$UID_VAL, GID=$GID_VAL, home=$HOME_DIR, shell=$SHELL)"
230 exit 0
231fi
232
233# ── deleteUser ──────────────────────────────────────────────────────────────
234
235if [ "$ACTION" = "delete" ]; then
236 # Consume any remaining flags (some callers pass -secure, etc.)
237 while [ "$#" -gt 0 ]; do
238 case "$1" in
239 -secure|-keepHome|-adminUser|-adminPassword)
240 # Ignored — these are macOS-specific options
241 if [ "$1" = "-adminUser" ] || [ "$1" = "-adminPassword" ]; then
242 shift 2 2>/dev/null || shift
243 else
244 shift
245 fi
246 ;;
247 *)
248 errcho "sysadminctl: unknown option '$1' (ignoring)"
249 shift
250 ;;
251 esac
252 done
253
254 # Idempotent: if user doesn't exist, succeed silently
255 if ! user_exists "$USERNAME"; then
256 echo "User '$USERNAME' does not exist"
257 exit 0
258 fi
259
260 # Remove the user from /etc/passwd
261 TMPFILE=$(mktemp "${PASSWD_FILE}.XXXXXX")
262 grep -v "^${USERNAME}:" "$PASSWD_FILE" > "$TMPFILE" || true
263 mv "$TMPFILE" "$PASSWD_FILE"
264
265 echo "Deleted user '$USERNAME'"
266 exit 0
267fi
268
269# Should not reach here
270errcho "sysadminctl: internal error — unknown action '$ACTION'"
271exit 1