Files
netdisco/lib/App/Netdisco/Web.pm
Oliver Gorwits dff26abc5c API implementation (#712)
* initial v0 creator

* working json api for generic reports

* add require login

* move report swagger into plugin, and set new default layout of noop

* require proper role and also use new util func

* start to tidy authn

* some work on cleaning up web authn

* clean up the authN checks

* fix bug

* fix the auth for api

* fixes to json handling

* set swagger sort order

* enable most reports for api endpoints

* fix doc

* add paramters to reports

* add missed report

* allow api_parameters in reports config

* reorganise api

* add vlan search

* add port search

* make sure to enable layout processing

* add device search

* add v1 to api paths

* add Node Search

* support api_responses

* add device object search; fix spurious ports field in device result class

* handle some plugins just returning undef if search fails

* errors from api seamlessley

* fix error in date range default

* more sensible default for prefix

* change order of endpoints in swagger-ui

* all db row classes can now TO_JSON

* add device_port api endpoint

* add device ports endpoint

* do not expand docs

* add swagger ui json tree formatter

* add all relations from Device table

* add port relations

* add nodes retrieve on device or vlan

* rename to GetAPIKey

* update config for previous commit
2020-04-15 21:15:52 +01:00

319 lines
10 KiB
Perl
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package App::Netdisco::Web;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use Dancer::Plugin::Swagger;
use Dancer::Error;
use Dancer::Continuation::Route::ErrorSent;
use URI ();
use Socket6 (); # to ensure dependency is met
use HTML::Entities (); # to ensure dependency is met
use URI::QueryParam (); # part of URI, to add helper methods
use Path::Class 'dir';
use Module::Load ();
use App::Netdisco::Util::Web qw/
interval_to_daterange
request_is_api
request_is_api_report
request_is_api_search
/;
BEGIN {
# https://github.com/PerlDancer/Dancer/issues/967
no warnings 'redefine';
*Dancer::_redirect = sub {
my ($destination, $status) = @_;
my $response = Dancer::SharedData->response;
$response->status($status || 302);
$response->headers('Location' => $destination);
};
# neater than using Dancer::Plugin::Res to handle JSON differently
*Dancer::send_error = sub {
my ($body, $status) = @_;
if (request_is_api) {
status $status || 400;
$body = '' unless defined $body;
Dancer::Continuation::Route::ErrorSent->new(
return_value => to_json { error => $body, return_url => param('return_url') }
)->throw;
}
Dancer::Continuation::Route::ErrorSent->new(
return_value => Dancer::Error->new(
message => $body,
code => $status || 500)->render()
)->throw;
};
}
use App::Netdisco::Web::AuthN;
use App::Netdisco::Web::Static;
use App::Netdisco::Web::Search;
use App::Netdisco::Web::Device;
use App::Netdisco::Web::Report;
use App::Netdisco::Web::API::Objects;
use App::Netdisco::Web::AdminTask;
use App::Netdisco::Web::TypeAhead;
use App::Netdisco::Web::PortControl;
use App::Netdisco::Web::Statistics;
use App::Netdisco::Web::Password;
use App::Netdisco::Web::GenericReport;
sub _load_web_plugins {
my $plugin_list = shift;
foreach my $plugin (@$plugin_list) {
$plugin =~ s/^X::/+App::NetdiscoX::Web::Plugin::/;
$plugin = 'App::Netdisco::Web::Plugin::'. $plugin
if $plugin !~ m/^\+/;
$plugin =~ s/^\+//;
$ENV{ND2_LOG_PLUGINS} && debug "loading web plugin $plugin";
Module::Load::load $plugin;
}
}
if (setting('web_plugins') and ref [] eq ref setting('web_plugins')) {
_load_web_plugins( setting('web_plugins') );
}
if (setting('extra_web_plugins') and ref [] eq ref setting('extra_web_plugins')) {
unshift @INC, dir(($ENV{NETDISCO_HOME} || $ENV{HOME}), 'site_plugins')->stringify;
_load_web_plugins( setting('extra_web_plugins') );
}
# after plugins are loaded, add our own template path
push @{ config->{engines}->{netdisco_template_toolkit}->{INCLUDE_PATH} },
setting('views');
# any template paths in deployment.yml (should override plugins)
if (setting('template_paths') and ref [] eq ref setting('template_paths')) {
if (setting('site_local_files')) {
push @{setting('template_paths')},
dir(($ENV{NETDISCO_HOME} || $ENV{HOME}), 'nd-site-local', 'share')->stringify,
dir(($ENV{NETDISCO_HOME} || $ENV{HOME}), 'nd-site-local', 'share', 'views')->stringify;
}
unshift @{ config->{engines}->{netdisco_template_toolkit}->{INCLUDE_PATH} },
@{setting('template_paths')};
}
# load cookie key from database
setting('session_cookie_key' => undef);
setting('session_cookie_key' => 'this_is_for_testing_only')
if $ENV{HARNESS_ACTIVE};
eval {
my $sessions = schema('netdisco')->resultset('Session');
my $skey = $sessions->find({id => 'dancer_session_cookie_key'});
setting('session_cookie_key' => $skey->get_column('a_session')) if $skey;
};
Dancer::Session::Cookie::init(session);
# setup for swagger API
my $swagger = Dancer::Plugin::Swagger->instance->doc;
$swagger->{schemes} = ['http','https'];
$swagger->{consumes} = 'application/json';
$swagger->{produces} = 'application/json';
$swagger->{tags} = [
{name => 'General',
description => 'Log in and Log out'},
{name => 'Search',
description => 'Search Operations'},
{name => 'Objects',
description => 'Retrieve Device, Port, and associated Node Data'},
{name => 'Reports',
description => 'Canned and Custom Reports'},
];
$swagger->{securityDefinitions} = {
APIKeyHeader =>
{ type => 'apiKey', name => 'Authorization', in => 'header' },
BasicAuth =>
{ type => 'basic' },
};
$swagger->{security} = [ { APIKeyHeader => [] } ];
# workaround for https://github.com/PerlDancer/Dancer/issues/935
hook after_error_render => sub { setting('layout' => 'main') };
# build list of port detail columns
{
my @port_columns =
sort { $a->{idx} <=> $b->{idx} }
map {{ name => $_, %{ setting('sidebar_defaults')->{'device_ports'}->{$_} } }}
grep { $_ =~ m/^c_/ } keys %{ setting('sidebar_defaults')->{'device_ports'} };
splice @port_columns, setting('device_port_col_idx_left'), 0,
grep {$_->{position} eq 'left'} @{ setting('_extra_device_port_cols') };
splice @port_columns, setting('device_port_col_idx_mid'), 0,
grep {$_->{position} eq 'mid'} @{ setting('_extra_device_port_cols') };
splice @port_columns, setting('device_port_col_idx_right'), 0,
grep {$_->{position} eq 'right'} @{ setting('_extra_device_port_cols') };
set('port_columns' => \@port_columns);
# update sidebar_defaults so hooks scanning params see new plugin cols
setting('sidebar_defaults')->{'device_ports'}->{ $_->{name} } = $_
for @port_columns;
}
hook 'before' => sub {
my $key = request->path;
if (param('tab') and ($key !~ m/ajax/)) {
$key .= ('/' . param('tab'));
}
$key =~ s|.*/(\w+)/(\w+)$|${1}_${2}|;
var(sidebar_key => $key);
# copy sidebar defaults into vars so we can mess about with it
foreach my $sidebar (keys %{setting('sidebar_defaults')}) {
vars->{'sidebar_defaults'}->{$sidebar} = { map {
($_ => setting('sidebar_defaults')->{$sidebar}->{$_}->{'default'})
} keys %{setting('sidebar_defaults')->{$sidebar}} };
}
};
hook 'before_template' => sub {
# search or report from navbar, or reset of sidebar, can ignore params
return if param('firstsearch')
or var('sidebar_key') !~ m/^\w+_\w+$/;
# update defaults to contain the passed url params
# (this follows initial copy from config.yml, then cookie restore)
var('sidebar_defaults')->{var('sidebar_key')}->{$_} = param($_)
for keys %{ var('sidebar_defaults')->{var('sidebar_key')} || {} };
};
hook 'before_template' => sub {
my $tokens = shift;
# allow portable static content
$tokens->{uri_base} = request->base->path
if request->base->path ne '/';
# allow portable dynamic content
$tokens->{uri_for} = sub { uri_for(@_)->path_query };
# current query string to all resubmit from within ajax template
my $queryuri = URI->new();
$queryuri->query_param($_ => param($_))
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(@_) };
# create date ranges from within templates
$tokens->{to_daterange} = sub { interval_to_daterange(@_) };
# data structure for DataTables records per page menu
$tokens->{table_showrecordsmenu} =
to_json( setting('table_showrecordsmenu') );
# linked searches will use these default url path params
foreach my $sidebar_key (keys %{ var('sidebar_defaults') }) {
my ($mode, $report) = ($sidebar_key =~ m/(\w+)_(\w+)/);
if ($mode =~ m/^(?:search|device)$/) {
$tokens->{$sidebar_key} = uri_for("/$mode", {tab => $report});
}
elsif ($mode =~ m/^report$/) {
$tokens->{$sidebar_key} = uri_for("/$mode/$report");
}
foreach my $col (keys %{ var('sidebar_defaults')->{$sidebar_key} }) {
$tokens->{$sidebar_key}->query_param($col,
var('sidebar_defaults')->{$sidebar_key}->{$col});
}
# fix Plugin Template Variables to be only path+query
$tokens->{$sidebar_key} = $tokens->{$sidebar_key}->path_query;
}
# helper from NetAddr::MAC for the MAC formatting
$tokens->{mac_format_call} = 'as_'. lc(param('mac_format'))
if param('mac_format');
# allow very long lists of ports
$Template::Directive::WHILE_MAX = 10_000;
# allow hash keys with leading underscores
$Template::Stash::PRIVATE = undef;
};
# prevent Template::AutoFilter taking action on CSV output
hook 'before_template' => sub {
my $template_engine = engine 'template';
if (not request->is_ajax
and header('Content-Type')
and header('Content-Type') eq 'text/comma-separated-values' ) {
$template_engine->{config}->{AUTO_FILTER} = 'none';
$template_engine->init();
}
# debug $template_engine->{config}->{AUTO_FILTER};
};
hook 'after_template_render' => sub {
my $template_engine = engine 'template';
if (not request->is_ajax
and header('Content-Type')
and header('Content-Type') eq 'text/comma-separated-values' ) {
$template_engine->{config}->{AUTO_FILTER} = 'html_entity';
$template_engine->init();
}
# debug $template_engine->{config}->{AUTO_FILTER};
};
# support for report api which is basic table result in json
hook before_layout_render => sub {
my ($tokens, $html_ref) = @_;
return unless request_is_api_report or request_is_api_search;
${ $html_ref } =
$tokens->{results} ? (to_json $tokens->{results}) : {};
};
# workaround for Swagger plugin weird response body
hook 'after' => sub {
my $r = shift; # a Dancer::Response
if (request->path eq '/swagger.json') {
$r->content( to_json( $r->content ) );
header('Content-Type' => 'application/json');
}
# instead of setting serialiser
# and also to handle some plugins just returning undef if search fails
if (request_is_api) {
header('Content-Type' => 'application/json');
$r->content( $r->content || '[]' );
}
};
# remove empty lines from CSV response
# this makes writing templates much more straightforward!
hook 'after' => sub {
my $r = shift; # a Dancer::Response
if ($r->content_type and $r->content_type eq 'text/comma-separated-values') {
my @newlines = ();
my @lines = split m/\n/, $r->content;
foreach my $line (@lines) {
push @newlines, $line if $line !~ m/^\s*$/;
}
$r->content(join "\n", @newlines);
}
};
any qr{.*} => sub {
var('notfound' => true);
status 'not_found';
template 'index';
};
true;