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);