Files
netdisco/lib/App/Netdisco/Web/AuthN.pm
Oliver Gorwits 46d8c52a08 Implement changes for API authentication and Swagger UI (#541)
* initial token-based-api login handler

* add token schema and validation

* initial import of pyro3d api code

* basic Swagger spec support

* Merge in working copy of API/Device.pm

* Fix some error handling for API/Device.pm

* Break out utility functions into separate file, to allow other api portions to use

* Add NodeIP support.

* Add nodeip plugin to config

* remove double define of "plugin:" (#448)

disclaimer: i did not test this is any way, came across it when looking for something else.

* only AuthZ header for api use, and alway regen key on login

* use RFC7235

* workaround for Swagger plugin weird response body

* do not autodiscover swagger routes

* code formatting only

* move api util to utils area

* initial full swagger spec for nodeip search

* add api user role and fix api auth failure response

* update version of swagger-ui to 3.20.3

* add more openapi defs

* fixes to SQL and api spec

* clean up subs

* improvements to login/logout for API

* make api logout work

* add openapi tags to group operations

* allow api params to be generated from DBIC schema spec

* remove API calls for nodes and devices

* remove some poor assumptions about api calls

* tidy up

* remove DDP

* make login and logout similar

* example of api call being handled by ajax call

* make the branch authonly
2019-03-17 20:27:19 +00:00

198 lines
6.1 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::AuthN;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use Dancer::Plugin::Swagger;
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);
# from the internals of Dancer::Plugin::Auth::Extensible
my $provider = Dancer::Plugin::Auth::Extensible::auth_provider('users');
if (! session('logged_in_user')
and request->path ne uri_for('/login')->path
and request->path ne uri_for('/logout')->path
and request->path ne uri_for('/swagger.json')->path
and index(request->path, uri_for('/swagger-ui')->path) != 0) {
if (setting('trust_x_remote_user')
and scalar request->header('X-REMOTE_USER')
and length scalar request->header('X-REMOTE_USER')) {
(my $user = scalar request->header('X-REMOTE_USER')) =~ s/@[^@]*$//;
return if setting('validate_remote_user')
and not $provider->get_user_details($user);
session(logged_in_user => $user);
session(logged_in_user_realm => 'users');
}
elsif (setting('trust_remote_user')
and defined $ENV{REMOTE_USER}
and length $ENV{REMOTE_USER}) {
(my $user = $ENV{REMOTE_USER}) =~ s/@[^@]*$//;
return if setting('validate_remote_user')
and not $provider->get_user_details($user);
session(logged_in_user => $user);
session(logged_in_user_realm => 'users');
}
elsif (setting('no_auth')) {
session(logged_in_user => 'guest');
session(logged_in_user_realm => 'users');
}
elsif (request_is_api()
and index(request->path, uri_for('/api')->path) == 0) {
my $token = request->header('Authorization');
my $user = $provider->validate_api_token($token)
or return;
session(logged_in_user => $user);
session(logged_in_user_realm => 'users');
}
else {
# user has no AuthN - force to handler for '/'
request->path_info('/');
}
}
};
# user redirected here (POST -> GET) when login fails
get qr{^/(?:login(?:/denied)?)?} => sub {
if (request_is_api()) {
status('unauthorized');
return to_json {
error => 'not authorized',
return_url => param('return_url'),
};
}
else {
template 'index', { return_url => param('return_url') };
}
};
# 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 => [],
responses => {
default => {
examples => {
'application/json' => { api_key => 'cc9d5c02d8898e5728b7d7a0339c0785' } } },
},
},
post '/login' => sub {
my $mode = (request_is_api() ? 'API' : 'WebUI');
# get authN data from request (HTTP BasicAuth or Form params)
my $authheader = request->header('Authorization');
if (defined $authheader and $authheader =~ /^Basic (.*)$/i) {
my ($u, $p) = split(m/:/, (MIME::Base64::decode($1) || ":"));
params->{username} = $u;
params->{password} = $p;
}
# validate authN
my ($success, $realm) = authenticate_user(param('username'),param('password'));
if ($success) {
my $user = schema('netdisco')->resultset('User')
->find({ username => { -ilike => quotemeta(param('username')) } });
session logged_in_user => $user->username;
session logged_in_fullname => $user->fullname;
session logged_in_user_realm => $realm;
schema('netdisco')->resultset('UserLog')->create({
username => session('logged_in_user'),
userip => request->remote_address,
event => "Login ($mode)",
details => param('return_url'),
});
$user->update({ last_on => \'now()' });
if ($mode eq 'API') {
$user->update({
token_from => time,
token => \'md5(random()::text)',
})->discard_changes();
return to_json { api_key => $user->token };
}
redirect param('return_url');
}
else {
session->destroy;
schema('netdisco')->resultset('UserLog')->create({
username => param('username'),
userip => request->remote_address,
event => "Login Failure ($mode)",
details => param('return_url'),
});
if ($mode eq 'API') {
status('unauthorized');
return to_json { error => 'authentication failed' };
}
vars->{login_failed}++;
forward uri_for('/login'),
{ login_failed => 1, return_url => param('return_url') },
{ method => 'GET' };
}
};
# 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} = [];
# 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'],
parameters => [],
responses => { default => { examples => { 'application/json' => {} } } },
},
get '/logout' => sub {
my $mode = (request_is_api() ? 'API' : 'WebUI');
# clear out API token
my $user = schema('netdisco')->resultset('User')
->find({ username => session('logged_in_user')});
$user->update({token => undef, token_from => undef})->discard_changes()
if $user and $user->in_storage;
# invalidate session cookie
session->destroy;
schema('netdisco')->resultset('UserLog')->create({
username => session('logged_in_user'),
userip => request->remote_address,
event => "Logout ($mode)",
details => '',
});
if ($mode eq 'API') {
return to_json {};
}
redirect uri_for('/inventory')->path;
};
true;