Files
Pierre Warnier 7e28940874 expiry, and usermod's subordinate-id options (#299)
* usermod: add and remove subordinate id ranges

GNU usermod has -v/--add-subuids, -V/--del-subuids, -w/--add-subgids and
-W/--del-subgids; ours had none of them, which came to light while writing
the tests for newuidmap. The range helpers live in shadow_core::subid so
whatever next touches /etc/subuid reads the same file the same way.

The behaviour was established by running the GNU tool: a range the user
already holds is not added twice; a removal trims an entry it overlaps,
splits one it cuts through the middle of, deletes one it covers, and ignores
a range never held; nothing checks other users' entries, since an
administrator may share a range on purpose; and an invalid range --
reversed, not two numbers, above the 32-bit ids the kernel maps -- is exit 3
with the GNU wording, before any file is written. Entries are keyed by the
login name given on the command line and are not renamed by -l, as GNU's
are not: a range is a grant to a name.

-V is GNU's letter for the deletion, so clap's own -V for --version had to
go; --version stays.

* expiry: check and enforce password expiration

expiry is the twenty-eighth tool and the last the Scope table lists as in
scope. It judges the caller's own /etc/shadow line as login would: silent
when all is well, a warning inside warn_days, a forced change through PAM
when the password has expired, a refusal when the account has. Shell
profiles run it so a session that did not come through login -- a display
manager, an ssh key -- still meets the aging policy.

The rules are in shadow_core::shadow::Aging, one function over the aging
fields with checked arithmetic, since every input is read from a file anyone
who can write /etc/shadow chooses. Its order is login's: account expiry
first, then a last change of 0 or an aged-out password, then the warning
window, with a maximum age of 10000 or more meaning never as chage -l prints
it. A locked password is a policy of its own and is not reported.

The GNU tool is setgid shadow -- enough to read the file, no more -- and the
change goes through PAM as the caller. The per-tool install ships it the
same way, a new install class. The single setuid binary keeps euid 0 for it,
having nothing narrower to offer. Under --prefix it reports and never
changes anything, since a PAM change would act on this system's account of
the same name; that is also what lets the required-change path be asserted
in the integration tests without a prompt, while the e2e suite drives the
prompt itself on a real user under a pty and checks the hash changed.

groupmems is not coming. Debian 13 and Fedora do not ship it, and on Ubuntu
24.04 every action fails against PAM's default for want of a
/etc/pam.d/groupmems: a tool one distribution ships and none can use. The
Scope table says so.

Verified: make check clean with the unprivileged run; 935 tests on alpine
and 950 on fedora with PAM; 410 e2e assertions against a real install; 59
GNU comparisons; 35 arm64 assertions under emulation.

* shadow-core: stop asking nscd to invalidate a shadow cache it never had

Every passwd, chage and chpasswd ended with `nscd -i shadow`, and on a host
running nscd that printed "nscd: 'shadow' is not a known database" on the
user's terminal each time. nscd caches passwd, group, hosts, services and
netgroup; the passwd entries it holds carry the `x` placeholder, so a
password change leaves nothing of nscd's stale. Only the databases nscd
knows are passed to it now. sss_cache still gets -U for a shadow change,
which is right: sssd does cache what a password change affects.
2026-09-07 15:47:43 +02:00

73 lines
2.2 KiB
Rust

// This file is part of the shadow-rs package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//! Integration tests for every shadow-rs utility.
//!
//! One test binary, not fourteen. Each of the fourteen used to link all
//! fourteen tool crates and carry its own copy of the helpers through a
//! `#[path = "../common/mod.rs"]` include; this is the layout the rest of
//! uutils uses. The `#[path]` attributes are needed only because the
//! directory name carries a hyphen, which is not a module name.
// The print macros are denied for the tools, where a panic on a closed stream
// would abandon a half-written account file. In a test a panic is a failed
// test, which is what one wants, and printing why a test skipped is useful.
#![allow(clippy::print_stdout, clippy::print_stderr)]
mod common;
#[path = "by-util/test_chage.rs"]
mod test_chage;
#[path = "by-util/test_chfn.rs"]
mod test_chfn;
#[path = "by-util/test_chgpasswd.rs"]
mod test_chgpasswd;
#[path = "by-util/test_chpasswd.rs"]
mod test_chpasswd;
#[path = "by-util/test_chsh.rs"]
mod test_chsh;
#[path = "by-util/test_expiry.rs"]
mod test_expiry;
#[path = "by-util/test_fuzz_corpus.rs"]
mod test_fuzz_corpus;
#[path = "by-util/test_gpasswd.rs"]
mod test_gpasswd;
#[path = "by-util/test_groupadd.rs"]
mod test_groupadd;
#[path = "by-util/test_groupdel.rs"]
mod test_groupdel;
#[path = "by-util/test_groupmod.rs"]
mod test_groupmod;
#[path = "by-util/test_grpck.rs"]
mod test_grpck;
#[path = "by-util/test_login.rs"]
mod test_login;
#[path = "by-util/test_multicall.rs"]
mod test_multicall;
#[path = "by-util/test_newgrp.rs"]
mod test_newgrp;
#[path = "by-util/test_newuidmap.rs"]
mod test_newuidmap;
#[path = "by-util/test_newusers.rs"]
mod test_newusers;
#[path = "by-util/test_passwd.rs"]
mod test_passwd;
#[path = "by-util/test_pwck.rs"]
mod test_pwck;
#[path = "by-util/test_pwconv.rs"]
mod test_pwconv;
#[path = "by-util/test_sg.rs"]
mod test_sg;
#[path = "by-util/test_useradd.rs"]
mod test_useradd;
#[path = "by-util/test_userdel.rs"]
mod test_userdel;
#[path = "by-util/test_usermod.rs"]
mod test_usermod;
#[path = "by-util/test_usermod_subid.rs"]
mod test_usermod_subid;
#[path = "by-util/test_vipw.rs"]
mod test_vipw;