Service-user creation → skidbladnir's pure-Rust useradd replacement (exposed)
dup-audit P1 (finding #4). nornir shelled Command::new("useradd") --system … (nornir/src/bin/nornir.rs:7481) to mint its dedicated service user. The constellation law is: service/user management lives in skidbladnir (the service layer), and there is NO shell-out (zero-shell charter). skidbladnir already had the 100%-Rust replacement — it just wasn't reachable from a consumer.
What was already there (private)
systemd.rs grows the dedicated service user as part of install_systemd: it allocates the lowest free system id in [100, 1000) and appends matching /etc/group + /etc/passwd + a locked /etc/shadow row — no useradd subprocess. That logic lived in a private ensure_system_user / create_system_user_in, callable only from inside the crate.
What this change does — expose the seam
Three symbols in skidbladnir::systemd are now public so nornir (and holger, dwarfs) consume them instead of hand-rolling useradd + chown:
| symbol | shape | role |
ensure_system_user(user, home) |
-> Result<(u32, u32)> |
the useradd --system --shell /usr/sbin/nologin replacement: idempotent, creates home, chowns it, returns the (uid, gid) |
create_system_user_in(passwd, group, shadow, user, home) |
-> Result<(u32, u32)> |
the file-targeted core (drive an alternate root / test fixture; unit-tested against temp files) |
chown_recursive(path, uid, gid) |
-> Result<()> |
libc chown tree-walk so a consumer can re-own extra state dirs to the fresh user (kills the chown twin too) |
ensure_system_user now returns (uid, gid) (was ()) so a caller can reuse the ids for additional chowns without re-parsing /etc/passwd. A #[cfg(not(unix))] stub mirrors install_systemd (clear linux-only error).
Consumer migration (the twin-kill)
nornir's ensure_system_user (the useradd shell-out), its passwd_ids re-parse and its chown_recursive twin collapse to:
let (uid, gid) = skidbladnir::systemd::ensure_system_user(user, NORNIR_HOME)?;
That deletes the Command::new("useradd") deploy-subprocess entirely. (Landed separately in the nornir repo; skidbladnir's job is to expose the seam — done here.)
Tests
tests/public_api.rs pins the new public surface from a consumer's view (no live /etc, no root): create_system_user_in against a temp fixture root proves the id-allocation + idempotence contract, and ensure_system_user / chown_recursive are asserted reachable as public fn items. The in-crate create_system_user_allocates_consistent_id_and_is_idempotent unit test still covers the allocation math.