fix(init): gate userdel/groupdel on --purge to avoid nuking the operator
Every plain `decnet deinit` ran userdel + groupdel unconditionally. In dev the operator may pass `--user $USER --group $USER` to avoid file ownership churn against a source checkout — at which point deinit would cheerfully delete their own login account. Move user/group removal behind --purge, matching the existing behaviour for /var/lib/decnet + /var/log/decnet. Help text updated: --purge now clearly advertises that it also wipes the service user/group, with an explicit warning to only run it when `decnet init` created the account in the first place. Test updated: plain --deinit must NOT invoke userdel/groupdel; --deinit --purge must.
This commit is contained in:
@@ -501,14 +501,18 @@ def register(app: typer.Typer) -> None:
|
|||||||
deinit: bool = typer.Option(
|
deinit: bool = typer.Option(
|
||||||
False, "--deinit",
|
False, "--deinit",
|
||||||
help="Undo a previous init: stop + disable decnet.target, remove "
|
help="Undo a previous init: stop + disable decnet.target, remove "
|
||||||
"unit files, polkit rule, tmpfiles.d entry, /etc/decnet, and "
|
"unit files, polkit rule, tmpfiles.d entry, /etc/decnet. "
|
||||||
"the decnet user/group. Preserves /var/lib/decnet and "
|
"Preserves /var/lib/decnet, /var/log/decnet, and the "
|
||||||
"/var/log/decnet — pass --purge to remove those too.",
|
"service user/group — pass --purge to remove those too.",
|
||||||
),
|
),
|
||||||
purge: bool = typer.Option(
|
purge: bool = typer.Option(
|
||||||
False, "--purge",
|
False, "--purge",
|
||||||
help="With --deinit, also wipe /var/lib/decnet and "
|
help="With --deinit, also wipe /var/lib/decnet, "
|
||||||
"/var/log/decnet. Destructive — operator data is gone.",
|
"/var/log/decnet, AND the service user/group. "
|
||||||
|
"Destructive — operator data is gone, and if --user "
|
||||||
|
"points at your own login account, that account goes "
|
||||||
|
"with it. Only use when the user/group was created by "
|
||||||
|
"`decnet init` in the first place.",
|
||||||
),
|
),
|
||||||
user: str = typer.Option(
|
user: str = typer.Option(
|
||||||
"decnet", "--user",
|
"decnet", "--user",
|
||||||
@@ -664,14 +668,25 @@ def register(app: typer.Typer) -> None:
|
|||||||
f"{pfx / 'var/log/decnet'} (operator data); "
|
f"{pfx / 'var/log/decnet'} (operator data); "
|
||||||
"re-run with --purge to remove.[/]"
|
"re-run with --purge to remove.[/]"
|
||||||
)
|
)
|
||||||
_step(
|
# User / group removal is also gated on --purge. In dev the
|
||||||
f"remove user {user!r}",
|
# operator may have passed their own login user via
|
||||||
lambda: _remove_user(user, dry_run=dry_run),
|
# `--user $USER` to avoid ownership churn; an unconditional
|
||||||
)
|
# `userdel anti` during deinit would nuke their account.
|
||||||
_step(
|
if purge:
|
||||||
f"remove group {group!r}",
|
_step(
|
||||||
lambda: _remove_group(group, dry_run=dry_run),
|
f"remove user {user!r}",
|
||||||
)
|
lambda: _remove_user(user, dry_run=dry_run),
|
||||||
|
)
|
||||||
|
_step(
|
||||||
|
f"remove group {group!r}",
|
||||||
|
lambda: _remove_group(group, dry_run=dry_run),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
console.print(
|
||||||
|
f"[dim]preserved user {user!r} and group {group!r}; "
|
||||||
|
"re-run with --purge to remove (only do this if "
|
||||||
|
"they were created by `decnet init`).[/]"
|
||||||
|
)
|
||||||
console.print("[bold green]DECNET deinit complete.[/]")
|
console.print("[bold green]DECNET deinit complete.[/]")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -387,11 +387,13 @@ def test_deinit_removes_units_polkit_tmpfiles_and_preserves_data(
|
|||||||
assert (prefix / "var/lib/decnet").exists()
|
assert (prefix / "var/lib/decnet").exists()
|
||||||
assert (prefix / "var/log/decnet/events.jsonl").read_text() == "{}\n"
|
assert (prefix / "var/log/decnet/events.jsonl").read_text() == "{}\n"
|
||||||
|
|
||||||
# systemctl disable + daemon-reload + userdel + groupdel were invoked.
|
# systemctl disable + daemon-reload invoked.
|
||||||
assert ["systemctl", "disable", "--now", "decnet.target"] in subprocess_calls
|
assert ["systemctl", "disable", "--now", "decnet.target"] in subprocess_calls
|
||||||
assert ["systemctl", "daemon-reload"] in subprocess_calls
|
assert ["systemctl", "daemon-reload"] in subprocess_calls
|
||||||
assert ["userdel", "decnet"] in subprocess_calls
|
# User / group are PRESERVED without --purge — an operator who
|
||||||
assert ["groupdel", "decnet"] in subprocess_calls
|
# passed --user $USER during dev must not lose their login account.
|
||||||
|
assert ["userdel", "decnet"] not in subprocess_calls
|
||||||
|
assert ["groupdel", "decnet"] not in subprocess_calls
|
||||||
|
|
||||||
|
|
||||||
def test_deinit_purge_wipes_data_dirs(
|
def test_deinit_purge_wipes_data_dirs(
|
||||||
@@ -406,6 +408,9 @@ def test_deinit_purge_wipes_data_dirs(
|
|||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
assert not (prefix / "var/lib/decnet").exists()
|
assert not (prefix / "var/lib/decnet").exists()
|
||||||
assert not (prefix / "var/log/decnet").exists()
|
assert not (prefix / "var/log/decnet").exists()
|
||||||
|
# --purge also removes the service user/group.
|
||||||
|
assert ["userdel", "decnet"] in subprocess_calls
|
||||||
|
assert ["groupdel", "decnet"] in subprocess_calls
|
||||||
|
|
||||||
|
|
||||||
def test_deinit_is_idempotent_on_clean_host(
|
def test_deinit_is_idempotent_on_clean_host(
|
||||||
|
|||||||
Reference in New Issue
Block a user