From 0c8f74597b7e03c507a9a168305437b37818161b Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sun, 3 May 2026 16:44:26 +0200 Subject: [PATCH] Add proper ACLs to nftables module [no-build] --- nftables/acl_security.pl | 73 +++++++++++++++++++++++ nftables/active.cgi | 18 ++++-- nftables/active_table.cgi | 4 +- nftables/clear_table.cgi | 2 + nftables/clear_tables.cgi | 4 +- nftables/create_table.cgi | 2 + nftables/defaultacl | 14 +++++ nftables/delete_chain.cgi | 2 + nftables/delete_chains.cgi | 2 + nftables/delete_set.cgi | 2 + nftables/delete_sets.cgi | 2 + nftables/delete_table.cgi | 2 + nftables/edit_chain.cgi | 2 + nftables/edit_rule.cgi | 9 ++- nftables/edit_set.cgi | 2 + nftables/import_table.cgi | 3 + nftables/index.cgi | 116 ++++++++++++++++++++++--------------- nftables/lang/en | 19 ++++++ nftables/manage_ip.cgi | 2 + nftables/move_rule.cgi | 2 + nftables/nftables-lib.pl | 55 +++++++++++++++++- nftables/rename_chain.cgi | 2 + nftables/restart.cgi | 1 + nftables/save_chain.cgi | 2 + nftables/save_rule.cgi | 4 ++ nftables/save_set.cgi | 2 + nftables/setup.cgi | 2 + 27 files changed, 291 insertions(+), 59 deletions(-) create mode 100644 nftables/acl_security.pl create mode 100644 nftables/defaultacl diff --git a/nftables/acl_security.pl b/nftables/acl_security.pl new file mode 100644 index 000000000..4c3ee2d0d --- /dev/null +++ b/nftables/acl_security.pl @@ -0,0 +1,73 @@ +use strict; +use warnings; +no warnings 'redefine'; +no warnings 'uninitialized'; + +require 'nftables-lib.pl'; +our (%in, %text); + +# acl_security_form(&options) +# Output HTML for editing security options for the nftables module +sub acl_security_form +{ +my ($o) = @_; + +my $mode = $o->{'tables'} eq '*' ? 1 : + $o->{'tables'} =~ /^\!/ ? 2 : 0; +my @selected = split(/\s+/, $o->{'tables'} || ''); +shift(@selected) if ($mode == 2 && @selected && $selected[0] eq '!'); +my @table_opts = acl_table_options(); + +print ui_table_row($text{'acl_tables'}, + ui_radio("tables_def", $mode, + [ [ 1, $text{'acl_tables_all'} ], + [ 0, $text{'acl_tables_sel'} ], + [ 2, $text{'acl_tables_nsel'} ] ])."
\n". + ui_select("tables", \@selected, \@table_opts, 6, 1), + 3); + +foreach my $a (qw(view active create setup chains sets rules raw delete + apply import clear quick)) { + print ui_table_row($text{'acl_'.$a}, ui_yesno_radio($a, $o->{$a})); + } +} + +# acl_security_save(&options) +# Parse the form for security options for the nftables module +sub acl_security_save +{ +if ($in{'tables_def'} == 1) { + $_[0]->{'tables'} = '*'; + } +elsif ($in{'tables_def'} == 2) { + $_[0]->{'tables'} = join(" ", "!", split(/\0/, $in{'tables'})); + } +else { + $_[0]->{'tables'} = join(" ", split(/\0/, $in{'tables'})); + } +foreach my $a (qw(view active create setup chains sets rules raw delete + apply import clear quick)) { + $_[0]->{$a} = $in{$a} || 0; + } +} + +# acl_table_options() +# Returns saved and active table choices for the ACL editor +sub acl_table_options +{ +my %seen; +my @opts; +foreach my $t (get_nftables_save()) { + push(@opts, [ table_acl_name($t), nft_table_spec($t) ]); + $seen{table_acl_name($t)} = 1; + } +my ($active, $err) = get_active_nftables_save(); +if (!$err) { + foreach my $t (@$active) { + next if ($seen{table_acl_name($t)}++); + push(@opts, [ table_acl_name($t), + nft_table_spec($t)." ($text{'active_title'})" ]); + } + } +return sort { $a->[1] cmp $b->[1] } @opts; +} diff --git a/nftables/active.cgi b/nftables/active.cgi index 369a3bb77..2086e0e7c 100755 --- a/nftables/active.cgi +++ b/nftables/active.cgi @@ -6,6 +6,7 @@ require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%text); +assert_acl('active'); ui_print_header(undef, $text{'active_title'}, "", "intro", 1, 1); @@ -13,10 +14,12 @@ my ($tables, $err) = get_active_nftables_save(); if ($err) { print text('active_failed', $err); } -elsif (!@$tables) { - print "$text{'active_none'}

\n"; -} else { + @$tables = grep { check_table_acl($_) } @$tables; + if (!@$tables) { + print "$text{'active_none'}

\n"; + } + else { my @saved_tables = get_nftables_save(); print ui_columns_start( [ $text{'active_table'}, $text{'active_flags'}, @@ -40,12 +43,13 @@ else { push(@actions, ui_link( "import_table.cgi?family=".urlize($t->{'family'}). "&name=".urlize($t->{'name'}), - $text{'active_import'})) if (!$is_saved); + $text{'active_import'})) + if (!$is_saved && check_acl('import')); push(@actions, ui_link( "clear_table.cgi?family=".urlize($t->{'family'}). "&name=".urlize($t->{'name'}), $text{'active_clear'})) - if (!table_is_externally_managed($t)); + if (!table_is_externally_managed($t) && check_acl('clear')); my $actions = @actions ? join(" ", @actions) : "-"; print ui_columns_row([ ui_link($table_url, html_escape(nft_table_spec($t))), @@ -59,7 +63,8 @@ else { } print ui_columns_end(); - my @clearable = grep { !table_is_externally_managed($_) } @$tables; + my @clearable = grep { !table_is_externally_managed($_) && + check_acl('clear') } @$tables; if (@clearable) { print ui_hr(); print ui_buttons_start(); @@ -68,6 +73,7 @@ else { $text{'active_clear_alldesc'}); print ui_buttons_end(); } + } } ui_print_footer("index.cgi", $text{'index_return'}); diff --git a/nftables/active_table.cgi b/nftables/active_table.cgi index 082aada68..b73a4293a 100755 --- a/nftables/active_table.cgi +++ b/nftables/active_table.cgi @@ -8,6 +8,7 @@ use warnings; our (%in, %text); ReadParse(); error_setup($text{'active_table_err'}); +assert_acl('active'); my ($tables, $err) = get_active_nftables_save(); error(text('active_failed', $err)) if ($err); @@ -20,6 +21,7 @@ foreach my $t (@$tables) { } } $table || error($text{'active_table_notable'}); +assert_table_acl($table); my @saved_tables = get_nftables_save(); my $status_key = active_table_status($table, \@saved_tables); my $is_saved = table_is_webmin_managed($table, \@saved_tables); @@ -32,7 +34,7 @@ print ui_table_row($text{'active_flags'}, html_escape($table->{'flags'} || "-")) print ui_table_row($text{'active_status'}, $text{'active_'.$status_key}); print ui_table_end(); -if (!$is_saved) { +if (!$is_saved && check_acl('import')) { print ui_buttons_start(); print ui_buttons_row( "import_table.cgi?family=".urlize($table->{'family'}). diff --git a/nftables/clear_table.cgi b/nftables/clear_table.cgi index cff049737..95dbee51a 100755 --- a/nftables/clear_table.cgi +++ b/nftables/clear_table.cgi @@ -8,6 +8,7 @@ use warnings; our (%in, %text); ReadParse(); error_setup($text{'clear_err'}); +assert_acl('clear'); my ($tables, $err) = get_active_nftables_save(); error(text('active_failed', $err)) if ($err); @@ -20,6 +21,7 @@ foreach my $t (@$tables) { } } $table || error($text{'active_table_notable'}); +assert_table_acl($table); if ($in{'confirm'}) { $err = delete_active_table($table); diff --git a/nftables/clear_tables.cgi b/nftables/clear_tables.cgi index 6076f1117..5d4b14779 100755 --- a/nftables/clear_tables.cgi +++ b/nftables/clear_tables.cgi @@ -8,11 +8,13 @@ use warnings; our (%in, %text); ReadParse(); error_setup($text{'clear_all_err'}); +assert_acl('clear'); my ($tables, $err) = get_active_nftables_save(); error(text('active_failed', $err)) if ($err); -my @clearable = grep { !table_is_externally_managed($_) } @$tables; +my @clearable = grep { !table_is_externally_managed($_) && + check_table_acl($_) } @$tables; @clearable || error($text{'clear_all_enone'}); if ($in{'confirm'}) { diff --git a/nftables/create_table.cgi b/nftables/create_table.cgi index 0456cc481..f09523237 100755 --- a/nftables/create_table.cgi +++ b/nftables/create_table.cgi @@ -8,6 +8,7 @@ use warnings; our (%in, %text); ReadParse(); error_setup($text{'create_err'}); +assert_acl('create'); my @families = qw(ip ip6 inet arp bridge netdev); my %family_ok = map { $_ => 1 } @families; @@ -40,6 +41,7 @@ if ($in{'create'}) { 'rules' => [], 'chains' => {}, 'sets' => {} }; + assert_table_acl($table); push(@tables, $table); my $err = create_table_configuration($table, @tables); error(text('create_failed', $err)) if ($err); diff --git a/nftables/defaultacl b/nftables/defaultacl new file mode 100644 index 000000000..733016617 --- /dev/null +++ b/nftables/defaultacl @@ -0,0 +1,14 @@ +view=1 +active=1 +tables=* +create=1 +setup=1 +chains=1 +sets=1 +rules=1 +raw=1 +delete=1 +apply=1 +import=1 +clear=1 +quick=1 diff --git a/nftables/delete_chain.cgi b/nftables/delete_chain.cgi index 25b93990b..97a05642a 100644 --- a/nftables/delete_chain.cgi +++ b/nftables/delete_chain.cgi @@ -8,10 +8,12 @@ use warnings; our (%in, %text); ReadParse(); error_setup($text{'delete_chain_err'}); +assert_acl('delete'); my @tables = get_nftables_save(); my $table = $tables[$in{'table'}]; $table || error($text{'chain_notable'}); +assert_table_acl($table); my $chain = $table->{'chains'}->{$in{'chain'}}; $chain || error($text{'chain_nochain'}); diff --git a/nftables/delete_chains.cgi b/nftables/delete_chains.cgi index 291d054e0..36619a956 100755 --- a/nftables/delete_chains.cgi +++ b/nftables/delete_chains.cgi @@ -8,6 +8,7 @@ use warnings; our (%in, %text); ReadParse(); error_setup($text{'delete_chains_err'}); +assert_acl('delete'); my @tables = get_nftables_save(); my $table_idx = $in{'table'}; @@ -24,6 +25,7 @@ if (defined($in{'table_family'}) && defined($in{'table_name'})) { } $table ||= $tables[$table_idx]; $table || error($text{'chain_notable'}); +assert_table_acl($table); my @chains = split(/\0/, $in{'d'} || ""); my %seen; diff --git a/nftables/delete_set.cgi b/nftables/delete_set.cgi index 63b522b53..dfba240bb 100755 --- a/nftables/delete_set.cgi +++ b/nftables/delete_set.cgi @@ -8,10 +8,12 @@ use warnings; our (%in, %text); ReadParse(); error_setup($text{'delete_set_err'}); +assert_acl('delete'); my @tables = get_nftables_save(); my $table = $tables[$in{'table'}]; $table || error($text{'set_notable'}); +assert_table_acl($table); my $set = $table->{'sets'}->{$in{'set'}}; $set || error($text{'set_noset'}); diff --git a/nftables/delete_sets.cgi b/nftables/delete_sets.cgi index e7d2ec187..c1e9a08fc 100755 --- a/nftables/delete_sets.cgi +++ b/nftables/delete_sets.cgi @@ -8,6 +8,7 @@ use warnings; our (%in, %text); ReadParse(); error_setup($text{'delete_sets_err'}); +assert_acl('delete'); my @tables = get_nftables_save(); my $table_idx = $in{'table'}; @@ -24,6 +25,7 @@ if (defined($in{'table_family'}) && defined($in{'table_name'})) { } $table ||= $tables[$table_idx]; $table || error($text{'set_notable'}); +assert_table_acl($table); my @sets = split(/\0/, $in{'s'} || ""); my %seen; diff --git a/nftables/delete_table.cgi b/nftables/delete_table.cgi index 564d3980d..15edc5792 100755 --- a/nftables/delete_table.cgi +++ b/nftables/delete_table.cgi @@ -8,6 +8,7 @@ use warnings; our (%in, %text); ReadParse(); error_setup($text{'delete_err'}); +assert_acl('delete'); my @tables = get_nftables_save(); my $table_idx = $in{'table'}; @@ -27,6 +28,7 @@ else { $table = $tables[$table_idx]; } $table || error($text{'delete_notable'}); +assert_table_acl($table); if ($in{'confirm'}) { my $needs_apply = needs_config_restart(); diff --git a/nftables/edit_chain.cgi b/nftables/edit_chain.cgi index 298498e82..3e17dbe1b 100644 --- a/nftables/edit_chain.cgi +++ b/nftables/edit_chain.cgi @@ -7,10 +7,12 @@ use strict; use warnings; our (%in, %text); ReadParse(); +assert_acl('chains'); my @tables = get_nftables_save(); my $table = $tables[$in{'table'}]; $table || error($text{'chain_notable'}); +assert_table_acl($table); my $chain = { }; my $chain_name = ""; diff --git a/nftables/edit_rule.cgi b/nftables/edit_rule.cgi index f531a2964..f3c45e208 100755 --- a/nftables/edit_rule.cgi +++ b/nftables/edit_rule.cgi @@ -7,8 +7,12 @@ use strict; use warnings; our (%in, %text, %config); ReadParse(); +assert_acl('rules'); +my $can_edit_raw = check_acl('raw'); my @tables = get_nftables_save(); my $table = $tables[$in{'table'}]; +$table || error($text{'move_notable'}); +assert_table_acl($table); my $rule; my $chain_def; my $chain_hook; @@ -301,10 +305,11 @@ print ui_hidden_table_end("advanced"); print ui_table_start($text{'edit_rule'}, "width=100%", 2); # Raw rule (read-only unless edit direct is checked) -my $raw_controls = ui_checkbox("edit_direct", 1, $text{'edit_raw_rule_direct'}, 0); +my $raw_controls = $can_edit_raw ? + ui_checkbox("edit_direct", 1, $text{'edit_raw_rule_direct'}, 0)."
" : ""; my $raw_area = ui_textarea("raw_rule", $rule->{'text'}, 4, 60, undef, undef, "readonly='true'"); -print ui_table_row(hlink($text{'edit_raw_rule'}, "raw_rule"), $raw_controls."
".$raw_area, +print ui_table_row(hlink($text{'edit_raw_rule'}, "raw_rule"), $raw_controls.$raw_area, undef, undef, ["data-column-span='all' data-column-locked='1'"]); print ui_table_end(); diff --git a/nftables/edit_set.cgi b/nftables/edit_set.cgi index bccd95787..312f63a2d 100755 --- a/nftables/edit_set.cgi +++ b/nftables/edit_set.cgi @@ -7,10 +7,12 @@ use strict; use warnings; our (%in, %text); ReadParse(); +assert_acl('sets'); my @tables = get_nftables_save(); my $table = $tables[$in{'table'}]; $table || error($text{'set_notable'}); +assert_table_acl($table); my $set = { }; my $set_name = ""; diff --git a/nftables/import_table.cgi b/nftables/import_table.cgi index 99d012d36..9acdb1fdf 100755 --- a/nftables/import_table.cgi +++ b/nftables/import_table.cgi @@ -9,6 +9,7 @@ use Storable qw(dclone); our (%in, %text); ReadParse(); error_setup($text{'import_err'}); +assert_acl('import'); my ($active, $active_err) = get_active_nftables_save(); error(text('active_failed', $active_err)) if ($active_err); @@ -21,6 +22,7 @@ foreach my $t (@$active) { } } $source || error($text{'import_esource'}); +assert_table_acl($source); my @tables = get_nftables_save(); if (table_is_webmin_managed($source, \@tables)) { @@ -45,6 +47,7 @@ if ($in{'import'}) { my $import = dclone($source); $import->{'name'} = $name; delete($import->{'flags'}); + assert_table_acl($import); push(@tables, $import); write_configuration(@tables); register_managed_table($import, diff --git a/nftables/index.cgi b/nftables/index.cgi index 36f36ab56..f1f02ff78 100755 --- a/nftables/index.cgi +++ b/nftables/index.cgi @@ -7,6 +7,11 @@ use strict; use warnings; our (%in, %text, %config); ReadParse(); +my $can_view_saved = check_acl('view'); +if (!$can_view_saved && !check_acl('active') && !check_acl('create') && + !check_acl('setup')) { + error($text{'acl_ecannot'}); +} my $partial = $in{'partial'}; if (!$partial) { ui_print_header(undef, $text{'index_title'}, "", "intro", 1, 1, @@ -24,16 +29,20 @@ if (!$cmd) { } # Load tables -my @tables = get_nftables_save(); +my @tables = $can_view_saved ? get_nftables_save() : ( ); +@tables = grep { check_table_acl($_) } @tables; my $rules_html = ""; if (!@tables) { $rules_html .= ui_buttons_start(); - $rules_html .= ui_buttons_row("setup.cgi", $text{'index_setup'}, $text{'index_setupdesc'}); + $rules_html .= ui_buttons_row("setup.cgi", $text{'index_setup'}, $text{'index_setupdesc'}) + if (check_acl('setup')); $rules_html .= ui_buttons_row("create_table.cgi", $text{'index_table_create'}, - $text{'index_table_createdesc'}); + $text{'index_table_createdesc'}) + if (check_acl('create')); $rules_html .= ui_buttons_row("active.cgi", $text{'index_active'}, - $text{'index_activedesc'}); + $text{'index_activedesc'}) + if (check_acl('active')); $rules_html .= ui_buttons_end(); } else { # Select table @@ -66,12 +75,13 @@ if (!@tables) { print ui_select("table", $in{'table'}, \@table_opts, 1, 0, 1, 0, "onchange='this.form.querySelector(\"[name=nft_submit]\").click()'"); print ui_submit("", "nft_submit", 0, "style='display:none'"); - print " ", ui_link_button("create_table.cgi", $text{'index_table_create'}); + print " ", ui_link_button("create_table.cgi", $text{'index_table_create'}) + if (check_acl('create')); print " ", ui_link_button( "delete_table.cgi?table=$in{'table'}&table_family=". urlize($tables[$in{'table'}]->{'family'}). "&table_name=".urlize($tables[$in{'table'}]->{'name'}), - $text{'index_table_delete'}); + $text{'index_table_delete'}) if (check_acl('delete')); print "\n"; print ui_form_end(); } @@ -90,14 +100,12 @@ if (!@tables) { my $set_form = $partial ? 1 : 2; my $has_sets = $curr->{'sets'} && ref($curr->{'sets'}) eq 'HASH' && keys(%{$curr->{'sets'}}); - my @set_select_links = $has_sets ? + my @set_select_links = $has_sets && check_acl('delete') ? ( select_all_link("s", $set_form), select_invert_link("s", $set_form) ) : ( ); - my @set_top_links = ( - @set_select_links, - ui_link("edit_set.cgi?table=$in{'table'}&new=1", - $text{'index_set_create'}) - ); + my @set_top_links = @set_select_links; + push(@set_top_links, ui_link("edit_set.cgi?table=$in{'table'}&new=1", + $text{'index_set_create'})) if (check_acl('sets')); $sets_html .= ui_links_row(\@set_top_links); my @set_tds = ( "width=5" ); $sets_html .= ui_columns_start( @@ -107,16 +115,19 @@ if (!@tables) { if ($has_sets) { foreach my $s (sort keys %{$curr->{'sets'}}) { my $set = $curr->{'sets'}->{$s} || { }; - my $actions_html = + my $actions_html = check_acl('sets') ? ui_link("edit_set.cgi?table=$in{'table'}&set=". - urlize($s), $text{'index_set_edit'}); - $sets_html .= ui_checked_columns_row([ + urlize($s), $text{'index_set_edit'}) : "-"; + my @cols = ( $s, $set->{'type'} || "-", $set->{'flags'} || "-", set_elements_summary($set), $actions_html - ], \@set_tds, "s", $s); + ); + $sets_html .= check_acl('delete') ? + ui_checked_columns_row(\@cols, \@set_tds, "s", $s) : + ui_columns_row([ "", @cols ]); } } $sets_html .= ui_columns_end(); @@ -131,14 +142,12 @@ if (!@tables) { $chains_html .= ui_hidden("table_family", $curr->{'family'}); $chains_html .= ui_hidden("table_name", $curr->{'name'}); my $chain_form = $partial ? 0 : 1; - my @chain_select_links = keys(%{$curr->{'chains'}}) ? + my @chain_select_links = keys(%{$curr->{'chains'}}) && check_acl('delete') ? ( select_all_link("d", $chain_form), select_invert_link("d", $chain_form) ) : ( ); - my @chain_top_links = ( - @chain_select_links, - ui_link("edit_chain.cgi?table=$in{'table'}&new=1", - $text{'index_chain_create'}) - ); + my @chain_top_links = @chain_select_links; + push(@chain_top_links, ui_link("edit_chain.cgi?table=$in{'table'}&new=1", + $text{'index_chain_create'})) if (check_acl('chains')); $chains_html .= ui_links_row(\@chain_top_links); my @chain_tds = ( "width=5" ); $chains_html .= ui_columns_start( @@ -165,14 +174,14 @@ if (!@tables) { my $desc = describe_rule($r); my $rule_url = "edit_rule.cgi?table=$in{'table'}&chain=". urlize($c)."&idx=$r->{'index'}"; - my $rule_link = ui_tag('a', $desc, - { 'href' => $rule_url }); + my $rule_link = check_acl('rules') ? + ui_tag('a', $desc, { 'href' => $rule_url }) : $desc; my $imgdir = "@{[get_webprefix()]}/images"; my $up_url = "move_rule.cgi?table=$in{'table'}&chain=". urlize($c)."&idx=$r->{'index'}&dir=up"; my $down_url = "move_rule.cgi?table=$in{'table'}&chain=". urlize($c)."&idx=$r->{'index'}&dir=down"; - my $down_move = $ri < $#rules ? + my $down_move = check_acl('rules') && $ri < $#rules ? ui_tag('a', ui_tag('img', undef, { 'class' => 'ui_up_down_arrows_down', @@ -183,7 +192,7 @@ if (!@tables) { ui_tag('img', undef, { 'class' => 'ui_up_down_arrows_gap', 'src' => "$imgdir/movegap.gif" }); - my $up_move = $ri > 0 ? + my $up_move = check_acl('rules') && $ri > 0 ? ui_tag('a', ui_tag('img', undef, { 'class' => 'ui_up_down_arrows_up', @@ -213,14 +222,18 @@ if (!@tables) { $rules_html_row = ui_tag('i', $text{'index_rules_none'}); } - my $actions_html = - ui_link("edit_chain.cgi?table=$in{'table'}&chain=". - urlize($c), $text{'index_cedit'})." | ". - ui_link("rename_chain.cgi?table=$in{'table'}&chain=". - urlize($c), $text{'index_crename'})." | ". - ui_link("edit_rule.cgi?table=$in{'table'}&chain=". - urlize($c)."&new=1", $text{'index_radd'}); - $chains_html .= ui_checked_columns_row([ + my @actions; + if (check_acl('chains')) { + push(@actions, ui_link("edit_chain.cgi?table=$in{'table'}&chain=". + urlize($c), $text{'index_cedit'})); + push(@actions, ui_link("rename_chain.cgi?table=$in{'table'}&chain=". + urlize($c), $text{'index_crename'})); + } + push(@actions, ui_link("edit_rule.cgi?table=$in{'table'}&chain=". + urlize($c)."&new=1", $text{'index_radd'})) + if (check_acl('rules')); + my $actions_html = @actions ? join(" | ", @actions) : "-"; + my @cols = ( $c, $chain_def->{'type'} || "-", $chain_def->{'hook'} || "-", @@ -228,29 +241,33 @@ if (!@tables) { $policy_label, $rules_html_row, $actions_html - ], \@chain_tds, "d", $c); + ); + $chains_html .= check_acl('delete') ? + ui_checked_columns_row(\@cols, \@chain_tds, "d", $c) : + ui_columns_row([ "", @cols ]); } $chains_html .= ui_columns_end(); $chains_html .= @chain_select_links ? ui_form_end([ [ undef, $text{'index_cdeletesel'} ] ]) : ui_form_end(); - my @tabs = ( - [ 'chains', $text{'index_tab_chains'} ], - [ 'sets', $text{'index_tab_sets'} ], - ); - my $tab = $in{'view'} && $in{'view'} eq 'sets' ? 'sets' : 'chains'; + my @tabs = ( [ 'chains', $text{'index_tab_chains'} ] ); + push(@tabs, [ 'sets', $text{'index_tab_sets'} ]) if (check_acl('sets')); + my $tab = check_acl('sets') && $in{'view'} && $in{'view'} eq 'sets' ? + 'sets' : 'chains'; $rules_html .= ui_hr(); $rules_html .= ui_tabs_start(\@tabs, "view", $tab, 1); $rules_html .= ui_tabs_start_tab("view", "chains"); $rules_html .= $chains_html; $rules_html .= ui_tabs_end_tab(); - $rules_html .= ui_tabs_start_tab("view", "sets"); - $rules_html .= $sets_html; - $rules_html .= ui_tabs_end_tab(); + if (check_acl('sets')) { + $rules_html .= ui_tabs_start_tab("view", "sets"); + $rules_html .= $sets_html; + $rules_html .= ui_tabs_end_tab(); + } $rules_html .= ui_tabs_end(1); - if (find_input_chain($curr)) { + if (check_acl('quick') && find_input_chain($curr)) { my $ip_placeholder = text('quick_ip_placeholder', '1.2.3.4', '2001:db8::1/64'); foreach my $action ( @@ -277,12 +294,15 @@ if ($partial) { print $rules_html; -if (@tables) { +if (@tables && (check_acl('apply') || check_acl('active') || check_acl('setup'))) { print ui_hr(); print ui_buttons_start(); - print ui_buttons_row("restart.cgi", $text{'index_apply'}, $text{'index_applydesc'}); - print ui_buttons_row("active.cgi", $text{'index_active'}, $text{'index_activedesc'}); - print ui_buttons_row("setup.cgi", $text{'index_setup'}, $text{'index_setupdesc'}); + print ui_buttons_row("restart.cgi", $text{'index_apply'}, $text{'index_applydesc'}) + if (check_acl('apply')); + print ui_buttons_row("active.cgi", $text{'index_active'}, $text{'index_activedesc'}) + if (check_acl('active')); + print ui_buttons_row("setup.cgi", $text{'index_setup'}, $text{'index_setupdesc'}) + if (check_acl('setup')); print ui_buttons_end(); } diff --git a/nftables/lang/en b/nftables/lang/en index a953c9430..f68b9e7c1 100644 --- a/nftables/lang/en +++ b/nftables/lang/en @@ -333,3 +333,22 @@ import_flags=Source flags import_external_note=This active table is marked as externally managed; importing creates a separate module-managed copy and does not change the active source table import_new_name=New table name import_ok=Import Copy +acl_ecannot=You are not allowed to perform this nftables action. +acl_etable=You are not allowed to manage table $1. +acl_tables=Tables this user can manage +acl_tables_all=All tables +acl_tables_sel=Selected tables +acl_tables_nsel=All except selected tables +acl_view=View managed tables and rules +acl_active=View active ruleset +acl_create=Create tables +acl_setup=Create ruleset profiles +acl_chains=Create, edit and rename chains +acl_sets=Create and edit sets +acl_rules=Create, edit, move and delete rules +acl_raw=Edit raw rule text +acl_delete=Delete tables, chains and sets +acl_apply=Apply saved configuration +acl_import=Import active tables +acl_clear=Clear active tables +acl_quick=Use quick allow/block controls diff --git a/nftables/manage_ip.cgi b/nftables/manage_ip.cgi index 02badffa3..cec01004e 100755 --- a/nftables/manage_ip.cgi +++ b/nftables/manage_ip.cgi @@ -7,6 +7,7 @@ use strict; use warnings; our (%in, %text); ReadParse(); +assert_acl('quick'); my $action = $in{'allow'} ? 'allow' : $in{'block'} ? 'block' : ''; error_setup($action eq 'allow' ? $text{'quick_allow_err'} : @@ -29,6 +30,7 @@ else { $table = $tables[$table_idx]; } $table || error($text{'quick_etable'}); +assert_table_acl($table); my $err = add_quick_ip_rule($table, $in{'ip'}, $action); error($err) if ($err); diff --git a/nftables/move_rule.cgi b/nftables/move_rule.cgi index a29ee766a..4b3067503 100755 --- a/nftables/move_rule.cgi +++ b/nftables/move_rule.cgi @@ -8,10 +8,12 @@ use warnings; our (%in, %text); ReadParse(); error_setup($text{'move_err'}); +assert_acl('rules'); my @tables = get_nftables_save(); my $table = $tables[$in{'table'}]; $table || error($text{'move_notable'}); +assert_table_acl($table); my $chain = $in{'chain'}; $chain || error($text{'move_nochain'}); diff --git a/nftables/nftables-lib.pl b/nftables/nftables-lib.pl index 633ffb1ab..830122fa2 100644 --- a/nftables/nftables-lib.pl +++ b/nftables/nftables-lib.pl @@ -5,16 +5,69 @@ BEGIN { push(@INC, ".."); }; ## no critic use WebminCore; use strict; use warnings; -our (%config, $module_config_directory, $module_var_directory); +our (%config, %access, $module_config_directory, $module_var_directory); our ($last_config_change_flag, $last_restart_time_flag); init_config(); +%access = get_module_acl(); $last_config_change_flag = $module_var_directory."/config-flag"; $last_restart_time_flag = $module_var_directory."/restart-flag"; +# check_acl(action) +# Returns true if the current Webmin user can perform an action +sub check_acl +{ +my ($action) = @_; +return $access{$action} ? 1 : 0; +} + +# assert_acl(action) +# Fails if the current Webmin user cannot perform an action +sub assert_acl +{ +my ($action) = @_; +check_acl($action) || error(text('acl_ecannot')); +} + +# table_acl_name(&table) +# Returns the ACL token for a table +sub table_acl_name +{ +my ($table) = @_; +return ($table->{'family'} || '').":".($table->{'name'} || ''); +} + +# check_table_acl(&table) +# Returns true if the current Webmin user can manage a table +sub check_table_acl +{ +my ($table) = @_; +return 0 if (!$table); +my $tables = defined($access{'tables'}) ? $access{'tables'} : '*'; +return 1 if ($tables eq '*'); +my $name = table_acl_name($table); +my @tokens = grep { $_ ne '' } split(/\s+/, $tables); +if (@tokens && $tokens[0] eq '!') { + my %deny = map { $_ => 1 } @tokens[1..$#tokens]; + return !$deny{$name}; + } +my %allow = map { $_ => 1 } @tokens; +return $allow{$name} ? 1 : 0; +} + +# assert_table_acl(&table) +# Fails if the current Webmin user cannot manage a table +sub assert_table_acl +{ +my ($table) = @_; +check_table_acl($table) || + error(text('acl_etable', html_escape(nft_table_spec($table)))); +} + # restart_button() # Returns HTML for the header apply button sub restart_button { +return "" if (!check_acl('apply')); my @tables = get_nftables_save(); return "" if (!@tables); my $args = "redir=".urlize(this_url()); diff --git a/nftables/rename_chain.cgi b/nftables/rename_chain.cgi index 011d62806..55ed5edf1 100644 --- a/nftables/rename_chain.cgi +++ b/nftables/rename_chain.cgi @@ -7,10 +7,12 @@ use strict; use warnings; our (%in, %text); ReadParse(); +assert_acl('chains'); my @tables = get_nftables_save(); my $table = $tables[$in{'table'}]; $table || error($text{'chain_notable'}); +assert_table_acl($table); my $chain = $table->{'chains'}->{$in{'chain'}}; $chain || error($text{'chain_nochain'}); diff --git a/nftables/restart.cgi b/nftables/restart.cgi index 0ae1f1886..c8301072b 100755 --- a/nftables/restart.cgi +++ b/nftables/restart.cgi @@ -8,6 +8,7 @@ use warnings; our (%in, %text); ReadParse(); error_setup($text{'apply_err'}); +assert_acl('apply'); my $err = apply_restore(); error($err) if ($err); diff --git a/nftables/save_chain.cgi b/nftables/save_chain.cgi index c3280d0b9..9bfa1841a 100644 --- a/nftables/save_chain.cgi +++ b/nftables/save_chain.cgi @@ -8,10 +8,12 @@ use warnings; our (%in, %text); ReadParse(); error_setup($text{'chain_err'}); +assert_acl('chains'); my @tables = get_nftables_save(); my $table = $tables[$in{'table'}]; $table || error($text{'chain_notable'}); +assert_table_acl($table); my $is_new = $in{'new'} ? 1 : 0; my $is_rename = $in{'rename'} ? 1 : 0; diff --git a/nftables/save_rule.cgi b/nftables/save_rule.cgi index cb2e0461e..264738515 100755 --- a/nftables/save_rule.cgi +++ b/nftables/save_rule.cgi @@ -8,8 +8,12 @@ use warnings; our (%in, %text); ReadParse(); error_setup($text{'save_err'}); +assert_acl('rules'); +assert_acl('raw') if ($in{'edit_direct'}); my @tables = get_nftables_save(); my $table = $tables[$in{'table'}]; +$table || error($text{'move_notable'}); +assert_table_acl($table); foreach my $sfield (qw(saddr_set daddr_set sport_set dport_set)) { if ($in{$sfield}) { diff --git a/nftables/save_set.cgi b/nftables/save_set.cgi index ff5f9267e..d9a4d6ed8 100755 --- a/nftables/save_set.cgi +++ b/nftables/save_set.cgi @@ -8,10 +8,12 @@ use warnings; our (%in, %text); ReadParse(); error_setup($text{'set_err'}); +assert_acl('sets'); my @tables = get_nftables_save(); my $table = $tables[$in{'table'}]; $table || error($text{'set_notable'}); +assert_table_acl($table); my $is_new = $in{'new'} ? 1 : 0; my $name = $in{'set_name'}; diff --git a/nftables/setup.cgi b/nftables/setup.cgi index 480416069..68ff5efdd 100644 --- a/nftables/setup.cgi +++ b/nftables/setup.cgi @@ -8,6 +8,7 @@ use warnings; our (%in, %text); ReadParse(); error_setup($text{'setup_err'}); +assert_acl('setup'); if ($in{'action'} eq 'create') { my $profile = $in{'profile'} || 'virtualmin'; my $table_name = $in{'table_name'} || default_profile_table_name(); @@ -31,6 +32,7 @@ if ($in{'action'} eq 'create') { my @allow = grep { $_ ne '' } split(/\0/, $in{'allow'} || ''); my $table = create_profile_ruleset($profile, $table_name, \@allow); + assert_table_acl($table); push(@tables, $table); my $error = save_configuration(@tables);