#975 RBAC for port control with new portctl_by_role setting
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 )
|
||||
|
||||
@@ -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(@_) };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -251,6 +251,7 @@ portctl_nowaps: false
|
||||
portctl_nophones: false
|
||||
portctl_vlans: false
|
||||
portctl_uplinks: false
|
||||
portctl_by_role: {}
|
||||
system_port_control_reasons:
|
||||
address: 'Address Allocation Abuse'
|
||||
copyright: 'Copyright Violation'
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<tr>
|
||||
<td class="nd_center-cell"><input data-form="add" name="fullname" type="text"></td>
|
||||
<td class="nd_center-cell"><input class="span2" data-form="add" name="username" type="text"></td>
|
||||
<td class="nd_center-cell"><input class="span2" data-form="add" name="password" type="password"></td>
|
||||
<td class="nd_center-cell"><input class="span1" data-form="add" name="password" type="password"></td>
|
||||
<td class="nd_center-cell">
|
||||
<div class="form-group">
|
||||
<select class="span2 form-control" data-form="add" name="auth_method">
|
||||
@@ -49,7 +49,7 @@
|
||||
<input class="span2" data-form="update" name="username" type="text" value="[% row.username | html_entity %]">
|
||||
</td>
|
||||
<td class="nd_center-cell">
|
||||
<input class="span2" data-form="update" name="password" type="password" value="********">
|
||||
<input class="span1" data-form="update" name="password" type="password" value="********">
|
||||
</td>
|
||||
<td class="nd_center-cell">
|
||||
<div class="form-group">
|
||||
@@ -62,7 +62,15 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="nd_center-cell">
|
||||
<input data-form="update" name="port_control" type="checkbox" [% ' checked="checked"' IF row.port_control %]>
|
||||
<div class="form-group">
|
||||
<select class="span2 form-control" data-form="update" name="port_control">
|
||||
<option[% ' selected' IF NOT row.port_control %] value="">Off</option>
|
||||
<option[% ' selected' IF row.port_control AND row.portctl_role == "" %] value="_global_">Enabled (any port)</option>
|
||||
[% FOREACH role IN port_control_roles %]
|
||||
<option[% ' selected' IF row.port_control AND row.portctl_role == role %] value="[% role | html_entity %]">Role: [% role | html_entity %]</option>
|
||||
[% END %]
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td class="nd_center-cell">
|
||||
<input data-form="update" name="admin" type="checkbox" [% ' checked="checked"' IF row.admin %]>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[% USE CSV -%]
|
||||
[% CSV.dump([ 'Full Name' 'Username'
|
||||
'LDAP Auth' 'RADIUS Auth' 'TACACS+ Auth' 'Port Control' 'Administrator' 'Created'
|
||||
'LDAP Auth' 'RADIUS Auth' 'TACACS+ Auth' 'Port Control' 'Port Control Role' 'Administrator' 'Created'
|
||||
'Last Login' 'Note']) %]
|
||||
|
||||
[% FOREACH row IN results %]
|
||||
@@ -11,6 +11,7 @@
|
||||
[% mylist.push(row.radius) %]
|
||||
[% mylist.push(row.tacacs) %]
|
||||
[% mylist.push(row.port_control) %]
|
||||
[% mylist.push(row.portctl_role) %]
|
||||
[% mylist.push(row.admin) %]
|
||||
[% mylist.push(row.created) %]
|
||||
[% mylist.push(row.last_seen) %]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[% SET user_can_port_control = user_has_role('port_control') %]
|
||||
[% SET user_can_port_control = user_has_role('port_control', d) %]
|
||||
<table class="table table-condensed table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[% SET user_can_port_control = user_has_role('port_control') %]
|
||||
[% SET user_can_port_control = user_has_role('port_control', device) %]
|
||||
<table id="dp-data-table" class="table table-bordered table-striped" width="100%" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<li[% ' class="active"' IF params.tab == tab.tag %]><a id="[% tab.tag | html_entity %]_link" href="#[% tab.tag | html_entity %]_pane">[% tab.label | html_entity %]</a></li>
|
||||
[% END %]
|
||||
<span id="nd_device-name">
|
||||
[% IF is_pseudo %]<span class="badge badge-warning">[% END %][% display_name | html_entity %][% IF is_pseudo %]</span>[% END %]
|
||||
[% IF netdisco_device.is_pseudo %]<span class="badge badge-warning">[% END %][% display_name | html_entity %][% IF netdisco_device.is_pseudo %]</span>[% END %]
|
||||
<a id="nd_csv-download" href="#" download="netdisco.csv">
|
||||
<i id="nd_csv-download-icon" class="text-info icon-file-text-alt icon-large"
|
||||
rel="tooltip" data-placement="left" data-offset="5" data-title="Download as CSV"></i></a>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<li><i class="icon-li icon-rss"></i> Wireless Access Point</li>
|
||||
<li><i class="icon-li icon-book"></i> Archived Data</li>
|
||||
<li><i class="icon-li icon-group"></i> Interface Group</li>
|
||||
[% IF user_has_role('port_control') %]
|
||||
[% IF user_has_role('port_control', netdisco_device) %]
|
||||
<li><i class="icon-li icon-refresh icon-spin"></i> Click "Update View"</li>
|
||||
[% END %]
|
||||
</ul>
|
||||
@@ -63,7 +63,7 @@
|
||||
<div id="nd_columns" class="collapse in">
|
||||
<ul class="nd_inputs-list unstyled">
|
||||
[% FOREACH item IN settings.port_columns %]
|
||||
[% NEXT IF item.name == 'c_admin' AND NOT user_has_role('port_control') %]
|
||||
[% NEXT IF item.name == 'c_admin' AND NOT user_has_role('port_control', netdisco_device) %]
|
||||
<li>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="[% item.name | html_entity %]"
|
||||
|
||||
Reference in New Issue
Block a user