#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
|
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 Path::Class;
|
||||||
use File::ShareDir 'dist_dir';
|
use File::ShareDir 'dist_dir';
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ __PACKAGE__->add_columns(
|
|||||||
{ data_type => "timestamp", is_nullable => 1 },
|
{ data_type => "timestamp", is_nullable => 1 },
|
||||||
"port_control",
|
"port_control",
|
||||||
{ data_type => "boolean", default_value => \"false", is_nullable => 1 },
|
{ data_type => "boolean", default_value => \"false", is_nullable => 1 },
|
||||||
|
"portctl_role",
|
||||||
|
{ data_type => "text", is_nullable => 1 },
|
||||||
"ldap",
|
"ldap",
|
||||||
{ data_type => "boolean", default_value => \"false", is_nullable => 1 },
|
{ data_type => "boolean", default_value => \"false", is_nullable => 1 },
|
||||||
"radius",
|
"radius",
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ sub vlan_reconfig_check {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
=head2 port_reconfig_check( $port )
|
=head2 port_reconfig_check( $port, $device?, $user? )
|
||||||
|
|
||||||
=over 4
|
=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
|
Permission check that C<portctl_vlans> is true if C<$port> is a vlan
|
||||||
subinterface.
|
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
|
=back
|
||||||
|
|
||||||
Will return nothing if these checks pass OK.
|
Will return false if these checks pass OK.
|
||||||
|
|
||||||
=cut
|
=cut
|
||||||
|
|
||||||
sub port_reconfig_check {
|
sub port_reconfig_check {
|
||||||
my $port = shift;
|
my ($port, $device, $user) = @_;
|
||||||
my $ip = $port->ip;
|
my $ip = $port->ip;
|
||||||
my $name = $port->port;
|
my $name = $port->port;
|
||||||
|
|
||||||
@@ -132,7 +137,50 @@ sub port_reconfig_check {
|
|||||||
return "forbidden: [$name] is a vlan interface on [$ip]"
|
return "forbidden: [$name] is a vlan interface on [$ip]"
|
||||||
if $is_vlan and not setting('portctl_vlans');
|
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 )
|
=head2 get_port( $device, $portname )
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ use App::Netdisco::Util::Web qw/
|
|||||||
request_is_api_report
|
request_is_api_report
|
||||||
request_is_api_search
|
request_is_api_search
|
||||||
/;
|
/;
|
||||||
|
use App::Netdisco::Util::Permission 'acl_matches';
|
||||||
|
|
||||||
BEGIN {
|
BEGIN {
|
||||||
no warnings 'redefine';
|
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') );
|
_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
|
# after plugins are loaded, add our own template path
|
||||||
push @{ config->{engines}->{netdisco_template_toolkit}->{INCLUDE_PATH} },
|
push @{ config->{engines}->{netdisco_template_toolkit}->{INCLUDE_PATH} },
|
||||||
setting('views');
|
setting('views');
|
||||||
@@ -269,8 +290,33 @@ hook 'before_template' => sub {
|
|||||||
for grep {$_ ne 'return_url'} keys %{params()};
|
for grep {$_ ne 'return_url'} keys %{params()};
|
||||||
$tokens->{my_query} = $queryuri->query();
|
$tokens->{my_query} = $queryuri->query();
|
||||||
|
|
||||||
# access to logged in user's roles
|
# access to logged in user's roles (modulo RBAC)
|
||||||
$tokens->{user_has_role} = sub { user_has_role(@_) };
|
$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
|
# create date ranges from within templates
|
||||||
$tokens->{to_daterange} = sub { interval_to_daterange(@_) };
|
$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;
|
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;
|
true;
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ get '/device' => require_login sub {
|
|||||||
|
|
||||||
params->{'tab'} ||= 'details';
|
params->{'tab'} ||= 'details';
|
||||||
template 'device', {
|
template 'device', {
|
||||||
is_pseudo => $first->is_pseudo,
|
netdisco_device => $first,
|
||||||
display_name => ($others ? $first->ip : ($first->dns || $first->ip)),
|
display_name => ($others ? $first->ip : ($first->dns || $first->ip)),
|
||||||
lgroup_list => [ schema(vars->{'tenant'})->resultset('Device')->get_distinct_col('location') ],
|
lgroup_list => [ schema(vars->{'tenant'})->resultset('Device')->get_distinct_col('location') ],
|
||||||
hgroup_list => setting('host_group_displaynames'),
|
hgroup_list => setting('host_group_displaynames'),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use NetAddr::IP::Lite ':lower';
|
|||||||
register_admin_task({
|
register_admin_task({
|
||||||
tag => 'topology',
|
tag => 'topology',
|
||||||
label => 'Manual Device Topology',
|
label => 'Manual Device Topology',
|
||||||
|
roles => [qw/admin port_control/],
|
||||||
});
|
});
|
||||||
|
|
||||||
sub _sanity_ok {
|
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'),
|
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'),
|
admin => (param('admin') ? \'true' : \'false'),
|
||||||
note => param('note'),
|
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'),
|
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'),
|
admin => (param('admin') ? \'true' : \'false'),
|
||||||
note => param('note'),
|
note => param('note'),
|
||||||
});
|
});
|
||||||
@@ -110,9 +118,11 @@ get '/ajax/content/admin/users' => require_role admin => sub {
|
|||||||
|
|
||||||
return unless scalar @results;
|
return unless scalar @results;
|
||||||
|
|
||||||
|
my @port_control_roles = sort keys %{ setting('portctl_by_role') || {} };
|
||||||
|
|
||||||
if ( request->is_ajax ) {
|
if ( request->is_ajax ) {
|
||||||
template 'ajax/admintask/users.tt',
|
template 'ajax/admintask/users.tt',
|
||||||
{ results => \@results, },
|
{ results => \@results, port_control_roles => \@port_control_roles },
|
||||||
{ layout => undef };
|
{ layout => undef };
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ get '/ajax/content/device/ports' => require_login sub {
|
|||||||
|
|
||||||
# add acl on port config
|
# add acl on port config
|
||||||
if (param('c_admin') and user_has_role('port_control')) {
|
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
|
# 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",
|
or return Status->error(sprintf "Unknown port name [%s] on device %s",
|
||||||
$job->port, $job->device);
|
$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")
|
return Status->error("Cannot alter port: $port_reconfig_check")
|
||||||
if $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",
|
or return Status->error(sprintf "Unknown port name [%s] on device %s",
|
||||||
$job->port, $job->device);
|
$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")
|
return Status->error("Cannot alter port: $port_reconfig_check")
|
||||||
if $port_reconfig_check;
|
if $port_reconfig_check;
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ register_worker({ phase => 'check' }, sub {
|
|||||||
vars->{'port'} = get_port($device, $pn)
|
vars->{'port'} = get_port($device, $pn)
|
||||||
or return Status->error("Unknown port name [$pn] on device $device");
|
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")
|
return Status->error("Cannot alter port: $port_reconfig_check")
|
||||||
if $port_reconfig_check;
|
if $port_reconfig_check;
|
||||||
|
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ portctl_nowaps: false
|
|||||||
portctl_nophones: false
|
portctl_nophones: false
|
||||||
portctl_vlans: false
|
portctl_vlans: false
|
||||||
portctl_uplinks: false
|
portctl_uplinks: false
|
||||||
|
portctl_by_role: {}
|
||||||
system_port_control_reasons:
|
system_port_control_reasons:
|
||||||
address: 'Address Allocation Abuse'
|
address: 'Address Allocation Abuse'
|
||||||
copyright: 'Copyright Violation'
|
copyright: 'Copyright Violation'
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="nd_center-cell"><input data-form="add" name="fullname" type="text"></td>
|
<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="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">
|
<td class="nd_center-cell">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<select class="span2 form-control" data-form="add" name="auth_method">
|
<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 %]">
|
<input class="span2" data-form="update" name="username" type="text" value="[% row.username | html_entity %]">
|
||||||
</td>
|
</td>
|
||||||
<td class="nd_center-cell">
|
<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>
|
||||||
<td class="nd_center-cell">
|
<td class="nd_center-cell">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -62,7 +62,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="nd_center-cell">
|
<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>
|
||||||
<td class="nd_center-cell">
|
<td class="nd_center-cell">
|
||||||
<input data-form="update" name="admin" type="checkbox" [% ' checked="checked"' IF row.admin %]>
|
<input data-form="update" name="admin" type="checkbox" [% ' checked="checked"' IF row.admin %]>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[% USE CSV -%]
|
[% USE CSV -%]
|
||||||
[% CSV.dump([ 'Full Name' 'Username'
|
[% 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']) %]
|
'Last Login' 'Note']) %]
|
||||||
|
|
||||||
[% FOREACH row IN results %]
|
[% FOREACH row IN results %]
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
[% mylist.push(row.radius) %]
|
[% mylist.push(row.radius) %]
|
||||||
[% mylist.push(row.tacacs) %]
|
[% mylist.push(row.tacacs) %]
|
||||||
[% mylist.push(row.port_control) %]
|
[% mylist.push(row.port_control) %]
|
||||||
|
[% mylist.push(row.portctl_role) %]
|
||||||
[% mylist.push(row.admin) %]
|
[% mylist.push(row.admin) %]
|
||||||
[% mylist.push(row.created) %]
|
[% mylist.push(row.created) %]
|
||||||
[% mylist.push(row.last_seen) %]
|
[% 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">
|
<table class="table table-condensed table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<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">
|
<table id="dp-data-table" class="table table-bordered table-striped" width="100%" cellspacing="0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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>
|
<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 %]
|
[% END %]
|
||||||
<span id="nd_device-name">
|
<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">
|
<a id="nd_csv-download" href="#" download="netdisco.csv">
|
||||||
<i id="nd_csv-download-icon" class="text-info icon-file-text-alt icon-large"
|
<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>
|
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-rss"></i> Wireless Access Point</li>
|
||||||
<li><i class="icon-li icon-book"></i> Archived Data</li>
|
<li><i class="icon-li icon-book"></i> Archived Data</li>
|
||||||
<li><i class="icon-li icon-group"></i> Interface Group</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>
|
<li><i class="icon-li icon-refresh icon-spin"></i> Click "Update View"</li>
|
||||||
[% END %]
|
[% END %]
|
||||||
</ul>
|
</ul>
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
<div id="nd_columns" class="collapse in">
|
<div id="nd_columns" class="collapse in">
|
||||||
<ul class="nd_inputs-list unstyled">
|
<ul class="nd_inputs-list unstyled">
|
||||||
[% FOREACH item IN settings.port_columns %]
|
[% 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>
|
<li>
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
<input type="checkbox" id="[% item.name | html_entity %]"
|
<input type="checkbox" id="[% item.name | html_entity %]"
|
||||||
|
|||||||
Reference in New Issue
Block a user