No description
  • Python 92.3%
  • Makefile 4.4%
  • Shell 2.3%
  • Dockerfile 1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
JButler 6447816ec0
Some checks failed
CI / syntax / lint / tests / staged install (push) Failing after 3s
Add CI, licence, and repo hygiene; make gates runnable as one command
The repo had no CI and no licence file, and the license metadata
contradicted itself: debian/copyright and the RPM spec said MIT-0 while
embedding the plain-MIT text with the attribution condition intact.

- .forgejo/workflows/ci.yml: one job, six steps, each calling a Makefile
  target, so `make check` locally and CI run identical commands.
- .forgejo/workflows/packages.yml: builds the .deb in ubuntu:24.04 and the
  .rpm in rockylinux:9 as job containers (no docker-in-docker), inspects
  each, uploads both as artifacts. Gated on a version/tag check first.
- Makefile: add check, syntax, lint, shellcheck, version-check, stage-check.
  A gate fails with an install hint when its tool is missing rather than
  passing quietly. dist-deb and dist-rpm now assert the artifact exists
  instead of swallowing the move with `|| true`.
- tools/check-version.py: the version is written four times (Makefile,
  useradmin.py __version__, debian/changelog, spec) with nothing deriving one
  from another. It globs the built .deb by the Makefile's value, so drift
  silently leaves the package outside dist/.
- LICENSE: MIT-0, and the attribution clause removed from debian/copyright to
  match the label the spec and changelog already used.
- ruff.toml: E4/E5/E7/E9/F/B. E741 and B904 are ignored with their sites
  listed - pre-existing debt, documented in CONTRIBUTING.md. F841 stays on:
  the one violation (a dead backend.groups() read in cli_list) is silenced
  inline with a TODO rather than by switching the rule off tree-wide.
- .gitignore/.gitattributes: ignore build, cache and local agent state; force
  LF, since a CRLF useradmin.py breaks the shebang in the shipped binary.
  .pi/remote-pi/config.json holds a machine-specific agent name, so it is
  untracked.
- packaging/rhel/useradmin.spec: ship LICENSE as %license. Correct the
  changelog dates (debian/changelog had "Sat, 05 Oct 2026", which is a Monday;
  the spec said 2024).

Verified: make check green; RPM built on rockylinux:9 and DEB on ubuntu:24.04
with the exact CI step bodies (the DEB needed build-essential added to
dpkg-checkbuilddeps, which is now in the workflow).
2026-10-05 16:32:14 -04:00
.forgejo/workflows Add CI, licence, and repo hygiene; make gates runnable as one command 2026-10-05 16:32:14 -04:00
debian Add CI, licence, and repo hygiene; make gates runnable as one command 2026-10-05 16:32:14 -04:00
packaging/rhel Add CI, licence, and repo hygiene; make gates runnable as one command 2026-10-05 16:32:14 -04:00
tests feat: implement useradmin v3 per DESIGN-useradmin-v3.md 2026-10-05 14:17:31 -04:00
tools Add CI, licence, and repo hygiene; make gates runnable as one command 2026-10-05 16:32:14 -04:00
.gitattributes Add CI, licence, and repo hygiene; make gates runnable as one command 2026-10-05 16:32:14 -04:00
.gitignore Add CI, licence, and repo hygiene; make gates runnable as one command 2026-10-05 16:32:14 -04:00
CONTRIBUTING.md Add CI, licence, and repo hygiene; make gates runnable as one command 2026-10-05 16:32:14 -04:00
DESIGN-useradmin-v3.md planned 2026-10-05 13:48:02 -04:00
LICENSE Add CI, licence, and repo hygiene; make gates runnable as one command 2026-10-05 16:32:14 -04:00
Makefile Add CI, licence, and repo hygiene; make gates runnable as one command 2026-10-05 16:32:14 -04:00
README.md Add CI, licence, and repo hygiene; make gates runnable as one command 2026-10-05 16:32:14 -04:00
ruff.toml Add CI, licence, and repo hygiene; make gates runnable as one command 2026-10-05 16:32:14 -04:00
useradmin.desktop feat: implement useradmin v3 per DESIGN-useradmin-v3.md 2026-10-05 14:17:31 -04:00
useradmin.py Add CI, licence, and repo hygiene; make gates runnable as one command 2026-10-05 16:32:14 -04:00
useradmin.svg feat: implement useradmin v3 per DESIGN-useradmin-v3.md 2026-10-05 14:17:31 -04:00

useradmin

A graphical tool for managing local user accounts and groups on a single Linux machine.

Purpose

Designed for a technical lead who manages ~12 local accounts on an airgapped Linux box and is uncomfortable in a terminal. The tool creates, modifies, and deletes accounts; sets passwords; manages group membership; and handles expiry.

All mutations use shadow-utils (useradd, usermod, userdel, groupadd, groupdel, gpasswd, chage, passwd). State is read through getent. Passwords are set via passwd under a pty so PAM policy is enforced.

Target environments

  • Ubuntu 22.04+, Rocky 8/9, RHEL 8/9
  • Python 3.6+ (stdlib only — no PyPI packages)
  • Tkinter + ttk (python3-tk on Ubuntu, python3-tkinter on RHEL/Rocky)

Installation

From RPM (RHEL/Rocky)

sudo dnf install useradmin-*.rpm

From DEB (Ubuntu)

sudo apt install ./useradmin_*.deb

Manual install

sudo cp useradmin.py /usr/bin/useradmin
sudo chmod +x /usr/bin/useradmin
sudo cp useradmin.desktop /usr/share/applications/
sudo cp useradmin.svg /usr/share/icons/hicolor/scalable/apps/

Usage

GUI mode

sudo useradmin

Or launch from the desktop menu (System → User Admin).

CLI mode

sudo useradmin --no-gui list

Dry-run mode

sudo useradmin --dry-run

Shows the commands that would be executed without running them.

Features

  • Account list: table with name, UID, full name, status, expiry; sortable and filterable
  • Create account: username, full name, primary group, supplementary groups, password, expiry
  • Modify account: full name, shell, groups, password, expiry, aging
  • Delete account: with confirmation and optional home directory removal
  • Group management: create/delete groups, add/remove members
  • Password management: set password via passwd under pty (PAM-enforced)
  • Dry-run mode: preview commands before executing

Safety

  • Verify after apply: every mutation re-reads state and shows before/after
  • Input validation: usernames, shells, GECOS fields validated before execution
  • No blank-password unlock: refuses to unlock accounts with no password (W3 fix)
  • No fail-open reads: unreadable shadow → status "unknown", never a guess (W4 fix)
  • Flags preserved on relaunch: --dry-run survives sudo relaunch (W1 fix)
  • GECOS round-trip: commas in "Doe, John" preserved, not mangled (W9 fix)
  • Aging from shadow fields: uses shadow indices, not chage -l text (W5 fix)
  • All columns sortable: column IDs and sort keys defined together (W6 fix)
  • argparse for CLI: --help prints help, doesn't launch GUI (W17 fix)

Logging

All mutations logged to syslog (facility LOCAL6). Passwords are never logged.

Configuration

/etc/useradmin/config.json:

{
  "managed_groups": ["devs", "ops"],
  "default_shell": "/bin/bash",
  "create_home": true,
  "home_base": "/home",
  "min_uid": 1000
}
  • managed_groups: if set, only accounts in these groups are shown
  • default_shell: default for new accounts
  • min_uid: hide system accounts below this UID

Testing

All gates

make check

Runs syntax, lint, shellcheck, version consistency, unit tests and a staged install - the same steps CI runs. See CONTRIBUTING.md for tool setup and the release flow.

Container smoke tests

./tests/smoke-test.sh

Builds Docker images for Ubuntu 22.04, Ubuntu 24.04, Rocky 8, Rocky 9, and runs smoke tests in each.

Security

  • GUI runs as root (sudo useradmin)
  • All reads via getent (NSS-correct, no direct file manipulation)
  • All mutations via shadow-utils (no shell=True, no option injection)
  • Passwords only exist transiently in memory, never in argv or logs
  • All subprocess calls have timeouts (10s default, 30s for useradd -m)

Wayland

On Wayland, root GUI cannot draw on the user's session. The desktop icon uses sudo -A with SUDO_ASKPASS to work around this. Alternatively, launch from a terminal with sudo useradmin.

License

MIT-0 (MIT No Attribution) - see LICENSE. Use at your own risk; this tool runs as root and edits account databases.