Skip to content

newusers: create or update accounts in batch - #293

Open
pierre-warnier wants to merge 2 commits into
chgpasswd-toolfrom
newusers-tool
Open

newusers: create or update accounts in batch#293
pierre-warnier wants to merge 2 commits into
chgpasswd-toolfrom
newusers-tool

Conversation

@pierre-warnier

Copy link
Copy Markdown
Collaborator

Adds newusers, the eighteenth tool: one line of seven fields becomes a whole account.

Stacked on #292. Base retargets to main as the stack merges.

For each line it writes the /etc/passwd record, the hashed /etc/shadow record, a group where one is needed, and the home directory with /etc/skel copied in.

All or nothing

Every line is parsed, every name validated, every group resolved and every password hashed before anything is written. A batch with one bad line leaves the system exactly as it was, including for the good lines above it.

The failure mode is nothing happened, not the first two hundred accounts exist and the rest do not. That is the whole reason to reach for this tool instead of a shell loop around useradd.

Home creation is now shared, not copied

useradd's version forces the umask so the requested mode is exact, and hands ownership over through a descriptor opened O_NOFOLLOW — so nobody who can write the parent can swap in a symlink between the mkdir and the chown. Copying that into a second tool is a second chance to get it wrong, silently. It moved to shadow_core::home; useradd calls it and its own tests still pass.

Deliberate divergences

All three established by running the GNU tool, not by reading it:

GNU here
empty password field passed to PAM, which refuses it after creating the account refused before anything is written
pw_gid naming a group that does not exist falls back to the user's own ID, creates no group — the account points at a GID that is not there refused
missing parent directory fails, unlike GNU's own useradd -b created, as useradd -b does

--badname is implemented rather than stubbed: it drops the portability rules on the login name but keeps the checks that stop a name from corrupting the file or being read as an option, which is what makes the flag safe to offer. That is what lets a domain-qualified name from a directory join through.

Verification

cargo test --workspace 828 passed on debian, alpine and fedora
make check fmt, clippy ×3, tests ±pam, plus the unprivileged run — exit 0
e2e deployment suite 297 assertions against a real install
make test-gnu-compat 45 comparisons against the GNU binaries
make test-arm64 22 assertions under emulation

The e2e section re-hashes the stored field with its own salt and compares, so a tool writing a well-formed hash of the wrong string fails; the negative control — the same check against a different password — is asserted to fail. An earlier draft of that assertion ended in || true and was therefore always green, which is worth saying out loud, because it is exactly the failure mode these checks exist to catch.

pierre-warnier and others added 2 commits September 6, 2026 16:35
newusers is the eighteenth tool and the third of the nine that both Debian
and Fedora ship and we did not. One input line of seven fields produces a
whole account: the passwd record, the hashed shadow record, a group, and the
home directory with the skeleton copied in.

Every line is parsed, every name validated, every group resolved and every
password hashed before anything is written. A batch with one bad line leaves
the system exactly as it was, including for the good lines above it -- the
failure mode is "nothing happened", not "the first two hundred accounts
exist and the rest do not". That is the whole reason to reach for this tool
over a shell loop around useradd.

Home directory creation moved to shadow_core::home rather than being copied.
useradd's version forces the umask so the requested mode is exact, and hands
over ownership through a descriptor opened O_NOFOLLOW so nobody who can write
the parent can swap in a symlink between the mkdir and the chown. A second
copy of that is a second chance to get it wrong, and the mistake would be
silent. useradd now calls the shared one and its own tests still pass.

Three deliberate divergences, all established by running the GNU tool:

  - An empty password field is refused before anything is written. Hashing
    "" gives a valid hash that a bare Enter matches. GNU passes the empty
    field to PAM, which refuses it *after* creating the account, leaving a
    half-made account behind -- verified.
  - A pw_gid naming a group that does not exist is refused. GNU falls back to
    the user's own ID and creates no group, so the account is left pointing
    at a GID that is not there, which grpck then reports.
  - A missing parent directory is created, as useradd -b does. GNU's newusers
    fails there, which makes it disagree with GNU's own useradd on the same
    path.

--badname is implemented rather than stubbed: it drops the portability rules
on the login name while keeping the checks that stop a name from corrupting
the file or being read as an option, which is what makes the flag safe to
offer at all. It is what lets a domain-qualified name from a directory join
through.

The e2e suite re-hashes the stored field with its own salt and compares, so a
tool that wrote a well-formed hash of the wrong string would fail; the
negative control, the same check against a different password, is asserted to
fail. An earlier version of that assertion ended in `|| true` and was
therefore always green -- worth saying, because it is the failure mode these
checks exist to catch.

Verified: 828 tests on debian, alpine and fedora; make check clean, including
the unprivileged run; 297 e2e assertions against a real install; 45 GNU
comparisons; 22 arm64 assertions under emulation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant