package App::Netdisco::Web::AuthN; use Dancer ':syntax'; use Dancer::Plugin::DBIC; use Dancer::Plugin::Auth::Extensible; use Dancer::Plugin::Swagger; use MIME::Base64; 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('/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) { 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 request->header('Authorization')) { 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 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 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) { 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}->{'/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', path => '/api/logout', tags => ['General'], parameters => [], responses => { default => { examples => { 'application/json' => {} } } }, }, get qr{^/(?:api/)?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; }; # 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}->{'/api/logout'} ->{get}->{security} = [ {}, {APIKeyHeader => []} ]; true;