diff --git a/lib/App/Netdisco/Util/Web.pm b/lib/App/Netdisco/Util/Web.pm index f4d9d70a..ff4451ab 100644 --- a/lib/App/Netdisco/Util/Web.pm +++ b/lib/App/Netdisco/Util/Web.pm @@ -1,14 +1,17 @@ package App::Netdisco::Util::Web; -use strict; -use warnings; +use Dancer ':syntax'; -use base 'Exporter'; use Time::Piece; use Time::Seconds; + +use base 'Exporter'; our @EXPORT = (); our @EXPORT_OK = qw/ - sort_port sort_modules interval_to_daterange sql_match + request_is_api + sort_port sort_modules + interval_to_daterange + sql_match /; our %EXPORT_TAGS = (all => \@EXPORT_OK); @@ -25,6 +28,18 @@ subroutines. =head1 EXPORT_OK +=head2 request_is_api + +Whether the request should be interpreted as an API call. + +=cut + +sub request_is_api { + return (setting('api_token_lifetime') + and request->accept =~ m/(?:json|javascript)/ + and index(request->path, uri_for('/api')->path) == 0); +} + =head2 sql_match( $value, $exact? ) Convert wildcard characters "C<*>" and "C" to "C<%>" and "C<_>" diff --git a/lib/App/Netdisco/Web.pm b/lib/App/Netdisco/Web.pm index 8ab97e2e..e65279d2 100644 --- a/lib/App/Netdisco/Web.pm +++ b/lib/App/Netdisco/Web.pm @@ -13,7 +13,8 @@ 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 'interval_to_daterange'; +use App::Netdisco::Util::Web + qw/request_is_api interval_to_daterange/; use App::Netdisco::Web::AuthN; use App::Netdisco::Web::Static; @@ -82,7 +83,7 @@ $swagger->{schemes} = ['http','https']; $swagger->{consumes} = 'application/json'; $swagger->{produces} = 'application/json'; $swagger->{tags} = [ - {name => 'Global'}, + {name => 'General'}, {name => 'Devices', description => 'Operations relating to Devices (switches, routers, etc)'}, {name => 'Nodes', @@ -94,7 +95,7 @@ $swagger->{securityDefinitions} = { APIKeyHeader => { type => 'apiKey', name => 'Authorization', in => 'header' }, BasicAuth => - { type => 'basic' }, + { type => 'basic' }, }; $swagger->{security} = [ { APIKeyHeader => [] } ]; @@ -234,6 +235,9 @@ hook 'after' => sub { # forward API calls to AJAX route handlers any '/api/:type/:identifier/:method' => require_login sub { + pass unless setting('api_enabled') + ->{ params->{'type'} }->{ params->{'method'} }; + vars->{'is_api'} = 1; my $target = sprintf '/ajax/content/%s/%s', params->{'type'}, params->{'method'}; @@ -241,9 +245,15 @@ any '/api/:type/:identifier/:method' => require_login sub { }; any qr{.*} => sub { - var('notfound' => true); - status 'not_found'; - template 'index'; + if (request_is_api()) { + status(404); + return to_json { error => 'not found' }; + } + else { + var('notfound' => true); + status 'not_found'; + template 'index'; + } }; { diff --git a/lib/App/Netdisco/Web/AuthN.pm b/lib/App/Netdisco/Web/AuthN.pm index 660ee54e..715d8282 100644 --- a/lib/App/Netdisco/Web/AuthN.pm +++ b/lib/App/Netdisco/Web/AuthN.pm @@ -5,14 +5,9 @@ use Dancer::Plugin::DBIC; use Dancer::Plugin::Auth::Extensible; use Dancer::Plugin::Swagger; +use App::Netdisco::Util::Web 'request_is_api'; use MIME::Base64; -sub request_is_api { - return (setting('api_token_lifetime') - and request->header('Authorization') - and request->accept =~ m/(?:json|javascript)/); -} - hook 'before' => sub { params->{return_url} ||= ((request->path ne uri_for('/')->path) ? request->uri : uri_for('/inventory')->path); @@ -22,7 +17,9 @@ hook 'before' => sub { if (! session('logged_in_user') and request->path ne uri_for('/login')->path + and request->path ne uri_for('/api/login')->path and request->path ne uri_for('/logout')->path + and request->path ne uri_for('/api/logout')->path and request->path ne uri_for('/swagger.json')->path and index(request->path, uri_for('/swagger-ui')->path) != 0) { @@ -53,7 +50,7 @@ hook 'before' => sub { session(logged_in_user_realm => 'users'); } elsif (request_is_api() - and index(request->path, uri_for('/api')->path) == 0) { + and request->header('Authorization')) { my $token = request->header('Authorization'); my $user = $provider->validate_api_token($token) @@ -85,18 +82,24 @@ get qr{^/(?:login(?:/denied)?)?} => sub { # override default login_handler so we can log access in the database swagger_path { - description => 'Obtain an API Key using HTTP BasicAuth', - tags => ['Global'], - parameters => [], + description => 'Obtain an API Key using BasicAuth or parameters', + path => '/api/login', + tags => ['General'], + parameters => [ + { name => 'username', in => 'formData', required => false, type => 'string' }, + { name => 'password', in => 'formData', required => false, type => 'string' }, + ], responses => { default => { examples => { 'application/json' => { api_key => 'cc9d5c02d8898e5728b7d7a0339c0785' } } }, }, }, -post '/login' => sub { +post qr{^/(?:api/)?login$} => sub { my $mode = (request_is_api() ? 'API' : 'WebUI'); + my $x = params; use DDP; p $x; + # get authN data from request (HTTP BasicAuth or Form params) my $authheader = request->header('Authorization'); if (defined $authheader and $authheader =~ /^Basic (.*)$/i) { @@ -158,13 +161,13 @@ post '/login' => sub { # ugh, *puke*, but D::P::Swagger has no way to set this with swagger_path # must be after the path is declared, above. -Dancer::Plugin::Swagger->instance->doc->{paths}->{'/login'} - ->{post}->{security}->[0]->{BasicAuth} = []; +Dancer::Plugin::Swagger->instance->doc->{paths}->{'/api/login'} + ->{post}->{security} = [ {}, {BasicAuth => []} ]; # we override the default login_handler, so logout has to be handled as well swagger_path { description => 'Destroy user API Key and session cookie', - tags => ['Global'], + tags => ['General'], parameters => [], responses => { default => { examples => { 'application/json' => {} } } }, }, diff --git a/share/config.yml b/share/config.yml index a868ffd9..b6425a62 100644 --- a/share/config.yml +++ b/share/config.yml @@ -90,6 +90,28 @@ web_plugins: - Device::Addresses - Device::Vlans extra_web_plugins: [] +api_enabled: + device: + details: true + port: false + ports: false + modules: false + addresses: false + vlans: false + node: false + vlan: false + report: false + admin: + topology: false + pseudodevice: false + user: false + report: + slowdevices: false + performance: false + timedoutdevices: false + duplicaedevices: false + orphaned: false + userlog: false sidebar_defaults: search_node: stamps: { default: checked }