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 @@
+
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/,