#975 RBAC for port control with new portctl_by_role setting

This commit is contained in:
Oliver Gorwits
2023-06-27 22:52:04 +01:00
parent 8d85562396
commit 2cbb68889a
19 changed files with 139 additions and 41 deletions

View File

@@ -11,7 +11,7 @@ __PACKAGE__->load_namespaces(
);
our # try to hide from kwalitee
$VERSION = 79; # schema version used for upgrades, keep as integer
$VERSION = 80; # schema version used for upgrades, keep as integer
use Path::Class;
use File::ShareDir 'dist_dir';

View File

@@ -27,6 +27,8 @@ __PACKAGE__->add_columns(
{ data_type => "timestamp", is_nullable => 1 },
"port_control",
{ data_type => "boolean", default_value => \"false", is_nullable => 1 },
"portctl_role",
{ data_type => "text", is_nullable => 1 },
"ldap",
{ data_type => "boolean", default_value => \"false", is_nullable => 1 },
"radius",

View File

@@ -63,7 +63,7 @@ sub vlan_reconfig_check {
return;
}
=head2 port_reconfig_check( $port )
=head2 port_reconfig_check( $port, $device?, $user? )
=over 4
@@ -90,14 +90,19 @@ C<$port> has a phone connected.
Permission check that C<portctl_vlans> is true if C<$port> is a vlan
subinterface.
=item *
Permission check on C<portctl_by_role> if the device and user are provided. A
bare username will be promoted to a user instance.
=back
Will return nothing if these checks pass OK.
Will return false if these checks pass OK.
=cut
sub port_reconfig_check {
my $port = shift;
my ($port, $device, $user) = @_;
my $ip = $port->ip;
my $name = $port->port;
@@ -132,7 +137,50 @@ sub port_reconfig_check {
return "forbidden: [$name] is a vlan interface on [$ip]"
if $is_vlan and not setting('portctl_vlans');
return;
# portctl_by_role check
if ($device and ref $device and $user) {
$user = ref $user ? $user :
schema(vars->{'tenant'})->resultset('User')
->find({ username => $user });
my $username = $user->username;
# special case admin user allowed to continue, because
# they can submit port control jobs
return "forbidden: user [$username] has no right to reconfigure ports"
unless ($user->admin or $user->port_control);
my $role = $user->portctl_role;
my $acl = $role ? setting('portctl_by_role')->{$role} : undef;
if ($acl and (ref $acl eq q{} or ref $acl eq ref [])) {
# all ports are permitted when the role acl is a device acl
# but check the device anyway
return "forbidden: user [$username] has no right to reconfigure ports"
unless acl_matches($device, $acl);
}
elsif ($acl and ref $acl eq ref {}) {
my $found = false;
foreach my $key (sort keys %$acl) {
# lhs matches device, rhs matches port
next unless $key and $acl->{$key};
if (acl_matches($device, $key)
and acl_matches($port, $acl->{$key})) {
$found = true;
last;
}
}
return "forbidden: user [$username] role [$role] cannot reconfigure port [$name] on [$ip]"
unless $found;
}
elsif ($role) {
return "forbidden: user [$username] is assigned an unknown role"
unless $user->port_control;
}
}
return false;
}
=head2 get_port( $device, $portname )

View File

@@ -27,6 +27,7 @@ use App::Netdisco::Util::Web qw/
request_is_api_report
request_is_api_search
/;
use App::Netdisco::Util::Permission 'acl_matches';
BEGIN {
no warnings 'redefine';
@@ -138,6 +139,26 @@ if (setting('extra_web_plugins') and ref [] eq ref setting('extra_web_plugins'))
_load_web_plugins( setting('extra_web_plugins') );
}
foreach my $tag (keys %{ setting('_admin_tasks') }) {
my $code = sub {
# trick the ajax into working as if this were a tabbed page
params->{tab} = $tag;
var(nav => 'admin');
template 'admintask', {
task => setting('_admin_tasks')->{ $tag },
}, { layout => 'main' };
};
if (setting('_admin_tasks')->{ $tag }->{ 'roles' }) {
get "/admin/$tag" => require_any_role setting('_admin_tasks')->{ $tag }->{ 'roles' } => $code;
}
else {
get "/admin/$tag" => require_role admin => $code;
}
}
# after plugins are loaded, add our own template path
push @{ config->{engines}->{netdisco_template_toolkit}->{INCLUDE_PATH} },
setting('views');
@@ -269,8 +290,33 @@ hook 'before_template' => sub {
for grep {$_ ne 'return_url'} keys %{params()};
$tokens->{my_query} = $queryuri->query();
# access to logged in user's roles
$tokens->{user_has_role} = sub { user_has_role(@_) };
# access to logged in user's roles (modulo RBAC)
$tokens->{user_has_role} = sub {
my ($role, $device) = @_;
return false unless $role;
return user_has_role($role) if $role ne 'port_control';
return false unless user_has_role('port_control');
return true if not $device;
my $user = logged_in_user or return false;
return true unless $user->portctl_role;
my $acl = setting('portctl_by_role')->{$user->portctl_role};
if ($acl and (ref $acl eq q{} or ref $acl eq ref [])) {
return true if acl_matches($device, $acl);
}
elsif ($acl and ref $acl eq ref {}) {
foreach my $key (grep { defined } sort keys %$acl) {
# lhs matches device, rhs matches port
# but we are not interested in the ports
return true if acl_matches($device, $key);
}
}
# assigned an unknown role
return false;
};
# create date ranges from within templates
$tokens->{to_daterange} = sub { interval_to_daterange(@_) };

View File

@@ -105,23 +105,4 @@ ajax "/ajax/control/admin/snapshot_del" => require_role setting('defanged_admin'
schema(vars->{'tenant'})->resultset('DeviceBrowser')->search({ip => $device->addr})->delete;
};
get '/admin/*' => require_role admin => sub {
my ($tag) = splat;
if (exists setting('_admin_tasks')->{ $tag }) {
# trick the ajax into working as if this were a tabbed page
params->{tab} = $tag;
var(nav => 'admin');
template 'admintask', {
task => setting('_admin_tasks')->{ $tag },
}, { layout => 'main' };
}
else {
var('notfound' => true);
status 'not_found';
template 'index', {}, { layout => 'main' };
}
};
true;

View File

@@ -84,7 +84,7 @@ get '/device' => require_login sub {
params->{'tab'} ||= 'details';
template 'device', {
is_pseudo => $first->is_pseudo,
netdisco_device => $first,
display_name => ($others ? $first->ip : ($first->dns || $first->ip)),
lgroup_list => [ schema(vars->{'tenant'})->resultset('Device')->get_distinct_col('location') ],
hgroup_list => setting('host_group_displaynames'),

View File

@@ -14,6 +14,7 @@ use NetAddr::IP::Lite ':lower';
register_admin_task({
tag => 'topology',
label => 'Manual Device Topology',
roles => [qw/admin port_control/],
});
sub _sanity_ok {

View File

@@ -52,6 +52,10 @@ ajax '/ajax/control/admin/users/add' => require_role setting('defanged_admin') =
)),
port_control => (param('port_control') ? \'true' : \'false'),
portctl_role =>
((param('port_control') and param('port_control') ne '_global_')
? param('port_control') : ''),
admin => (param('admin') ? \'true' : \'false'),
note => param('note'),
});
@@ -92,6 +96,10 @@ ajax '/ajax/control/admin/users/update' => require_role setting('defanged_admin'
)),
port_control => (param('port_control') ? \'true' : \'false'),
portctl_role =>
((param('port_control') and param('port_control') ne '_global_')
? param('port_control') : ''),
admin => (param('admin') ? \'true' : \'false'),
note => param('note'),
});
@@ -110,9 +118,11 @@ get '/ajax/content/admin/users' => require_role admin => sub {
return unless scalar @results;
my @port_control_roles = sort keys %{ setting('portctl_by_role') || {} };
if ( request->is_ajax ) {
template 'ajax/admintask/users.tt',
{ results => \@results, },
{ results => \@results, port_control_roles => \@port_control_roles },
{ layout => undef };
}
else {

View File

@@ -251,7 +251,7 @@ get '/ajax/content/device/ports' => require_login sub {
# add acl on port config
if (param('c_admin') and user_has_role('port_control')) {
map {$_->{portctl} = (port_reconfig_check($_) ? false : true)} @results;
map {$_->{portctl} = (port_reconfig_check($_, $device, logged_in_user) ? false : true)} @results;
}
# empty set would be a 'no records' msg

View File

@@ -17,7 +17,7 @@ register_worker({ phase => 'check' }, sub {
or return Status->error(sprintf "Unknown port name [%s] on device %s",
$job->port, $job->device);
my $port_reconfig_check = port_reconfig_check(vars->{'port'});
my $port_reconfig_check = port_reconfig_check(vars->{'port'}, $job->device, $job->username);
return Status->error("Cannot alter port: $port_reconfig_check")
if $port_reconfig_check;

View File

@@ -19,7 +19,7 @@ register_worker({ phase => 'check' }, sub {
or return Status->error(sprintf "Unknown port name [%s] on device %s",
$job->port, $job->device);
my $port_reconfig_check = port_reconfig_check(vars->{'port'});
my $port_reconfig_check = port_reconfig_check(vars->{'port'}, $job, $job->username);
return Status->error("Cannot alter port: $port_reconfig_check")
if $port_reconfig_check;

View File

@@ -16,7 +16,7 @@ register_worker({ phase => 'check' }, sub {
vars->{'port'} = get_port($device, $pn)
or return Status->error("Unknown port name [$pn] on device $device");
my $port_reconfig_check = port_reconfig_check(vars->{'port'});
my $port_reconfig_check = port_reconfig_check(vars->{'port'}, $device, $job->username);
return Status->error("Cannot alter port: $port_reconfig_check")
if $port_reconfig_check;