Fix to gate packaged unit deletion behind config

ⓘ Add a disabled-by-default module option for deleting packaged systemd unit files, while keeping local unit deletion allowed and enforcing the policy in both UI and backend paths.
This commit is contained in:
Ilia Ross
2026-06-23 19:47:47 +02:00
parent 7a68b1b994
commit 41b476c87a
7 changed files with 135 additions and 13 deletions

View File

@@ -5,6 +5,7 @@ visible_tabs=service,timer,socket,path,target,storage,resources,device,user
show_runtime_units=1
default_create_scope=system
manual_vendor_units=1
delete_vendor_units=0
default_linger=1
show_unit_suffixes=0
show_dropin_inventory=1

View File

@@ -5,6 +5,7 @@ visible_tabs=Tabs to show on the index page,15,visible_tabs
show_runtime_units=Show generated and transient units,1,1-Yes,0-No
default_create_scope=Default scope for new units,1,system-System units,user-User units
manual_vendor_units=Include vendor unit files in the manual editor,1,1-Yes,0-No
delete_vendor_units=Allow deleting packaged unit files,1,1-Yes,0-No
default_linger=Enable linger by default for new user units,1,1-Yes,0-No
show_unit_suffixes=Show full unit names with type suffixes,1,1-Yes,0-No
show_dropin_inventory=Show drop-in override inventory,1,1-Yes,0-No

View File

@@ -1087,7 +1087,8 @@ else {
[ 'delete_override',
$text{'edit_deleteoverridenow'} || "Delete Override" ]);
}
elsif ($unit_file_editable && $in{'name'} ne 'webmin.service' &&
elsif (($edit_user_scope || system_unit_file_deletable($u)) &&
$unit_file_editable && $in{'name'} ne 'webmin.service' &&
systemd_can_delete($edit_user_scope, $unituser)) {
push(@delete_buttons, [ 'delete', $text{'delete'} ]);
}

View File

@@ -0,0 +1,9 @@
<header>Allow deleting packaged unit files</header>
<p>Controls whether the module may delete unit files from package-managed
vendor directories such as <tt>/usr/lib/systemd/system</tt> or
<tt>/lib/systemd/system</tt>. This is disabled by default because package
updates may restore or expect those files.</p>
<p>When set to no, only local administrator-created unit files under
<tt>/etc/systemd/system</tt> can be deleted. Packaged units can still be
disabled, masked, or customized with drop-in overrides.</p>

View File

@@ -217,6 +217,7 @@ systemd_title2_user=Edit Systemd User Unit
systemd_title2_view=View Systemd Unit
systemd_title2_view_user=View Systemd User Unit
systemd_egone=Unit no longer exists!
systemd_elocaldelete=Deleting packaged unit files is disabled in the module configuration. Disable, mask or create a drop-in override, or enable packaged unit deletion first.
systemd_header=Systemd unit details
systemd_name=Unit name
systemd_type=Unit type

View File

@@ -32,6 +32,9 @@ $config{"default_create_scope"} = "system"
$config{"default_create_scope"} !~ /^(system|user)$/);
$config{"manual_vendor_units"} = 1
if (!defined($config{"manual_vendor_units"}));
$config{"delete_vendor_units"} = 0
if (!defined($config{"delete_vendor_units"}) ||
$config{"delete_vendor_units"} !~ /^[01]$/);
$config{"default_linger"} = 1
if (!defined($config{"default_linger"}));
$config{"show_unit_suffixes"} = 0
@@ -2853,7 +2856,7 @@ Returns true if a system unit root is the local administrator directory.
sub local_unit_file_root
{
my ($root) = @_;
return $root eq "/etc/systemd/system";
return $root eq get_local_unit_root();
}
=head2 get_system_unit_file_root_candidates()
@@ -2863,7 +2866,7 @@ Returns possible systemd unit directories before existence and symlink checks.
=cut
sub get_system_unit_file_root_candidates
{
return ("/etc/systemd/system",
return (get_local_unit_root(),
"/usr/lib/systemd/system",
"/lib/systemd/system");
}
@@ -3417,18 +3420,23 @@ return reload_user_manager($user);
=head2 delete_system_unit(name)
Delete all traces of some systemd unit.
Delete a permitted systemd unit file.
=cut
sub delete_system_unit
{
my ($name) = @_;
return (0, $text{'systemd_ename'}) if (!valid_unit_name($name));
my $file = get_unit_root($name)."/".$name;
return (0, $text{'systemd_egone'}) if (!-e $file && !-l $file);
unlink_logged($file);
reload_manager();
return (1, "");
foreach my $root (get_system_unit_file_root_candidates()) {
my $file = $root."/".$name;
next if (!-e $file && !-l $file);
return (0, $text{'systemd_elocaldelete'})
if (!system_unit_root_delete_allowed($root));
unlink_logged($file);
reload_manager();
return (1, "");
}
return (0, $text{'systemd_egone'});
}
=head2 get_unit_types()
@@ -3558,6 +3566,48 @@ return 0 if ($unit->{'file'} =~ m{/systemd/(transient|generator)/});
return 1;
}
=head2 system_unit_file_deletable(&unit)
Returns 1 if a system unit record points to a unit file that can be removed
under the current module configuration.
=cut
sub system_unit_file_deletable
{
my ($unit) = @_;
return 0 if (!unit_file_editable($unit));
return system_unit_file_delete_allowed($unit->{'file'}, $unit->{'name'});
}
=head2 system_unit_file_delete_allowed(file, name)
Returns 1 if a system unit path is in a root where deleting unit files is
currently permitted.
=cut
sub system_unit_file_delete_allowed
{
my ($file, $name) = @_;
return 0 if (!$file || !$name || !valid_unit_name($name));
foreach my $root (get_system_unit_file_root_candidates()) {
next if (!system_unit_root_delete_allowed($root));
return 1 if ($file eq $root."/".$name);
}
return 0;
}
=head2 system_unit_root_delete_allowed(root)
Returns 1 if system unit files under this root may be deleted.
=cut
sub system_unit_root_delete_allowed
{
my ($root) = @_;
return (local_unit_file_root($root) ||
$config{'delete_vendor_units'} eq '1') ? 1 : 0;
}
=head2 unit_visible_on_index(&unit)
Returns 1 if a unit should be included on index tabs, honoring the option to
@@ -3701,7 +3751,7 @@ sub get_unit_root
{
my ($name, $packaged) = @_;
# Common system and vendor unit directories.
my $systemd_local_conf = "/etc/systemd/system";
my $systemd_local_conf = get_local_unit_root();
my $systemd_unit_dir1 = "/usr/lib/systemd/system";
my $systemd_unit_dir2 = "/lib/systemd/system";
if ($name) {
@@ -3713,7 +3763,7 @@ if ($name) {
return $p if (-r "$p/$name");
}
}
# Always use /etc/systemd/system for locally created units.
# Always use the local administrator directory for locally created units.
return $systemd_local_conf if (!$packaged && -d $systemd_local_conf);
# Debian prefers /lib/systemd/system for packaged units.
@@ -3729,6 +3779,16 @@ if (-d $systemd_unit_dir1) {
return $systemd_unit_dir2;
}
=head2 get_local_unit_root()
Returns the local administrator directory for system unit files.
=cut
sub get_local_unit_root
{
return "/etc/systemd/system";
}
=head2 get_unit_pid([name])

View File

@@ -80,6 +80,7 @@ our (%access, %config, %in, %text, %gconfig, $remote_user);
systemd_ereadonly => 'runtime unit file',
systemd_ename => 'bad unit name',
systemd_egone => 'unit gone',
systemd_elocaldelete => 'local unit delete only',
systemd_eclash => 'unit clash',
systemd_emountwhat => 'missing mount source',
systemd_emountname => 'bad mount name',
@@ -196,6 +197,27 @@ ok(!unit_file_editable({
unitstate => 'generated',
}),
'generated unit files are read-only');
ok(system_unit_file_deletable({
name => 'local.service',
file => '/etc/systemd/system/local.service',
unitstate => 'enabled',
}),
'local system unit files can be deleted');
ok(!system_unit_file_deletable({
name => 'vendor.service',
file => '/usr/lib/systemd/system/vendor.service',
unitstate => 'enabled',
}),
'packaged system unit files cannot be deleted');
{
local $config{'delete_vendor_units'} = 1;
ok(system_unit_file_deletable({
name => 'vendor.service',
file => '/usr/lib/systemd/system/vendor.service',
unitstate => 'enabled',
}),
'packaged system unit files can be deleted when configured');
}
{
local $config{'show_runtime_units'} = 0;
ok(unit_visible_on_index({
@@ -908,9 +930,18 @@ like(get_unit_root(), qr{^/(etc|usr/lib|lib)/systemd/system$},
{
my $root = "$work/system-root";
make_path($root);
my $vendor_root = "$work/system-vendor-root";
make_path($root, $vendor_root);
my $reloaded = 0;
local *main::get_unit_root = sub { return $root };
local *main::get_local_unit_root = sub { return $root };
local *main::get_unit_root = sub {
my ($name) = @_;
return $vendor_root if (defined($name) && $name eq 'vendor.service');
return $root;
};
local *main::get_system_unit_file_root_candidates = sub {
return ($root, $vendor_root);
};
local *main::reload_manager = sub { $reloaded++ };
local *main::has_command = sub { return };
my ($ok) = create_system_unit(
@@ -949,6 +980,20 @@ like(get_unit_root(), qr{^/(etc|usr/lib|lib)/systemd/system$},
ok(!$ok, 'delete_system_unit rejects already-missing unit');
is($out, $text{'systemd_egone'},
'delete_system_unit reports stale missing unit');
write_test_file("$vendor_root/vendor.service", "packaged");
($ok, $out) = delete_system_unit('vendor.service');
ok(!$ok, 'delete_system_unit rejects packaged system unit files');
is($out, $text{'systemd_elocaldelete'},
'delete_system_unit reports local-only delete policy');
ok(-e "$vendor_root/vendor.service",
'delete_system_unit leaves packaged system unit file alone');
{
local $config{'delete_vendor_units'} = 1;
($ok, $out) = delete_system_unit('vendor.service');
ok($ok, 'delete_system_unit can delete packaged unit files when configured');
ok(!-e "$vendor_root/vendor.service",
'delete_system_unit removes configured packaged unit file');
}
}
{
@@ -1999,6 +2044,8 @@ like($config_source, qr/^default_create_scope=/m,
'module config exposes default create scope');
like($config_source, qr/^manual_vendor_units=/m,
'module config exposes vendor-file manual editor visibility');
like($config_source, qr/^delete_vendor_units=/m,
'module config exposes packaged unit delete policy');
like($config_source, qr/^default_linger=/m,
'module config exposes default linger choice');
like($config_source, qr/^show_dropin_inventory=/m,
@@ -2236,6 +2283,8 @@ like($edit_source, qr/readonly='readonly'/,
'edit page shows runtime-managed unit files as read-only');
like($edit_source, qr/unit_file_editable/,
'edit page hides save and delete for runtime-managed unit files');
like($edit_source, qr/system_unit_file_deletable/,
'edit page hides delete for packaged system unit files');
like($edit_source, qr/edit_depsnow/,
'edit page includes dependency inspect action');
like($edit_source, qr/edit_propsnow/,