From 0290ec16a547ecbcb856f49198ea2edb4a0b760c Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Tue, 23 Jun 2026 23:24:38 +0200 Subject: [PATCH] Fix to make packaged unit edits opt-in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⓘ Default packaged unit files to read-only, keep drop-ins as the safe override path, hide boot controls for protected base units, and reject [Install] sections in drop-in overrides. --- systemd/config | 1 + systemd/config.info | 1 + systemd/edit_manual.cgi | 7 +- systemd/edit_unit.cgi | 33 +++++--- systemd/help/config_edit_vendor_units.html | 10 +++ systemd/lang/en | 3 + systemd/save_unit.cgi | 6 +- systemd/systemd-lib.pl | 96 +++++++++++++++++++++- systemd/t/run-tests.t | 94 +++++++++++++++++++++ 9 files changed, 235 insertions(+), 16 deletions(-) create mode 100644 systemd/help/config_edit_vendor_units.html diff --git a/systemd/config b/systemd/config index c09206786..47e04856b 100644 --- a/systemd/config +++ b/systemd/config @@ -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 +edit_vendor_units=0 delete_vendor_units=0 default_linger=1 show_unit_suffixes=0 diff --git a/systemd/config.info b/systemd/config.info index 3a29cad55..64662e113 100644 --- a/systemd/config.info +++ b/systemd/config.info @@ -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 +edit_vendor_units=Allow editing packaged unit files,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 diff --git a/systemd/edit_manual.cgi b/systemd/edit_manual.cgi index f98facc04..9c29143f2 100755 --- a/systemd/edit_manual.cgi +++ b/systemd/edit_manual.cgi @@ -23,6 +23,7 @@ my $info = $allowed{$in{'file'}} || $files[0]; my $file = $info->{'file'}; my $data = read_manual_unit_file($info); defined($data) || error($text{'manual_eread'}); +my $file_writable = manual_unit_file_writable($info); ui_print_header(undef, $text{'manual_title'}, ""); my $desc = $info->{'scope'} eq 'user' ? @@ -43,9 +44,11 @@ print ui_form_end(); print ui_form_start("save_manual.cgi", "form-data"); print ui_hidden("file", $file); print ui_table_start(undef, undef, 2); -print ui_table_row(undef, ui_textarea("data", $data, 35, 120), 2); +print ui_table_row(undef, ui_textarea("data", $data, 35, 120, undef, + undef, $file_writable ? undef : + "readonly='readonly'"), 2); print ui_table_end(); -print ui_form_end([ [ "save", $text{'save'} ] ]); +print ui_form_end($file_writable ? [ [ "save", $text{'save'} ] ] : undef); ui_print_footer("index.cgi", $text{'index_return'}); diff --git a/systemd/edit_unit.cgi b/systemd/edit_unit.cgi index e3806cb61..c71059133 100755 --- a/systemd/edit_unit.cgi +++ b/systemd/edit_unit.cgi @@ -143,7 +143,9 @@ my (@units, @unittypes, @types, @killmodes, @restarts, @protects); my (%creatable_types); my $default_unittype = 'service'; my $unit_file_editable = 0; +my $unit_file_writable = 0; my $can_save_unit = 0; +my $can_write_current_file = 0; my $remote_uinfo = get_user_details($remote_user); # New units start with an empty record. Existing units are looked up from the @@ -186,12 +188,17 @@ else { $dropin_file = $dropin_info->{'file'}; } $unit_file_editable = unit_file_editable($u); + $unit_file_writable = $edit_user_scope ? $unit_file_editable : + system_unit_file_writable($u); $can_save_unit = $edit_dropin ? systemd_can_dropin($edit_user_scope, $unituser) : systemd_can_edit($edit_user_scope, $unituser); + $can_write_current_file = + ($edit_dropin ? $unit_file_editable : $unit_file_writable) && + $can_save_unit ? 1 : 0; # Runtime-managed units are inspect-only, so title them as views. - my $title_key = $unit_file_editable && $can_save_unit ? + my $title_key = $can_write_current_file ? ($edit_user_scope ? 'systemd_title2_user' : 'systemd_title2') : ($edit_user_scope ? 'systemd_title2_view_user' : 'systemd_title2_view'); @@ -985,8 +992,8 @@ else { } print ui_table_row(hlink($text{'systemd_conf'}, "systemd_conf"), ui_textarea("data", $conf, 20, 80, undef, - undef, $unit_file_editable && - $can_save_unit ? undef : + undef, $can_write_current_file ? + undef : "readonly='readonly'")); if ($edit_user_scope) { @@ -1010,6 +1017,7 @@ else { # Only file-backed installable units can have their startup state changed. if (boot_state_changeable($u->{'unitstate'}, $u->{'name'}) && + ($edit_user_scope || $unit_file_writable) && systemd_can_boot($edit_user_scope, $unituser)) { print ui_table_row(hlink($text{'systemd_boot'}, "systemd_boot"), ui_yesno_radio("boot", $u->{'boot'})); @@ -1036,7 +1044,7 @@ if ($in{'new'}) { else { # Keep save, override, runtime and inspection actions in nearby clusters; # destructive actions stay isolated on the far side of the button row. - my @save_buttons = $unit_file_editable && $can_save_unit ? + my @save_buttons = $can_write_current_file ? ( [ undef, $text{'save'} ] ) : ( ); my @control_buttons; my @inspect_buttons = systemd_can_inspect($edit_user_scope, $unituser) ? @@ -1066,18 +1074,21 @@ else { my @override_buttons; if ($edit_dropin) { + my $base_unit_editable = + $unit_file_writable && + systemd_can_edit($edit_user_scope, $unituser); + my $stock_text = $base_unit_editable + ? $text{'edit_stockunitnow'} + : $text{'edit_view_stockunitnow'}; push(@override_buttons, - [ 'stock_unit', - $text{'edit_stockunitnow'} || "Stock Unit" ]); + [ 'stock_unit', $stock_text ]); } elsif ($unit_file_editable && systemd_can_dropin($edit_user_scope, $unituser)) { my $override_text = dropin_exists($edit_user_scope, $unituser, $in{'name'}) ? - ($text{'edit_editoverridenow'} || - "Edit Override") : - ($text{'edit_overridenow'} || - "Create Override"); + $text{'edit_editoverridenow'} : + $text{'edit_overridenow'}; push(@override_buttons, [ 'override', $override_text ]); } my @delete_buttons; @@ -1085,7 +1096,7 @@ else { systemd_can_dropin($edit_user_scope, $unituser)) { push(@delete_buttons, [ 'delete_override', - $text{'edit_deleteoverridenow'} || "Delete Override" ]); + $text{'edit_deleteoverridenow'} ]); } elsif (($edit_user_scope || system_unit_file_deletable($u)) && $unit_file_editable && $in{'name'} ne 'webmin.service' && diff --git a/systemd/help/config_edit_vendor_units.html b/systemd/help/config_edit_vendor_units.html new file mode 100644 index 000000000..502aebf54 --- /dev/null +++ b/systemd/help/config_edit_vendor_units.html @@ -0,0 +1,10 @@ +
Allow editing packaged unit files
+

Controls whether the module may directly save changes to package-managed +unit files from vendor directories such as /usr/lib/systemd/system +or /lib/systemd/system. This is disabled by default because package +updates may overwrite or depend on those files.

+ +

When set to no, packaged unit files can still be inspected in the manual +editor when vendor files are included, but they are read-only. Use drop-in +overrides or local units under /etc/systemd/system for normal +customization.

diff --git a/systemd/lang/en b/systemd/lang/en index 9dc88ad83..4e6622129 100644 --- a/systemd/lang/en +++ b/systemd/lang/en @@ -51,6 +51,7 @@ edit_logsnow=Logs edit_overridenow=Create Override edit_editoverridenow=Edit Override edit_stockunitnow=Edit Base Unit +edit_view_stockunitnow=View Base Unit edit_deleteoverridenow=Delete Override ss_ecannot=You are not allowed to start or stop systemd units acl_section_users=User unit owner restrictions @@ -217,6 +218,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_evendoredit=Editing packaged unit files is disabled in the module configuration. Create a drop-in override, or enable packaged unit editing first. 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 @@ -314,6 +316,7 @@ systemd_euserhome=Failed to create systemd user unit directory systemd_euserunitfile=Unsafe or invalid systemd user unit file systemd_euserunitdir=Unsafe or wrongly-owned systemd user unit directory systemd_edropinfile=Unsafe or invalid systemd drop-in override file +systemd_edropininstall=Drop-in overrides cannot contain an [Install] section. Use enable or disable actions to change startup state. systemd_ereadonly=This systemd unit file is managed at runtime and cannot be edited directly. systemd_eloginctl=The loginctl command is not available on your system systemd_esystemctl=The systemctl command is not available on your system diff --git a/systemd/save_unit.cgi b/systemd/save_unit.cgi index 438a10a72..a7d270fd6 100755 --- a/systemd/save_unit.cgi +++ b/systemd/save_unit.cgi @@ -697,6 +697,9 @@ else { if (!unit_file_editable($u)) { error($text{'systemd_ereadonly'}); } + if (!$edit_dropin && !$user_scope && !system_unit_file_writable($u)) { + error($text{'systemd_evendoredit'}); + } $in{'data'} =~ /\S/ || error($text{'systemd_econf'}); $in{'data'} =~ s/\r//g; my $save_data = $edit_dropin ? @@ -788,7 +791,8 @@ else { # Apply startup state changes after saving the config. if (defined($in{'boot'}) && - boot_state_changeable($u->{'unitstate'}, $u->{'name'})) { + boot_state_changeable($u->{'unitstate'}, $u->{'name'}) && + ($user_scope || system_unit_file_writable($u))) { systemd_can_boot($user_scope, $unituser) || systemd_acl_error('pboot'); if ($user_scope) { diff --git a/systemd/systemd-lib.pl b/systemd/systemd-lib.pl index 737edb1db..86c15ec1a 100644 --- a/systemd/systemd-lib.pl +++ b/systemd/systemd-lib.pl @@ -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{"edit_vendor_units"} = 0 + if (!defined($config{"edit_vendor_units"}) || + $config{"edit_vendor_units"} !~ /^[01]$/); $config{"delete_vendor_units"} = 0 if (!defined($config{"delete_vendor_units"}) || $config{"delete_vendor_units"} !~ /^[01]$/); @@ -1756,10 +1759,12 @@ systemd-analyze cannot reliably load their temporary copies by name. sub verify_dropin_data { my ($file, $unit_data, $dropin_data, $user_scope, $unitstate, $user) = @_; -my $analyze = has_command("systemd-analyze"); -return (1, undef) if (!$analyze); return (0, $text{'systemd_econf'}) if (!defined($unit_data) || !defined($dropin_data)); +return (0, $text{'systemd_edropininstall'}) + if (dropin_has_install_section($dropin_data)); +my $analyze = has_command("systemd-analyze"); +return (1, undef) if (!$analyze); my $name = $file; $name =~ s/^.*\///; return (0, $text{'systemd_ename'}) if (!valid_unit_file_name($name)); @@ -1905,6 +1910,8 @@ Writes the standard local drop-in override file for a system unit. sub write_system_dropin_file { my ($unit, $data) = @_; +return (0, $text{'systemd_edropininstall'}) + if (dropin_has_install_section($data)); my $file = system_dropin_file($unit); return (0, $text{'systemd_ename'}) if (!$file); my $dir = $file; @@ -2009,6 +2016,8 @@ Writes the standard user drop-in override file as the owning Unix user. sub write_user_dropin_file { my ($user, $unit, $data) = @_; +return (0, $text{'systemd_edropininstall'}) + if (dropin_has_install_section($data)); my $file = user_dropin_file($user, $unit); return (0, $text{'systemd_euserunitfile'}) if (!$file || !user_dropin_file_safe($user, $file, 0)); @@ -2174,6 +2183,8 @@ Writes a safe existing system drop-in config file. sub write_system_dropin_config_file { my ($file, $data) = @_; +return (0, $text{'systemd_edropininstall'}) + if (dropin_has_install_section($data)); return (0, $text{'systemd_edropinfile'}) if (!system_dropin_config_file_safe($file, 1)); return (1, undef) if (is_readonly_mode()); @@ -2252,6 +2263,8 @@ Writes a safe existing user drop-in config file as the owning Unix user. sub write_user_dropin_config_file { my ($user, $file, $data) = @_; +return (0, $text{'systemd_edropininstall'}) + if (dropin_has_install_section($data)); return (0, $text{'systemd_edropinfile'}) if (!user_dropin_config_file_safe($user, $file, 1)); return (1, undef) if (is_readonly_mode()); @@ -2418,6 +2431,24 @@ $data =~ s/^### Lines below this comment will be discarded\s*\n.*\z//ms; return $data; } +=head2 dropin_has_install_section(data) + +Returns 1 if a drop-in contains an active C<[Install]> section. Drop-ins are +for unit overrides; startup state is managed by enable and disable actions. + +=cut +sub dropin_has_install_section +{ +my ($data) = @_; +return 0 if (!defined($data)); +foreach my $line (split(/\n/, $data)) { + $line =~ s/\r//g; + next if ($line =~ /^\s*(?:[#;]|$)/); + return 1 if ($line =~ /^\s*\[\s*Install\s*\]\s*(?:[#;].*)?$/i); + } +return 0; +} + =head2 delete_user_unit_file(user, file) Deletes a user unit file as the owning Unix user after path validation, so a @@ -3022,6 +3053,8 @@ sub write_manual_unit_file my ($info, $data) = @_; return (0, $text{'manual_efile'}) if (!$info || !$info->{'file'}); +return (0, $text{'systemd_evendoredit'}) + if (!manual_unit_file_writable($info)); $data = "" if (!defined($data)); $data =~ s/\0//g; $data =~ s/\r//g; @@ -3067,6 +3100,20 @@ unlock_file($info->{'file'}); return (1, undef); } +=head2 manual_unit_file_writable(info) + +Returns 1 if a manual editor file descriptor may be saved. + +=cut +sub manual_unit_file_writable +{ +my ($info) = @_; +return 0 if (!$info || !$info->{'scope'}); +return 1 if ($info->{'scope'} eq 'user'); +return 1 if ($info->{'kind'} && $info->{'kind'} eq 'dropin'); +return system_unit_file_edit_allowed($info->{'file'}, $info->{'name'}); +} + =head2 mark_units_changed() Updates the flag file indicating that manual unit-file edits need reload. @@ -3579,6 +3626,51 @@ return 0 if (!unit_file_editable($unit)); return system_unit_file_delete_allowed($unit->{'file'}, $unit->{'name'}); } +=head2 system_unit_file_writable(&unit) + +Returns 1 if a system unit record points to a unit file that can be edited +under the current module configuration. + +=cut +sub system_unit_file_writable +{ +my ($unit) = @_; +return 0 if (!unit_file_editable($unit)); +return system_unit_file_edit_allowed($unit->{'file'}, $unit->{'name'}); +} + +=head2 system_unit_file_edit_allowed(file, name) + +Returns 1 if a system unit path is in a root where editing unit files is +currently permitted. + +=cut +sub system_unit_file_edit_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_edit_allowed($root)); + if ($file eq $root."/".$name) { + return 0 if (-l $file); + return 1; + } + } +return 0; +} + +=head2 system_unit_root_edit_allowed(root) + +Returns 1 if system unit files under this root may be edited. + +=cut +sub system_unit_root_edit_allowed +{ +my ($root) = @_; +return (local_unit_file_root($root) || + $config{'edit_vendor_units'} eq '1') ? 1 : 0; +} + =head2 system_unit_file_delete_allowed(file, name) Returns 1 if a system unit path is in a root where deleting unit files is diff --git a/systemd/t/run-tests.t b/systemd/t/run-tests.t index d45c5a7d4..c1d148734 100644 --- a/systemd/t/run-tests.t +++ b/systemd/t/run-tests.t @@ -77,9 +77,11 @@ our (%access, %config, %in, %text, %gconfig, $remote_user); systemd_euserunitfile => 'bad user unit file', systemd_euserunitdir => 'bad user unit dir', systemd_edropinfile => 'bad drop-in file', + systemd_edropininstall => 'bad drop-in install section', systemd_ereadonly => 'runtime unit file', systemd_ename => 'bad unit name', systemd_egone => 'unit gone', + systemd_evendoredit => 'vendor unit edit disabled', systemd_elocaldelete => 'local unit delete only', systemd_eclash => 'unit clash', systemd_emountwhat => 'missing mount source', @@ -197,6 +199,27 @@ ok(!unit_file_editable({ unitstate => 'generated', }), 'generated unit files are read-only'); +ok(system_unit_file_writable({ + name => 'local.service', + file => '/etc/systemd/system/local.service', + unitstate => 'enabled', + }), + 'local system unit files can be edited directly'); +ok(!system_unit_file_writable({ + name => 'vendor.service', + file => '/usr/lib/systemd/system/vendor.service', + unitstate => 'enabled', + }), + 'packaged system unit files cannot be edited directly by default'); +{ + local $config{'edit_vendor_units'} = 1; + ok(system_unit_file_writable({ + name => 'vendor.service', + file => '/usr/lib/systemd/system/vendor.service', + unitstate => 'enabled', + }), + 'packaged system unit files can be edited directly when configured'); +} ok(system_unit_file_deletable({ name => 'local.service', file => '/etc/systemd/system/local.service', @@ -1009,11 +1032,18 @@ like(get_unit_root(), qr{^/(etc|usr/lib|lib)/systemd/system$}, "[Service]\nRestart=always\n"); write_test_file("$packaged_root/vendor.socket", ""); write_test_file("$packaged_root/vendor.mount", ""); + symlink("$packaged_root/vendor.socket", "$local_root/vendor-link.service"); my @commands; local @main::list_units_cache = (); local *main::get_system_unit_file_roots = sub { return ($local_root, $packaged_root); }; + local *main::get_system_unit_file_root_candidates = sub { + return ($local_root, $packaged_root); + }; + local *main::get_local_unit_root = sub { + return $local_root; + }; local *main::get_system_dropin_roots = sub { return ($local_root); }; @@ -1148,6 +1178,21 @@ like(get_unit_root(), qr{^/(etc|usr/lib|lib)/systemd/system$}, 'manual unit files include system drop-in override files'); is($manual{"$local_root/demo.service.d/00-local.conf"}->{'kind'}, 'dropin', 'manual drop-in descriptor is marked'); + ok(manual_unit_file_writable($manual{"$local_root/local.path"}), + 'manual local system unit files are writable'); + ok(!manual_unit_file_writable($manual{"$packaged_root/vendor.mount"}), + 'manual packaged unit files are read-only by default'); + ok(!system_unit_file_writable({ + name => 'vendor-link.service', + file => "$local_root/vendor-link.service", + unitstate => 'enabled', + }), + 'local symlink unit files are not edited directly'); + { + local $config{'edit_vendor_units'} = 1; + ok(manual_unit_file_writable($manual{"$packaged_root/vendor.mount"}), + 'manual packaged unit files are writable when configured'); + } ok(!manual_system_unit_file_safe("$local_root/../escape.service"), 'manual system unit file safety rejects traversal'); my $manual_info = manual_unit_file("$local_root/local.path"); @@ -1161,6 +1206,8 @@ like(get_unit_root(), qr{^/(etc|usr/lib|lib)/systemd/system$}, manual_unit_file("$local_root/demo.service.d/00-local.conf"); ok($manual_dropin_info, 'manual_unit_file returns allowed drop-in descriptor'); + ok(manual_unit_file_writable($manual_dropin_info), + 'manual system drop-in files remain writable'); is(read_manual_unit_file($manual_dropin_info), "[Service]\nRestart=always\n", 'read_manual_unit_file reads system drop-in files'); @@ -1170,6 +1217,12 @@ like(get_unit_root(), qr{^/(etc|usr/lib|lib)/systemd/system$}, is(slurp_test_file("$local_root/demo.service.d/00-local.conf"), "[Service]\nRestart=on-failure\n", 'manual system drop-in write preserves exact file'); + ($ok, $err) = write_manual_unit_file( + $manual{"$packaged_root/vendor.mount"}, + "[Mount]\nWhat=/tmp\nWhere=/vendor\n"); + ok(!$ok, 'write_manual_unit_file rejects packaged unit writes by default'); + is($err, $text{'systemd_evendoredit'}, + 'write_manual_unit_file reports packaged unit edit policy'); unlink($main::unit_config_change_flag); unlink($main::daemon_reload_time_flag); ok(!needs_daemon_reload(), 'daemon reload is not needed initially'); @@ -1280,6 +1333,13 @@ like(get_unit_root(), qr{^/(etc|usr/lib|lib)/systemd/system$}, 'verify_dropin_data verifies the base unit name'); ok(!-e "$verify_root/verify-4/drop.service.d/override.conf", 'verify_dropin_data removes temporary override files'); + ($ok, $err) = verify_dropin_data( + '/etc/systemd/system/drop.service', + "[Unit]\nDescription=Drop\n[Service]\nExecStart=/bin/true\n", + "[Install]\nWantedBy=multi-user.target\n", 0); + ok(!$ok, 'verify_dropin_data rejects Install sections'); + is($err, $text{'systemd_edropininstall'}, + 'verify_dropin_data reports Install section policy'); my $before_transient_verify = scalar(@verify_commands); ($ok, $err) = verify_dropin_data( @@ -1346,6 +1406,10 @@ like(get_unit_root(), qr{^/(etc|usr/lib|lib)/systemd/system$}, "### Anything between here and the comment below will become ". "the new contents of the file\n\n\n\n", 'dropin_effective_data discards commented base unit contents'); + ok(dropin_has_install_section("[Install]\nWantedBy=multi-user.target\n"), + 'drop-in install-section detector rejects active Install sections'); + ok(!dropin_has_install_section("# [Install]\n[Service]\nRestart=always\n"), + 'drop-in install-section detector ignores commented examples'); my ($ok, $out) = write_system_dropin_file('demo.service', $template); ok($ok, 'write_system_dropin_file writes standard override files'); @@ -1368,6 +1432,12 @@ like(get_unit_root(), qr{^/(etc|usr/lib|lib)/systemd/system$}, "$dropin_root/demo.service.d/10-extra.conf", "[Service]\nRestartSec=10s\n"); ok($ok, 'system drop-in config writer updates exact safe file'); + ($ok, $out) = write_system_dropin_config_file( + "$dropin_root/demo.service.d/10-extra.conf", + "[Install]\nWantedBy=multi-user.target\n"); + ok(!$ok, 'system drop-in config writer rejects Install sections'); + is($out, $text{'systemd_edropininstall'}, + 'system drop-in config writer reports Install section policy'); is(slurp_test_file("$dropin_root/demo.service.d/10-extra.conf"), "[Service]\nRestartSec=10s\n", 'system drop-in config writer preserves non-standard filename'); @@ -1459,6 +1529,12 @@ like(get_unit_root(), qr{^/(etc|usr/lib|lib)/systemd/system$}, is(slurp_test_file("$root/demo.service.d/20-local.conf"), "[Service]\nEnvironment=DEMO=2\n", 'user drop-in config writer preserves non-standard filename'); + ($ok, $out) = write_user_dropin_config_file( + 'alice', "$root/demo.service.d/20-local.conf", + "[Install]\nWantedBy=default.target\n"); + ok(!$ok, 'user drop-in config writer rejects Install sections'); + is($out, $text{'systemd_edropininstall'}, + 'user drop-in config writer reports Install section policy'); ok(dropin_exists(1, 'alice', 'demo.service'), 'dropin_exists detects user override files'); is(read_user_dropin_file('alice', 'demo.service'), @@ -2044,6 +2120,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/^edit_vendor_units=/m, + 'module config exposes packaged unit edit policy'); like($config_source, qr/^delete_vendor_units=/m, 'module config exposes packaged unit delete policy'); like($config_source, qr/^default_linger=/m, @@ -2185,6 +2263,12 @@ like($save_source, qr/write_user_unit_file/, 'save page uses safe user-unit writer'); like($save_source, qr/unit_file_editable/, 'save page rejects direct writes to runtime-managed unit files'); +like($save_source, qr/system_unit_file_writable/, + 'save page rejects direct writes to packaged unit files by default'); +like($save_source, qr/systemd_evendoredit/, + 'save page reports packaged unit edit policy'); +like($save_source, qr/boot_state_changeable.*?\(\$user_scope \|\| system_unit_file_writable\(\$u\)\)/s, + 'save page ignores edit-form boot changes for protected packaged units'); like($save_source, qr/verify_unit_data/, 'save page verifies raw unit edits before writing'); like($save_source, qr/dropin_template/, @@ -2279,10 +2363,14 @@ like($edit_source, like($edit_source, qr/systemd_unituser.*systemd_runtime_state.*systemd_unit_state.*systemd_boot.*systemd_linger_user/s, 'edit page orders user unit metadata rows'); +like($edit_source, qr/boot_state_changeable.*?\(\$edit_user_scope \|\| \$unit_file_writable\).*?systemd_can_boot/s, + 'edit page hides boot radio for protected packaged units'); 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_writable/, + 'edit page makes packaged system unit files read-only by default'); like($edit_source, qr/system_unit_file_deletable/, 'edit page hides delete for packaged system unit files'); like($edit_source, qr/edit_depsnow/, @@ -2297,6 +2385,8 @@ like($edit_source, qr/edit_deleteoverridenow/, 'edit page labels override deletes clearly'); like($edit_source, qr/edit_stockunitnow/, 'edit page links override edits back to the stock unit'); +like($edit_source, qr/edit_view_stockunitnow/, + 'edit page labels protected base units as view-only from drop-ins'); like($edit_source, qr/stock_unit/, 'edit page uses a grouped button for stock-unit navigation'); like($edit_source, qr/systemd_can_edit/, @@ -2353,6 +2443,8 @@ unlike($edit_manual_source, qr/action_links\(/, 'manual editor header omits daemon reload action'); like($edit_manual_source, qr/systemd_can_manual/, 'manual editor filters files by ACL'); +like($edit_manual_source, qr/manual_unit_file_writable/, + 'manual editor hides save for read-only packaged unit files'); like($dropins_source, qr/list_system_dropin_override_files/, 'drop-in inventory lists system drop-ins'); like($dropins_source, qr/list_all_user_dropin_override_files/, @@ -2365,6 +2457,8 @@ like($dropins_source, qr/sub dropin_file_arg\b/, 'drop-in inventory links non-standard drop-ins by exact file'); like($save_manual_source, qr/write_manual_unit_file/, 'manual save uses constrained unit file writer'); +like($lib_source, qr/sub manual_unit_file_writable\b/, + 'library distinguishes writable manual files from read-only inventory'); like($save_manual_source, qr/mark_units_changed/, 'manual system save marks daemon reload as needed'); like($save_manual_source, qr/mark_user_units_changed/,