From ed5bc3e4b6526e54e501451d40f1e8ede9e5f216 Mon Sep 17 00:00:00 2001 From: itamarperdomo Date: Thu, 11 Jun 2026 11:50:56 -0400 Subject: [PATCH 01/16] Improve alpine mysql/mariadb default support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * The MySQL module in Alpine it lacks default values. This change uses the necessary values ​​for the module to function. * These values ​​work for any version of Alpine Linux from 3.8 to Edge, since MariaDB is and always has been the default package. * The tools server package was included in the package instalation * Missing changelog entries were included --- mysql/CHANGELOG | 2 ++ mysql/config-alpine-linux | 25 +++++++++++++++++++++++++ software/CHANGELOG | 2 ++ software/apk-lib.pl | 2 +- 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 mysql/config-alpine-linux diff --git a/mysql/CHANGELOG b/mysql/CHANGELOG index fd0d8b9cb..82fd19c5b 100644 --- a/mysql/CHANGELOG +++ b/mysql/CHANGELOG @@ -1,3 +1,5 @@ +---- Changes since 2.641 ---- +Added defaults for alpine linux and fix package installs ---- Changes since 1.140 ---- The not null flag and a default value can be specified for fields in new tables. The form for creating an initial table in a new database is now the same as the one for adding a table to an existing database. diff --git a/mysql/config-alpine-linux b/mysql/config-alpine-linux new file mode 100644 index 000000000..797d4015e --- /dev/null +++ b/mysql/config-alpine-linux @@ -0,0 +1,25 @@ +start_cmd=rc-service mariadb start +mysql=/usr/bin/mariadb +mysqld=/usr/bin/mariadbd +mysqldump=/usr/bin/mysqldump +mysqlimport=/usr/bin/mariadb-import +mysql_libs=/usr/lib/mariadb/plugin +mysqladmin=/usr/bin/mariadb-admin +mysqlshow=/usr/bin/mariadb-show +perpage=25 +style=1 +add_mode=1 +nodbi=0 +access=*: * +blob_mode=0 +date_subs=0 +passwd_mode=0 +mysql_data=/var/lib/mysql +max_dbs=50 +my_cnf=/etc/my.cnf +max_text=1000 +nopwd=0 +webmin_subs=0 +ssl=0 +stop_cmd=rc-service mariadb stop + diff --git a/software/CHANGELOG b/software/CHANGELOG index 63d12958f..d1fbb992d 100644 --- a/software/CHANGELOG +++ b/software/CHANGELOG @@ -1,3 +1,5 @@ +---- Changes since 2.641 ---- +Fix mysql/mariadb package installs ---- Changes since 1.130 ---- Packages can now be installed directly from yum, if installed. The entire system can also be upgraded from yum. diff --git a/software/apk-lib.pl b/software/apk-lib.pl index b8d6d595d..68911afac 100644 --- a/software/apk-lib.pl +++ b/software/apk-lib.pl @@ -364,7 +364,7 @@ sub update_system_resolve my ($name) = @_; return $name eq "apache" ? "apache2" : $name eq "dhcpd" ? "dhcp" : - $name eq "mysql" ? "mariadb mariadb-client" : + $name eq "mysql" ? "mariadb mariadb-client mariadb-server-utils" : $name eq "postgresql" ? "postgresql postgresql-client" : $name eq "openldap" ? "openldap openldap-clients" : $name eq "ldap" ? "openldap openldap-clients" : From 53c3ee1c5de021161cea005d89e7d2109b139b52 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Fri, 12 Jun 2026 23:59:36 +0200 Subject: [PATCH 02/16] Fix to show only related config options for current boot system --- init/config.info | 8 +++--- init/config.info.ca | 2 +- init/config.info.de | 2 +- init/config.info.no | 2 +- init/init-lib.pl | 67 ++++++++++++++++++++++++++++++++++++++++++--- init/lang/en | 5 ++-- 6 files changed, 73 insertions(+), 13 deletions(-) diff --git a/init/config.info b/init/config.info index 9ed4b0d3b..8ea07a704 100644 --- a/init/config.info +++ b/init/config.info @@ -5,7 +5,7 @@ order=Show boot order of actions?,1,1-Yes,0-No status_check=Show current status of actions,1,2-On index and action pages,1-On action page only,0-No sort_mode=Sort actions by,1,1-Boot order,0-Name line2=System configuration,11 -init_mode=Boot system,4,-Detect automatically,init-SysV init,upstart-Upstart,systemd-Systemd,osx-MacOS,rc-FreeBSD RC scripts,win32-Windows services,local-Single boot script +init_mode=Boot system,4,-Detect automatically,systemd-Systemd,init-SysV init,openrc-OpenRC,upstart-Upstart,local-Single boot script,rc-FreeBSD RC scripts,launchd-macOS launchd,osx-Legacy macOS StartupItems,win32-Windows services init_base=Directory in which runlevel directories are located,0 init_dir=Directory containing master init scripts,0 order_digits=Number of digits in action order,0 @@ -17,8 +17,8 @@ shutdown_command=Command to shutdown the system,0 inittab_id=inittab ID for bootup runlevel,0 rc_dir=FreeBSD rc scripts directories,3,None rc_conf=FreeBSD configuration files,3,None -line3=OSX system configuration,11 -startup_dirs=Darwin StartupItems directories,0 +line3=macOS StartupItems configuration,11 +startup_dirs=macOS StartupItems directories,0 darwin_setup=Directory for custom StartupItems,0 -hostconfig=Darwin hostconfig file,0 +hostconfig=Legacy hostconfig file,0 plist=Name of plist in a StartupItems directory,0 diff --git a/init/config.info.ca b/init/config.info.ca index 02d2e5360..2388274e4 100644 --- a/init/config.info.ca +++ b/init/config.info.ca @@ -5,7 +5,7 @@ order=Mostra l'ordre d'engegada de les accions,1,1-Sí,0-No status_check=Mostra l'estat actual de les accions,1,2-A les pàgines d'índex i de l'acció,1-Només a la pàgina de l'acció,0-No sort_mode=Ordena les accions per,1,1-Ordre d'engegada,0-Nom line2=Configuració del sistema,11 -init_mode=Engegada del sistema,4,-Detecta-ho automàticament,init-Init de SysV,upstart-Upstart,systemd-Systemd,osx-MacOS,rc-Scripts RC de FreeBSD,win32-Serveis Windows,local-Script únic d'engegada +init_mode=Engegada del sistema,4,-Detecta-ho automàticament,systemd-Systemd,init-Init de SysV,openrc-OpenRC,upstart-Upstart,local-Script únic d'engegada,rc-Scripts RC de FreeBSD,launchd-macOS launchd,osx-Legacy macOS StartupItems,win32-Serveis Windows init_base=Directori on s'allotgen els directoris de nivells d'execució,0 init_dir=Directori que conté els scripts init mestres,0 order_digits=Nombre de dígits de l'ordre de l'acció,0 diff --git a/init/config.info.de b/init/config.info.de index 4a0b7ffef..a54c5687d 100644 --- a/init/config.info.de +++ b/init/config.info.de @@ -5,7 +5,7 @@ order=Zeige die Aktionen in Reihenfolge des Starts an?,1,1-Ja,0-Nein status_check=Zeige aktuellen Status der Aktionen,1,2-Auf Index- und Aktionen-Seite,1-Nur auf der Aktionen-Seite,0-Nein sort_mode=Sortiere Aktionen nach,1,1-Boot-Reihenfolge,0-Name line2=Systemkonfiguration,11 -init_mode=Boot-System,4,-automatisch erkennen,init-SysV init,upstart-Upstart,systemd-Systemd,osx-MacOS,rc-FreeBSD RC Script,win32-Windows-Dienste,local-Single Bootskript +init_mode=Boot-System,4,-automatisch erkennen,systemd-Systemd,init-SysV init,openrc-OpenRC,upstart-Upstart,local-Single Bootskript,rc-FreeBSD RC Script,launchd-macOS launchd,osx-Legacy macOS StartupItems,win32-Windows-Dienste init_base=Verzeichnis, in dem die Runlevel-Verzeichnisse liegen,0 init_dir=Verzeichnis, in dem das Haupt-Skript liegt,0 order_digits=Anzahl Ziffern in Aktionsreihenfolge,0 diff --git a/init/config.info.no b/init/config.info.no index 227712c50..c75fcf84e 100644 --- a/init/config.info.no +++ b/init/config.info.no @@ -5,7 +5,7 @@ order=Vis handlingenes oppstartsrekkefølge?1,1-Ja,0-Nei status_check=Vis handlingenes gjeldende status,1,2-På indeks- og handlingssider,1-Bare på handlingssider,0-Nei sort_mode=Sorter handlinger etter,1,1-Oppstartrekkefølge,0-Navn line2=System konfigurasjon,11 -init_mode=Oppstartssystem,4,-Oppdag automatisk,init-SysV init,upstart-Upstart,systemd-Systemd,osx-MacOS,rc-FreeBSD RC scripts,win32-Windows tjenester,local-Enkelt oppstartsscript +init_mode=Oppstartssystem,4,-Oppdag automatisk,systemd-Systemd,init-SysV init,openrc-OpenRC,upstart-Upstart,local-Enkelt oppstartsscript,rc-FreeBSD RC scripts,launchd-macOS launchd,osx-Legacy macOS StartupItems,win32-Windows tjenester init_base=Katalog hvor kjøretids-kataloger finnes,0 init_dir=Katalog som inneholder master init scripts,0 order_digits=Antall sifre i handlingssortering,0 diff --git a/init/init-lib.pl b/init/init-lib.pl index 620f8351d..e3c25b8fa 100644 --- a/init/init-lib.pl +++ b/init/init-lib.pl @@ -28,9 +28,9 @@ use WebminCore; This variable is set based on the bootup system in use. Possible values are : -=item osx - MacOSX hostconfig files, for older versions +=item osx - Legacy macOS StartupItems and hostconfig files -=item launchd - MacOS Launchd, for newer versions +=item launchd - macOS launchd, for newer versions =item rc - FreeBSD 6+ RC files @@ -3142,11 +3142,70 @@ return $name =~ /\./ ? $name : "com.webmin.".$name; } # config_pre_load(mod-info, [mod-order]) -# Check if some config options are conditional +# Hides config options that do not apply to the detected boot system. sub config_pre_load { my ($modconf_info, $modconf_order) = @_; -$modconf_info->{'desc'} =~ s/2-[^,]+,// if ($init_mode eq "systemd"); +return if (ref($modconf_info) ne 'HASH'); + +if ($init_mode eq "systemd" && $modconf_info->{'desc'}) { + # Systemd has no runlevels, so keep only the plain yes/no choices. + $modconf_info->{'desc'} =~ s/2-[^,]+,//; + } + +my %keep = map { $_, 1 } &init_config_options_for_mode($init_mode); +foreach my $key (keys %$modconf_info) { + delete($modconf_info->{$key}) if (!$keep{$key}); + } +if (ref($modconf_order) eq 'ARRAY') { + @$modconf_order = grep { $keep{$_} } @$modconf_order; + } +&hide_single_init_config_section($modconf_info, $modconf_order); +} + +# init_config_options_for_mode(mode) +# Returns config.info keys that should be visible for a boot system. +sub init_config_options_for_mode +{ +my ($mode) = @_; +my @display = ( 'expert', 'desc', 'order', 'status_check', 'sort_mode' ); +my @common = ( 'init_mode', 'reboot_command', 'shutdown_command' ); +my @sysv = ( @common, 'init_base', 'init_dir', 'order_digits', + 'boot_levels', 'local_script', 'local_down', 'inittab_id' ); + +return ( 'line1', 'desc', 'line2', @common ) + if ($mode eq 'systemd'); +return ( 'line1', @display, 'line2', @sysv ) + if ($mode eq 'init' || $mode eq 'upstart' || $mode eq 'openrc'); +return ( 'line2', @common, 'local_script', 'local_down', + 'rc_dir', 'rc_conf' ) + if ($mode eq 'rc'); +return ( 'line2', @common, 'local_script', 'local_down' ) + if ($mode eq 'local'); +return ( 'line2', @common, 'line3', 'startup_dirs', 'darwin_setup', + 'hostconfig', 'plist' ) + if ($mode eq 'osx'); +return ( 'line2', @common ) + if ($mode eq 'launchd' || $mode eq 'win32'); +return ( 'line1', @display, 'line2', @sysv, 'rc_dir', 'rc_conf', + 'line3', 'startup_dirs', 'darwin_setup', 'hostconfig', 'plist' ); +} + +# hide_single_init_config_section(&config-info, [&config-order]) +# Removes the lone section header when filtering leaves only one group. +sub hide_single_init_config_section +{ +my ($modconf_info, $modconf_order) = @_; +my @sections = grep { + exists($modconf_info->{$_}) && + (split(/,/, $modconf_info->{$_}))[1] == 11 + } keys %$modconf_info; +return if (@sections != 1); + +delete($modconf_info->{$sections[0]}); +if (ref($modconf_order) eq 'ARRAY') { + @$modconf_order = grep { $_ ne $sections[0] } @$modconf_order; + } } 1; diff --git a/init/lang/en b/init/lang/en index 7844f1c36..5a1dab256 100644 --- a/init/lang/en +++ b/init/lang/en @@ -188,13 +188,14 @@ change_title=Switch Runlevel change_cmd=Switching to runlevel $1 with command $2. This may take some time, and Webmin might not be available anymore after switching. mode_init=SysV init -mode_osx=MacOS +mode_osx=Legacy macOS StartupItems mode_local=Single boot script mode_win32=Windows services mode_rc=FreeBSD RC scripts mode_upstart=Upstart mode_systemd=Systemd -mode_launchd=LaunchD +mode_launchd=macOS launchd +mode_openrc=OpenRC upstart_title1=Create Upstart Service upstart_title2=Edit Upstart Service From 8f8199a4bf2fba1fbda0856fcab5c8125e29010d Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sat, 13 Jun 2026 00:14:08 +0200 Subject: [PATCH 03/16] Fix to simplify and show only systemd services --- init/index.cgi | 11 +---------- init/init-lib.pl | 16 ++++++---------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/init/index.cgi b/init/index.cgi index c87868ae9..b5bf9863e 100755 --- a/init/index.cgi +++ b/init/index.cgi @@ -337,11 +337,9 @@ elsif ($init_mode eq "systemd" && $access{'bootup'}) { print &ui_links_row(\@links); print &ui_columns_start([ "", $text{'systemd_name'}, $config{'desc'} ? $text{'systemd_desc'} : (), - $text{'systemd_type'}, $text{'systemd_status'}, $text{'systemd_boot'}, $text{'index_ustatus'} ]); - my $units_piped = join('|', map { quotemeta } &get_systemd_unit_types()); foreach $u (&list_systemd_services()) { if ($u->{'legacy'}) { $l = "edit_action.cgi?0+".&urlize($u->{'name'}); @@ -350,13 +348,7 @@ elsif ($init_mode eq "systemd" && $access{'bootup'}) { $l = "edit_systemd.cgi?name=".&urlize($u->{'name'}); } my $sname = $u->{'name'}; - my ($type) = $sname =~ /\.([^.]+)$/; - if (defined($type) && $type =~ /^(?:$units_piped)$/) { - $sname =~ s/\.$type$//; - } - else { - $type = ''; - } + $sname =~ s/\.service$//; my $title = ($u->{'boot'} == -1 ? &html_escape($sname) : &ui_link($l, &html_escape($sname))); @@ -365,7 +357,6 @@ elsif ($init_mode eq "systemd" && $access{'bootup'}) { &ui_checkbox("d", $u->{'name'}, undef), $title, $desc // (), - $type, $u->{'fullstatus'} || "$text{'index_unknown'}", $u->{'boot'} == 1 ? &ui_text_color("$text{'yes'}", 'success') : diff --git a/init/init-lib.pl b/init/init-lib.pl index e3c25b8fa..b49f28d58 100644 --- a/init/init-lib.pl +++ b/init/init-lib.pl @@ -2311,16 +2311,15 @@ if (@list_systemd_services_cache && !$noinit) { return @list_systemd_services_cache; } -my $units_piped = join('|', &get_systemd_unit_types()); - # Get all systemd unit names my $out = &backquote_command("systemctl list-units --full --all -t service --no-legend"); my $ex = $?; foreach my $l (split(/\r?\n/, $out)) { $l =~ s/^[^a-z0-9\-\_\.]+//i; my ($unit, $loaded, $active, $sub, $desc) = split(/\s+/, $l, 5); + next if ($unit !~ /\.service$/); my $a = $unit; - $a =~ s/\.($units_piped)$//; + $a =~ s/\.service$//; my $f = &action_filename($a); if ($unit ne "UNIT" && $loaded eq "loaded" && !-r $f) { push(@units, $unit); @@ -2332,29 +2331,26 @@ foreach my $l (split(/\r?\n/, $out)) { # and so don't show up in systemctl list-units my $root = &get_systemd_root(undef, 1); opendir(UNITS, $root); -push(@units, grep { !/\.wants$/ && !/^\./ && !-d "$root/$_" } readdir(UNITS)); +push(@units, grep { /\.service$/ && !-d "$root/$_" } readdir(UNITS)); closedir(UNITS); # Also add units from list-unit-files that also don't show up $out = &backquote_command("systemctl list-unit-files -t service --no-legend"); foreach my $l (split(/\r?\n/, $out)) { - if ($l =~ /^(\S+\.($units_piped))\s+disabled/ || - $l =~ /^(\S+)\s+disabled/) { + if ($l =~ /^(\S+\.service)\s+disabled/) { push(@units, $1); } } # Skip useless units @units = grep { !/^sys-devices-/ && - !/^\-\.mount/ && - !/^\-\.slice/ && !/^dev-/ && !/^systemd-/ } @units; @units = &unique(@units); # Filter out templates -my @templates = grep { /\@$/ || /\@\.($units_piped)$/ } @units; -@units = grep { !/\@$/ && !/\@\.($units_piped)$/ } @units; +my @templates = grep { /\@$/ || /\@\.service$/ } @units; +@units = grep { !/\@$/ && !/\@\.service$/ } @units; # Dump state of all of them, 100 at a time my %info; From 8dd06e3c3498bec578125ee124a50a4e6f16db23 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sat, 13 Jun 2026 00:52:47 +0200 Subject: [PATCH 04/16] Add ability to hide services table if dedicated module is avail --- init/config.info | 1 + init/index.cgi | 120 ++++++++++++++++++++++++++--------------------- init/init-lib.pl | 22 ++++++++- init/lang/en | 2 + 4 files changed, 90 insertions(+), 55 deletions(-) diff --git a/init/config.info b/init/config.info index 8ea07a704..7f778fa2e 100644 --- a/init/config.info +++ b/init/config.info @@ -1,6 +1,7 @@ line1=Configurable options,11 expert=Allow selection of individual runlevels?,1,1-Yes,0-No desc=Display actions with descriptions,1,2-Yes, and show all runlevels,1-Yes,0-No +systemd_dedicated=Hide systemd services table?,1,-No,1-Yes order=Show boot order of actions?,1,1-Yes,0-No status_check=Show current status of actions,1,2-On index and action pages,1-On action page only,0-No sort_mode=Sort actions by,1,1-Boot order,0-Name diff --git a/init/index.cgi b/init/index.cgi index b5bf9863e..31cb73e1b 100755 --- a/init/index.cgi +++ b/init/index.cgi @@ -329,61 +329,74 @@ elsif ($init_mode eq "upstart" && $access{'bootup'}) { } elsif ($init_mode eq "systemd" && $access{'bootup'}) { - # Show systemd actions - print &ui_form_start("mass_systemd.cgi", "post"); - @links = ( &select_all_link("d"), - &select_invert_link("d"), - &ui_link("edit_systemd.cgi?new=1", $text{'index_sadd'}) ); - print &ui_links_row(\@links); - print &ui_columns_start([ "", $text{'systemd_name'}, - $config{'desc'} ? $text{'systemd_desc'} : (), - $text{'systemd_status'}, - $text{'systemd_boot'}, - $text{'index_ustatus'} ]); - foreach $u (&list_systemd_services()) { - if ($u->{'legacy'}) { - $l = "edit_action.cgi?0+".&urlize($u->{'name'}); - } - else { - $l = "edit_systemd.cgi?name=".&urlize($u->{'name'}); - } - my $sname = $u->{'name'}; - $sname =~ s/\.service$//; - my $title = ($u->{'boot'} == -1 ? - &html_escape($sname) : - &ui_link($l, &html_escape($sname))); - my $desc = $config{'desc'} ? &html_escape($u->{'desc'}) : undef; - print &ui_columns_row([ - &ui_checkbox("d", $u->{'name'}, undef), - $title, - $desc // (), - $u->{'fullstatus'} || "$text{'index_unknown'}", - $u->{'boot'} == 1 ? - &ui_text_color("$text{'yes'}", 'success') : - $u->{'boot'} == 2 ? - &ui_text_color("$text{'index_sboot6'}", 'success') : - $u->{'boot'} == -1 ? - &ui_text_color("$text{'index_sboot5'}", 'warn') : - &ui_text_color("$text{'no'}", 'warn'), - $u->{'status'} == 1 ? &ui_text_color("$text{'yes'}", 'success') : - $u->{'status'} == 0 ? - &ui_text_color("$text{'no'}", 'warn') : - "$text{'index_unknown'}", - ]); + if (!&init_show_systemd_services()) { + my $systemd_link = &ui_tag('a', + $text{'index_systemd_module'}, + { 'href' => '../systemd/' }); + print &ui_alert_box( + &text('index_systemd_dedicated', $systemd_link), + 'info', undef, undef, ""),"\n"; } - print &ui_columns_end(); - print &ui_links_row(\@links); - print &ui_form_end([ [ "start", $text{'index_start'} ], - [ "stop", $text{'index_stop'} ], - [ "restart", $text{'index_restart'} ], - undef, - [ "addboot", $text{'index_addboot'} ], - [ "delboot", $text{'index_delboot'} ], - undef, - [ "addboot_start", $text{'index_addboot_start'} ], - [ "delboot_stop", $text{'index_delboot_stop'} ], - ]); + else { + # Show systemd actions + print &ui_form_start("mass_systemd.cgi", "post"); + @links = ( &select_all_link("d"), + &select_invert_link("d"), + &ui_link("edit_systemd.cgi?new=1", + $text{'index_sadd'}) ); + print &ui_links_row(\@links); + print &ui_columns_start([ "", $text{'systemd_name'}, + $config{'desc'} ? + $text{'systemd_desc'} : (), + $text{'systemd_status'}, + $text{'systemd_boot'}, + $text{'index_ustatus'} ]); + foreach $u (&list_systemd_services()) { + if ($u->{'legacy'}) { + $l = "edit_action.cgi?0+".&urlize($u->{'name'}); + } + else { + $l = "edit_systemd.cgi?name=".&urlize($u->{'name'}); + } + my $sname = $u->{'name'}; + $sname =~ s/\.service$//; + my $title = ($u->{'boot'} == -1 ? + &html_escape($sname) : + &ui_link($l, &html_escape($sname))); + my $desc = $config{'desc'} ? + &html_escape($u->{'desc'}) : undef; + print &ui_columns_row([ + &ui_checkbox("d", $u->{'name'}, undef), + $title, + $desc // (), + $u->{'fullstatus'} || "$text{'index_unknown'}", + $u->{'boot'} == 1 ? + &ui_text_color("$text{'yes'}", 'success') : + $u->{'boot'} == 2 ? + &ui_text_color("$text{'index_sboot6'}", 'success') : + $u->{'boot'} == -1 ? + &ui_text_color("$text{'index_sboot5'}", 'warn') : + &ui_text_color("$text{'no'}", 'warn'), + $u->{'status'} == 1 ? &ui_text_color("$text{'yes'}", 'success') : + $u->{'status'} == 0 ? + &ui_text_color("$text{'no'}", 'warn') : + "$text{'index_unknown'}", + ]); + } + print &ui_columns_end(); + print &ui_links_row(\@links); + print &ui_form_end([ [ "start", $text{'index_start'} ], + [ "stop", $text{'index_stop'} ], + [ "restart", $text{'index_restart'} ], + undef, + [ "addboot", $text{'index_addboot'} ], + [ "delboot", $text{'index_delboot'} ], + undef, + [ "addboot_start", $text{'index_addboot_start'} ], + [ "delboot_stop", $text{'index_delboot_stop'} ], + ]); + } } elsif ($init_mode eq "launchd" && $access{'bootup'}) { # Show launchd agents @@ -440,4 +453,3 @@ if ($access{'shutdown'}) { print &ui_buttons_end(); &ui_print_footer("/", $text{'index'}); - diff --git a/init/init-lib.pl b/init/init-lib.pl index b49f28d58..451047399 100644 --- a/init/init-lib.pl +++ b/init/init-lib.pl @@ -3168,8 +3168,11 @@ my @display = ( 'expert', 'desc', 'order', 'status_check', 'sort_mode' ); my @common = ( 'init_mode', 'reboot_command', 'shutdown_command' ); my @sysv = ( @common, 'init_base', 'init_dir', 'order_digits', 'boot_levels', 'local_script', 'local_down', 'inittab_id' ); +my @systemd_display = ( 'desc' ); +push(@systemd_display, 'systemd_dedicated') + if (&init_dedicated_systemd_module_available()); -return ( 'line1', 'desc', 'line2', @common ) +return ( 'line1', @systemd_display, 'line2', @common ) if ($mode eq 'systemd'); return ( 'line1', @display, 'line2', @sysv ) if ($mode eq 'init' || $mode eq 'upstart' || $mode eq 'openrc'); @@ -3204,4 +3207,21 @@ if (ref($modconf_order) eq 'ARRAY') { } } +# init_dedicated_systemd_module_available() +# Returns 1 if the standalone Systemd module can be used by this Webmin user. +sub init_dedicated_systemd_module_available +{ +return &foreign_available("systemd") && &foreign_installed("systemd"); +} + +# init_show_systemd_services() +# Returns 1 if this module should show its legacy systemd service table. +sub init_show_systemd_services +{ +return 1 if (!&init_dedicated_systemd_module_available()); +return 0 if ($config{'systemd_dedicated'} && + $config{'systemd_dedicated'} eq '1'); +return 1; +} + 1; diff --git a/init/lang/en b/init/lang/en index 5a1dab256..1dbd92582 100644 --- a/init/lang/en +++ b/init/lang/en @@ -247,6 +247,8 @@ systemd_edesc=Missing unit description systemd_return=systemd unit systemd_econf=No systemd unit configuration entered systemd_estart=Missing commands to run on startup +index_systemd_module=Systemd Services and Units +index_systemd_dedicated=Use the dedicated $1 module to manage systemd services and units. This page now shows only system reboot and shutdown controls. launchd_title1=Create Launchd Agent launchd_title2=Edit Launchd Agent From da12554998194575c6d32097a17b1003ea0b5918 Mon Sep 17 00:00:00 2001 From: Joe Cooper Date: Sat, 13 Jun 2026 05:34:03 -0500 Subject: [PATCH 05/16] Don't fork. systemd prefers one process. --- webmin-systemd | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/webmin-systemd b/webmin-systemd index d7de6c4d6..52214a373 100644 --- a/webmin-systemd +++ b/webmin-systemd @@ -5,11 +5,10 @@ After=network.target network-online.target [Service] Environment="PERLLIB=WEBMIN_LIBDIR" -ExecStart=WEBMIN_LIBDIR/miniserv.pl WEBMIN_CONFIG/miniserv.conf +ExecStart=WEBMIN_LIBDIR/miniserv.pl --nofork WEBMIN_CONFIG/miniserv.conf ExecStop=WEBMIN_KILLCMD $MAINPID ExecReload=WEBMIN_KILLCMD -HUP $MAINPID PIDFile=WEBMIN_VAR/miniserv.pid -Type=forking Restart=always RestartSec=2s TimeoutSec=15s From ebbbf7cecbc13675b3f4c870b8f27b77f9da18cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D1=85=D0=B0=D1=80=D0=B4=20PICCORO=20Len?= =?UTF-8?q?z=20McKAY?= Date: Sat, 13 Jun 2026 10:20:44 -0400 Subject: [PATCH 06/16] Update CHANGELOG for software module on Alpine Linux * Update CHANGELOG for software pointing that alpine linux mysql package is supported for sure since v 3.16 --- software/CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/software/CHANGELOG b/software/CHANGELOG index d1fbb992d..988952cae 100644 --- a/software/CHANGELOG +++ b/software/CHANGELOG @@ -1,5 +1,5 @@ ---- Changes since 2.641 ---- -Fix mysql/mariadb package installs +Fix Alpine Linux mysql/mariadb package installs names due missing server utils (means at least Alpine Linux package installation is supported since Alpine linux v 3.16 up to edge) ---- Changes since 1.130 ---- Packages can now be installed directly from yum, if installed. The entire system can also be upgraded from yum. From 7d6af2074112ede7b9be41af3deb3ca8581661b8 Mon Sep 17 00:00:00 2001 From: Joe Cooper Date: Sat, 13 Jun 2026 15:42:53 -0500 Subject: [PATCH 07/16] Don't need ExecStop if systemd is managing process directly --- webmin-systemd | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/webmin-systemd b/webmin-systemd index 52214a373..34f275644 100644 --- a/webmin-systemd +++ b/webmin-systemd @@ -6,13 +6,10 @@ After=network.target network-online.target [Service] Environment="PERLLIB=WEBMIN_LIBDIR" ExecStart=WEBMIN_LIBDIR/miniserv.pl --nofork WEBMIN_CONFIG/miniserv.conf -ExecStop=WEBMIN_KILLCMD $MAINPID ExecReload=WEBMIN_KILLCMD -HUP $MAINPID -PIDFile=WEBMIN_VAR/miniserv.pid -Restart=always +Restart=on-failure RestartSec=2s -TimeoutSec=15s -TimeoutStopSec=300s +TimeoutStopSec=120s [Install] WantedBy=multi-user.target From 02bdfc20dbf2e4db2dbda58079da690de41741d6 Mon Sep 17 00:00:00 2001 From: Joe Cooper Date: Sat, 13 Jun 2026 15:43:24 -0500 Subject: [PATCH 08/16] Type simple for compat with old systems --- webmin-systemd | 1 + 1 file changed, 1 insertion(+) diff --git a/webmin-systemd b/webmin-systemd index 34f275644..ecb812347 100644 --- a/webmin-systemd +++ b/webmin-systemd @@ -4,6 +4,7 @@ Wants=network-online.target After=network.target network-online.target [Service] +Type=simple Environment="PERLLIB=WEBMIN_LIBDIR" ExecStart=WEBMIN_LIBDIR/miniserv.pl --nofork WEBMIN_CONFIG/miniserv.conf ExecReload=WEBMIN_KILLCMD -HUP $MAINPID From bf722a658d7a183306c1b90e7e0f96767551c5af Mon Sep 17 00:00:00 2001 From: Joe Cooper Date: Sat, 13 Jun 2026 15:59:45 -0500 Subject: [PATCH 09/16] Ah, I guess keep 5 minute TimeoutStopSec --- webmin-systemd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webmin-systemd b/webmin-systemd index ecb812347..dc91be820 100644 --- a/webmin-systemd +++ b/webmin-systemd @@ -10,7 +10,7 @@ ExecStart=WEBMIN_LIBDIR/miniserv.pl --nofork WEBMIN_CONFIG/miniserv.conf ExecReload=WEBMIN_KILLCMD -HUP $MAINPID Restart=on-failure RestartSec=2s -TimeoutStopSec=120s +TimeoutStopSec=300s [Install] WantedBy=multi-user.target From a56748a3fc136ddaa6d24a626d100beb9a8b870a Mon Sep 17 00:00:00 2001 From: Jamie Cameron Date: Sat, 13 Jun 2026 18:49:12 -0700 Subject: [PATCH 10/16] Add support for IP-based lets encrypt certs, with version 5.3 of certbot --- webmin/letsencrypt-lib.pl | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/webmin/letsencrypt-lib.pl b/webmin/letsencrypt-lib.pl index dabf3354e..d0a0cefd3 100755 --- a/webmin/letsencrypt-lib.pl +++ b/webmin/letsencrypt-lib.pl @@ -111,7 +111,7 @@ return &software::missing_install_link( "certbot", $text{'letsencrypt_certbot'}, $rlink, $rmsg); } -# request_letsencrypt_cert(domain|&domains, webroot, [email], [keysize], +# request_letsencrypt_cert(domain|&domains|&ips, webroot, [email], [keysize], # [request-mode], [use-staging], [account-email], # [key-type], [reuse-key], # [directory-url, server-key, server-hmac], @@ -125,11 +125,15 @@ my ($dom, $webroot, $email, $size, $mode, $staging, $account_email, $key_type, $reuse_key, $directory_url, $server_key, $server_hmac, $subset) = @_; my @doms = ref($dom) ? @$dom : ($dom); -$email ||= "root\@$doms[0]"; -$mode ||= "web"; @doms = &unique(@doms); +my @ips = grep { &check_ipaddress($_) } @doms; +@doms = grep { !&check_ipaddress($_) } @doms; +$email ||= (@doms ? "root\@$doms[0]" : "root\@".&get_system_hostname()); +$mode ||= "web"; $reuse_key = $config{'letsencrypt_reuse'} if (!defined($reuse_key)); my ($challenge, $wellknown, $challenge_new, $wellknown_new, $wildcard); +my $cmd_ver = $letsencrypt_cmd ? &get_certbot_major_version($letsencrypt_cmd) + : undef; # Wildcard mode? foreach my $d (@doms) { @@ -142,6 +146,17 @@ if (($server_key || $server_hmac) && !$letsencrypt_cmd) { return (0, $text{'letsencrypt_eeabnative'}); } +# Check if IP addresses are supported +if (@ips) { + $mode eq "dns" && return (0, "DNS-based validation cannot be used ". + "for IP addresses"); + $letsencrypt_cmd || return (0, "The certbot command is required for ". + "IP address certificates"); + &compare_version_numbers($cmd_ver, "5.3") >= 0 || + return (0, "Certbot version 5.3 or later is required for ". + "IP address certificates"); + } + if ($mode eq "web") { # Create a challenges directory under the web root if ($wildcard) { @@ -232,7 +247,6 @@ if ($letsencrypt_cmd) { &print_tempfile(TEMP, "text = True\n"); &close_tempfile(TEMP); my $dir = $letsencrypt_cmd; - my $cmd_ver = &get_certbot_major_version($letsencrypt_cmd); my $old_flags = ""; my $new_flags = ""; my $reuse_flags = ""; @@ -271,13 +285,14 @@ if ($letsencrypt_cmd) { $dir =~ s/\/[^\/]+$//; $size ||= 4096; my $out; + my $certname = @doms ? $doms[0] : $ips[0]; my $common_flags = " --duplicate". " --force-renewal". " --non-interactive". " --agree-tos". " --config ".quotemeta($temp)."". " --rsa-key-size ".quotemeta($size). - " --cert-name ".quotemeta($doms[0]). + " --cert-name ".quotemeta($certname). " --no-autorenew". (!$directory_url && $staging ? " --test-cert" : ""); if ($mode eq "web") { @@ -287,7 +302,9 @@ if ($letsencrypt_cmd) { "cd $dir && (echo A | $letsencrypt_cmd certonly". " -a webroot ". join("", map { " -d ".quotemeta($_) } @doms). + join("", map { " --ip-address ".quotemeta($_) } @ips). " --webroot-path ".quotemeta($webroot). + (@ips ? " --preferred-profile shortlived" : ""). $common_flags. $reuse_flags. $old_flags. @@ -328,6 +345,8 @@ if ($letsencrypt_cmd) { "cd $dir && (echo A | $letsencrypt_cmd certonly". " --standalone". join("", map { " -d ".quotemeta($_) } @doms). + join("", map { " --ip-address ".quotemeta($_) } @ips). + (@ips ? " --preferred-profile shortlived" : ""). $common_flags. $reuse_flags. $old_flags. @@ -353,8 +372,8 @@ if ($letsencrypt_cmd) { } else { # Try searching common paths - my @fulls = (glob("/etc/letsencrypt/live/$doms[0]-*/cert.pem"), - glob("/usr/local/etc/letsencrypt/live/$doms[0]-*/cert.pem")); + my @fulls = (glob("/etc/letsencrypt/live/$certname-*/cert.pem"), + glob("/usr/local/etc/letsencrypt/live/$certname-*/cert.pem")); if (@fulls) { my %stats = map { $_, [ stat($_) ] } @fulls; @fulls = sort { $stats{$a}->[9] <=> $stats{$b}->[9] } From 630de1441080c0c013e10be170255fb5f91fb625 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sun, 14 Jun 2026 12:46:43 +0200 Subject: [PATCH 11/16] Add custom nftables postgresql modules https://forum.virtualmin.com/t/custom-commands-module-has-gone/137403/65?u=ilia --- mod_core_list.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod_core_list.txt b/mod_core_list.txt index fd8c059c9..708966c4e 100644 --- a/mod_core_list.txt +++ b/mod_core_list.txt @@ -1 +1 @@ -acl apache authentic-theme backup-config bind8 change-user cron dovecot fail2ban fdisk filemin firewalld fsdump gray-theme htaccess-htpasswd init logrotate logviewer lvm mailboxes mailcap mount mysql net package-updates passwd phpini postfix proc procmail proftpd quota servers software spam sshd status system-status time updown useradmin usermin webmin webmincron webminlog xterm \ No newline at end of file +acl apache authentic-theme backup-config bind8 change-user cron custom dovecot fail2ban fdisk filemin firewalld fsdump gray-theme htaccess-htpasswd init logrotate logviewer lvm mailboxes mailcap mount mysql net nftables package-updates passwd phpini postfix postgresql proc procmail proftpd quota servers software spam sshd status system-status time updown useradmin usermin webmin webmincron webminlog xterm From 8953add81e66b7df89237d79331fb040b65b03a3 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sun, 14 Jun 2026 14:06:18 +0200 Subject: [PATCH 12/16] Fix it to properly retire and exclude nftables for now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⓘ Move "custom" and "postgresql" into the core Webmin package and add Debian/RPM package metadata so upgrades retire the old standalone module packages cleanly. --- makedebian.pl | 3 ++- makerpm.pl | 2 ++ mod_core_list.txt | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/makedebian.pl b/makedebian.pl index a12689c74..3cf04d9b4 100755 --- a/makedebian.pl +++ b/makedebian.pl @@ -127,7 +127,8 @@ Provides: $baseproduct EOF if ($product eq "webmin") { print CONTROL < Date: Sun, 14 Jun 2026 14:20:21 +0200 Subject: [PATCH 13/16] Fix to drop breaks --- makedebian.pl | 1 - 1 file changed, 1 deletion(-) diff --git a/makedebian.pl b/makedebian.pl index 3cf04d9b4..1d628b825 100755 --- a/makedebian.pl +++ b/makedebian.pl @@ -128,7 +128,6 @@ EOF if ($product eq "webmin") { print CONTROL < Date: Sun, 14 Jun 2026 16:00:41 +0200 Subject: [PATCH 14/16] Add module access helpers --- acl/acl-lib.pl | 128 ++++++++++++++++++++++++++++++++++++++++++++++ acl/t/run-tests.t | 116 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+) diff --git a/acl/acl-lib.pl b/acl/acl-lib.pl index c79b887c1..3c6c414bb 100755 --- a/acl/acl-lib.pl +++ b/acl/acl-lib.pl @@ -1433,6 +1433,134 @@ dbmclose(%sessiondb); return $sid; } +=head2 set_module_access(&modules, enabled, [&users-groups]) + +Grants or revokes Webmin module access for users and groups. The modules +parameter must be an array ref of module names. The enabled flag should be +1 to grant access, or 0 to revoke access. If the users-groups parameter is +not given, all users and groups are updated. Otherwise, it must be an array +ref of usernames and group names. Group names may be prefixed with @ to +target only a group. + +Returns the number of directly updated user and group records. + +=cut +sub set_module_access +{ +my ($mods, $enabled, $usersgroups) = @_; +$mods ||= []; +return 0 if (!@$mods); +my $set_module_access_list = sub { + my ($obj, $key, $addmods) = @_; + $addmods ||= $mods; + my @old = @{$obj->{$key} || []}; + my @new; + if ($enabled) { + @new = &unique(@old, @$addmods); + } + else { + my %remove = map { $_, 1 } @$mods; + @new = grep { !$remove{$_} } @old; + } + return 0 if (join("\0", @old) eq join("\0", @new)); + $obj->{$key} = \@new; + return 1; + }; +my $own_module_updates = sub { + my ($obj, $inherited) = @_; + return $mods if (!$enabled); + return [] if (!@{$obj->{'ownmods'} || []} && !@$inherited); + return [ grep { &indexof($_, @$inherited) < 0 } @$mods ]; + }; +my @users = &list_users(); +my @groups = &list_groups(); +my $all = !defined($usersgroups); +my (%target_user, %target_group); +if (!$all) { + foreach my $ug (@$usersgroups) { + if ($ug =~ /^\@(.*)$/) { + $target_group{$1} = 1; + } + else { + $target_user{$ug} = 1; + $target_group{$ug} = 1; + } + } + } +my $changed = 0; +my (%user_group, %group_parent); +foreach my $g (@groups) { + foreach my $m (@{$g->{'members'} || []}) { + if ($m =~ /^\@(.*)$/) { + $group_parent{$1} = $g; + } + else { + $user_group{$m} = $g; + } + } + } + +# Update groups first, so member users and sub-groups inherit the new set +foreach my $g (@groups) { + next if (!$all && !$target_group{$g->{'name'}}); + my $gchanged = 0; + my $parent = $group_parent{$g->{'name'}}; + my $ownmods = $own_module_updates->( + $g, [ @{$parent ? $parent->{'modules'} || [] : []} ]); + $gchanged += $set_module_access_list->($g, "modules"); + $gchanged += $set_module_access_list->($g, "ownmods", $ownmods); + if ($gchanged) { + &modify_group($g->{'name'}, $g); + &update_members(\@users, \@groups, $g->{'modules'}, + $g->{'members'}); + $changed++; + } + } + +# Update directly targeted users +foreach my $u (@users) { + next if (!$all && !$target_user{$u->{'name'}}); + my $uchanged = 0; + my $group = $user_group{$u->{'name'}}; + my $ownmods = $own_module_updates->( + $u, [ @{$group ? $group->{'modules'} || [] : []} ]); + $uchanged += $set_module_access_list->($u, "modules"); + $uchanged += $set_module_access_list->($u, "ownmods", $ownmods); + if ($uchanged) { + &modify_user($u->{'name'}, $u); + $changed++; + } + } + +if ($changed) { + undef(%main::acl_hash_cache); + undef(%main::acl_array_cache); + } +return $changed; +} + +=head2 enable_module_access(&modules, [&users-groups]) + +Grants users and groups access to one or more modules. This is a wrapper +around set_module_access. + +=cut +sub enable_module_access +{ +return &set_module_access($_[0], 1, $_[1]); +} + +=head2 disable_module_access(&modules, [&users-groups]) + +Revokes users and groups access to one or more modules. This is a wrapper +around set_module_access. + +=cut +sub disable_module_access +{ +return &set_module_access($_[0], 0, $_[1]); +} + =head2 update_members(&allusers, &allgroups, &modules, &members) Update the modules for members users and groups of some group. The parameters diff --git a/acl/t/run-tests.t b/acl/t/run-tests.t index 5f81caae0..0188e44e8 100644 --- a/acl/t/run-tests.t +++ b/acl/t/run-tests.t @@ -701,6 +701,122 @@ is(group_line({ name => 'empty' }), 'delete_group removes the only group'); } +# set_module_access and wrappers: batch enable/disable module visibility for +# users and groups, while keeping ownmods in sync for future group refreshes. +{ + _reset_fixture(); + create_user({ name => 'alice', + pass => 'x', + modules => [ 'useradmin', 'apache' ], + ownmods => [ 'apache' ] }); + create_group({ name => 'wheel', + members => [ 'alice' ], + modules => [ 'useradmin', 'apache' ], + desc => 'Sysadmins', + ownmods => [ 'apache' ] }); + _clear_caches(); + + is(disable_module_access([ 'apache' ]), 2, + 'disable_module_access without targets updates all users and groups'); + + my $alice = get_user('alice'); + my $wheel = get_group('wheel'); + is_deeply($alice->{'modules'}, [ 'useradmin' ], + 'global disable removes module from user modules'); + is_deeply($alice->{'ownmods'}, [], + 'global disable removes module from user ownmods'); + is_deeply($wheel->{'modules'}, [ 'useradmin' ], + 'global disable removes module from group modules'); + is_deeply($wheel->{'ownmods'}, [], + 'global disable removes module from group ownmods'); +} + +{ + _reset_fixture(); + create_user({ name => 'alice', + pass => 'x', + modules => [ 'useradmin' ] }); + create_group({ name => 'wheel', + members => [ 'alice' ], + modules => [ 'useradmin' ], + desc => 'Sysadmins' }); + _clear_caches(); + + is(enable_module_access([ 'apache' ], [ '@wheel' ]), 1, + 'enable_module_access can target one group by @name'); + + my $alice = get_user('alice'); + my $wheel = get_group('wheel'); + is_deeply($wheel->{'modules'}, [ 'useradmin', 'apache' ], + 'targeted group enable adds module to group modules'); + is_deeply($wheel->{'ownmods'}, [], + 'targeted group enable leaves top-level group ownmods unchanged'); + is_deeply($alice->{'modules'}, [ 'useradmin', 'apache' ], + 'targeted group enable propagates to member user modules'); + is_deeply($alice->{'ownmods'}, [], + 'targeted group enable leaves member user ownmods unchanged'); + + is(disable_module_access([ 'apache' ], [ '@wheel' ]), 1, + 'disable_module_access can target one group by @name'); + + $alice = get_user('alice'); + $wheel = get_group('wheel'); + is_deeply($wheel->{'modules'}, [ 'useradmin' ], + 'targeted group disable removes module from group modules'); + is_deeply($wheel->{'ownmods'}, [], + 'targeted group disable removes module from group ownmods'); + is_deeply($alice->{'modules'}, [ 'useradmin' ], + 'targeted group disable propagates to member user modules'); +} + +{ + _reset_fixture(); + create_user({ name => 'root', + pass => 'x', + modules => [ 'useradmin' ] }); + _clear_caches(); + + is(enable_module_access([ 'apache' ], [ 'root' ]), 1, + 'enable_module_access can target a standalone user'); + my $root = get_user('root'); + is_deeply($root->{'modules'}, [ 'useradmin', 'apache' ], + 'standalone user enable adds module to user modules'); + is_deeply($root->{'ownmods'}, [], + 'standalone user enable does not create ownmods'); +} + +{ + _reset_fixture(); + create_user({ name => 'bob', + pass => 'x', + modules => [ 'useradmin' ] }); + create_group({ name => 'wheel', + members => [ 'bob' ], + modules => [ 'useradmin' ], + desc => 'Sysadmins' }); + _clear_caches(); + + is(enable_module_access([ 'apache' ], [ 'bob' ]), 1, + 'enable_module_access can target one user by name'); + + my $bob = get_user('bob'); + is_deeply($bob->{'modules'}, [ 'useradmin', 'apache' ], + 'targeted user enable adds module to user modules'); + is_deeply($bob->{'ownmods'}, [ 'apache' ], + 'targeted user enable records explicit user ownmod'); + + # A later group refresh must not erase a directly granted user module. + my @users = list_users(); + my @groups = list_groups(); + my ($wheel) = grep { $_->{'name'} eq 'wheel' } @groups; + update_members(\@users, \@groups, $wheel->{'modules'}, + $wheel->{'members'}); + _clear_caches(); + $bob = get_user('bob'); + is_deeply($bob->{'modules'}, [ 'useradmin', 'apache' ], + 'targeted user enable survives later group refresh'); +} + # copy_acl_files (file mode): copy a module ACL file from one name to another. { _reset_fixture(); From de57c42f4a9644db90465637e90cc6454e645434 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sun, 14 Jun 2026 16:14:21 +0200 Subject: [PATCH 15/16] Fix to update nested group module access in parent order --- acl/acl-lib.pl | 17 +++++++++++++++-- acl/t/run-tests.t | 30 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/acl/acl-lib.pl b/acl/acl-lib.pl index 3c6c414bb..9d465e6ec 100755 --- a/acl/acl-lib.pl +++ b/acl/acl-lib.pl @@ -1499,9 +1499,23 @@ foreach my $g (@groups) { } } } +my (@ordered_groups, %ordered_group, %ordering_group); +my $add_ordered_group; +$add_ordered_group = sub { + my ($g) = @_; + return if (!$g || $ordered_group{$g->{'name'}}); + return if ($ordering_group{$g->{'name'}}++); + $add_ordered_group->($group_parent{$g->{'name'}}); + delete($ordering_group{$g->{'name'}}); + push(@ordered_groups, $g); + $ordered_group{$g->{'name'}}++; + }; +foreach my $g (@groups) { + $add_ordered_group->($g); + } # Update groups first, so member users and sub-groups inherit the new set -foreach my $g (@groups) { +foreach my $g (@ordered_groups) { next if (!$all && !$target_group{$g->{'name'}}); my $gchanged = 0; my $parent = $group_parent{$g->{'name'}}; @@ -2496,4 +2510,3 @@ return $mailbox."\@".join(".", @doms); } 1; - diff --git a/acl/t/run-tests.t b/acl/t/run-tests.t index 0188e44e8..310956382 100644 --- a/acl/t/run-tests.t +++ b/acl/t/run-tests.t @@ -769,6 +769,36 @@ is(group_line({ name => 'empty' }), 'targeted group disable propagates to member user modules'); } +{ + _reset_fixture(); + create_user({ name => 'carol', + pass => 'x', + modules => [ 'useradmin' ] }); + create_group({ name => 'child', + members => [ 'carol' ], + modules => [ 'useradmin' ], + desc => 'Child group' }); + create_group({ name => 'parent', + members => [ '@child' ], + modules => [ 'useradmin' ], + desc => 'Parent group' }); + _clear_caches(); + + is(enable_module_access([ 'apache' ]), 1, + 'enable_module_access handles all nested groups'); + + my $child = get_group('child'); + my $carol = get_user('carol'); + is_deeply($child->{'modules'}, [ 'useradmin', 'apache' ], + 'child group inherits module from parent enabled later in file'); + is_deeply($child->{'ownmods'}, [], + 'child group does not record inherited module as ownmod'); + is_deeply($carol->{'modules'}, [ 'useradmin', 'apache' ], + 'child group member inherits module through nested groups'); + is_deeply($carol->{'ownmods'}, [], + 'child group member does not record inherited module as ownmod'); +} + { _reset_fixture(); create_user({ name => 'root', From 5b0b6fbf1f0332301c2c7c95eaf806b263e74fc8 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sun, 14 Jun 2026 17:51:30 +0200 Subject: [PATCH 16/16] Add nftables module to the core too --- makedebian.pl | 2 +- makerpm.pl | 4 ++-- mod_core_list.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/makedebian.pl b/makedebian.pl index 1d628b825..316c619b9 100755 --- a/makedebian.pl +++ b/makedebian.pl @@ -127,7 +127,7 @@ Provides: $baseproduct EOF if ($product eq "webmin") { print CONTROL <