Skip to content

chgpasswd: set group passwords in batch - #292

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

chgpasswd: set group passwords in batch#292
pierre-warnier wants to merge 2 commits into
sg-toolfrom
chgpasswd-tool

Conversation

@pierre-warnier

Copy link
Copy Markdown
Collaborator

Adds chgpasswd, the seventeenth tool: chpasswd for groups.

Stacked on #291 — the diff against that branch is chgpasswd alone. Base retargets to main once #291 merges.

It reads group:password lines from stdin and applies them, following the rule that makes a batch tool safe to run unattended: every line is parsed, every group resolved and every password hashed before any file is written. A batch naming one group that does not exist changes nothing, rather than applying the lines above it and stopping. The two account files are then written in one transaction.

Where the hash goes

Established by running the GNU tool, not by reading it:

hash /etc/group
/etc/gshadow present goes to gshadow keeps x
/etc/gshadow absent goes to /etc/group holds the hash

/etc/group is world-readable, which is the whole reason for the split. And no gshadow file is created where none exists: conjuring one up would change how every other tool on the host reads group passwords.

Deliberate divergences

Three, all refusals:

  • -c NONE. GNU honours it and stores the password as clear text in /etc/gshadow. A readable group password is worth no more than none at all. -e already writes a field verbatim when that is genuinely what is wanted, and the error message says so — otherwise an operator just tries -c MD5 next.
  • -m, -c MD5, -c DES — as chpasswd already refuses them.
  • An empty password in plaintext mode. Hashing "" produces a valid hash that a bare Enter matches: a group anyone can enter, not a group with no password. -e with an empty field remains the way to clear it.

A field containing a colon is refused, since it would split the gshadow line and corrupt the file. The GNU tool refuses it too and likewise leaves the file untouched — agreement, not divergence.

Verification

cargo test --workspace 798 passed on debian, alpine and fedora
make check fmt, clippy ×3, tests with and without pam — exit 0
e2e deployment suite 273 assertions against a real install
make test-gnu-compat 44 comparisons against the GNU binaries

The e2e section checks the tools against each other rather than only against file shapes: the password chgpasswd sets is the one sg then accepts from a non-member, and a wrong one is refused. A hash written in a format nothing on the host can verify would pass a grep assertion and fail that one.

chgpasswd is chpasswd's counterpart for groups: it reads group:password
lines from stdin and applies them. It is the seventeenth tool and the second
of the nine that both Debian and Fedora ship and we did not.

It follows chpasswd's rule, which is what makes a batch tool safe to run
unattended: every line is parsed, every group resolved and every password
hashed before any file is written, so a batch naming one group that does not
exist changes nothing rather than applying the lines above it and stopping.
The two account files are then written in one transaction.

Where the hash goes was established by running the GNU tool, not by reading
it. With /etc/gshadow present the hash belongs there and /etc/group keeps the
`x` placeholder -- /etc/group is world-readable. Without it the hash goes
into /etc/group, and no gshadow file is created: conjuring one up would
change how every other tool on the host reads group passwords.

Three deliberate divergences, all refusals:

  - `-c NONE`, which GNU honours by storing the password as clear text in
    /etc/gshadow. A readable group password is worth no more than none at
    all, and `-e` already writes a field verbatim when that is what is
    wanted. The message says so, or an operator would just try `-c MD5`.
  - `-m` and `-c MD5`/`-c DES`, as chpasswd already refuses them.
  - An empty password in plaintext mode: hashing "" yields a valid hash that
    a bare Enter matches, which is a group anyone can enter rather than a
    group with no password. `-e ''` remains the way to clear the field.

A field containing a colon is refused, since it would split the gshadow line
and corrupt the file. The GNU tool refuses it too and likewise leaves the
file untouched, so this is agreement, not divergence.

The e2e suite checks the tools against each other rather than only against
the files: the password chgpasswd sets is the one sg then accepts from a
non-member, and a wrong one is refused. A hash written in a format nothing
can verify would pass a file-shape assertion and fail this one.

Verified: 798 tests on debian, alpine and fedora; make check clean; 273 e2e
assertions against a real install; 44 GNU comparisons.
Sixteen of the chgpasswd tests assumed root. They passed locally because
every container in docker-compose.yml runs as root, and failed in the one CI
job that does not.

Worse than the two that failed outright were the ones that passed for the
wrong reason: a test asserting exit 1 got exit 1 from "permission denied"
rather than from the condition it meant to check, so it was green while
testing nothing. Only the two expecting exit 0 gave the game away.

The guard itself is what chpasswd already does -- skip_unless_root() on every
test that writes an account file. The two that only exercise argument parsing
keep running everywhere, since clap answers before the root check.

The gap that let this through was local, so the fix is too: `make
test-unprivileged` re-runs the already-built test binaries as an ordinary
user, and `make check` now does it after the root run. Removing one guard
again makes it fail with "Permission denied", which is how it was verified.

SHADOW_TEST_REQUIRE_ROOT is cleared for that run on purpose: it exists to
turn a skip into a failure when the suite *is* root, which is the opposite
case.
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