1package App::Netdisco::Web::AuthN; 2 3use Dancer ':syntax'; 4use Dancer::Plugin::DBIC; 5use Dancer::Plugin::Auth::Extensible; 6use Dancer::Plugin::Swagger; 7 8use App::Netdisco::Util::Web 'request_is_api'; 9use MIME::Base64; 10 11# ensure that regardless of where the user is redirected, we have a link 12# back to the page they requested. 13hook 'before' => sub { 14 params->{return_url} ||= ((request->path ne uri_for('/')->path) 15 ? request->uri : uri_for(setting('web_home'))->path); 16}; 17 18# Dancer will create a session if it sees its own cookie. For the API and also 19# various auto login options we need to bootstrap the session instead. If no 20# auth data passed, then the hook simply returns, no session is set, and the 21# user is redirected to login page. 22hook 'before' => sub { 23 # return if request is for endpoints not requiring a session 24 return if ( 25 request->path eq uri_for('/login')->path 26 or request->path eq uri_for('/logout')->path 27 or request->path eq uri_for('/swagger.json')->path 28 or index(request->path, uri_for('/swagger-ui')->path) == 0 29 ); 30 31 # from the internals of Dancer::Plugin::Auth::Extensible 32 my $provider = Dancer::Plugin::Auth::Extensible::auth_provider('users'); 33 34 # API calls must conform strictly to path and header requirements 35 if (request_is_api) { 36 # Dancer will issue a cookie to the client which could be returned and 37 # cause API calls to succeed without passing token. Kill the session. 38 session->destroy; 39 40 my $token = request->header('Authorization'); 41 my $user = $provider->validate_api_token($token) 42 or return; 43 44 session(logged_in_user => $user); 45 session(logged_in_user_realm => 'users'); 46 return; 47 } 48 49 # after checking API, we can short circuit if Dancer reads its cookie OK 50 return if session('logged_in_user'); 51 52 if (setting('trust_x_remote_user') 53 and scalar request->header('X-REMOTE_USER') 54 and length scalar request->header('X-REMOTE_USER')) { 55 56 (my $user = scalar request->header('X-REMOTE_USER')) =~ s/@[^@]*$//; 57 return if setting('validate_remote_user') 58 and not $provider->get_user_details($user); 59 60 session(logged_in_user => $user); 61 session(logged_in_user_realm => 'users'); 62 } 63 elsif (setting('trust_remote_user') 64 and defined $ENV{REMOTE_USER} 65 and length $ENV{REMOTE_USER}) { 66 67 (my $user = $ENV{REMOTE_USER}) =~ s/@[^@]*$//; 68 return if setting('validate_remote_user') 69 and not $provider->get_user_details($user); 70 71 session(logged_in_user => $user); 72 session(logged_in_user_realm => 'users'); 73 } 74 elsif (setting('no_auth')) { 75 session(logged_in_user => 'guest'); 76 session(logged_in_user_realm => 'users'); 77 } 78 else { 79 # user has no AuthN - force to handler for '/' 80 request->path_info('/'); 81 } 82}; 83 84# override default login_handler so we can log access in the database 85swagger_path { 86 description => 'Obtain an API Key', 87 tags => ['General'], 88 path => setting('url_base')->with('/login')->path, 89 parameters => [], 90 responses => { default => { examples => { 91 'application/json' => { api_key => 'cc9d5c02d8898e5728b7d7a0339c0785' } } }, 92 }, 93}, 94post '/login' => sub { 95 my $api = ((request->accept and request->accept =~ m/(?:json|javascript)/) ? true : false); 96 97 # get authN data from BasicAuth header used by API, put into params 98 my $authheader = request->header('Authorization'); 99 if (defined $authheader and $authheader =~ /^Basic (.*)$/i) { 100 my ($u, $p) = split(m/:/, (MIME::Base64::decode($1) || ":")); 101 params->{username} = $u; 102 params->{password} = $p; 103 } 104 105 # validate authN 106 my ($success, $realm) = authenticate_user(param('username'),param('password')); 107 108 if ($success) { 109 my $user = schema('netdisco')->resultset('User') 110 ->find({ username => { -ilike => quotemeta(param('username')) } }); 111 112 session logged_in_user => $user->username; 113 session logged_in_fullname => $user->fullname; 114 session logged_in_user_realm => $realm; 115 116 schema('netdisco')->resultset('UserLog')->create({ 117 username => session('logged_in_user'), 118 userip => request->remote_address, 119 event => (sprintf 'Login (%s)', ($api ? 'API' : 'WebUI')), 120 details => param('return_url'), 121 }); 122 $user->update({ last_on => \'now()' }); 123 124 if ($api) { 125 header('Content-Type' => 'application/json'); 126 $user->update({ 127 token_from => time, 128 token => \'md5(random()::text)', 129 })->discard_changes(); 130 return to_json { api_key => $user->token }; 131 } 132 133 redirect param('return_url'); 134 } 135 else { 136 # invalidate session cookie 137 session->destroy; 138 139 schema('netdisco')->resultset('UserLog')->create({ 140 username => param('username'), 141 userip => request->remote_address, 142 event => (sprintf 'Login Failure (%s)', ($api ? 'API' : 'WebUI')), 143 details => param('return_url'), 144 }); 145 146 if ($api) { 147 header('Content-Type' => 'application/json'); 148 status('unauthorized'); 149 return to_json { error => 'authentication failed' }; 150 } 151 152 vars->{login_failed}++; 153 forward uri_for('/login'), 154 { login_failed => 1, return_url => param('return_url') }, 155 { method => 'GET' }; 156 } 157}; 158 159# ugh, *puke*, but D::P::Swagger has no way to set this with swagger_path 160# must be after the path is declared, above. 161Dancer::Plugin::Swagger->instance->doc 162 ->{paths}->{ setting('url_base')->with('/login')->path } 163 ->{post}->{security}->[0]->{BasicAuth} = []; 164 165# we override the default login_handler, so logout has to be handled as well 166swagger_path { 167 description => 'Destroy user API Key and session cookie', 168 tags => ['General'], 169 path => setting('url_base')->with('/logout')->path, 170 parameters => [], 171 responses => { default => { examples => { 'application/json' => {} } } }, 172}, 173get '/logout' => sub { 174 my $api = ((request->accept and request->accept =~ m/(?:json|javascript)/) ? true : false); 175 176 # clear out API token 177 my $user = schema('netdisco')->resultset('User') 178 ->find({ username => session('logged_in_user')}); 179 $user->update({token => undef, token_from => undef})->discard_changes() 180 if $user and $user->in_storage; 181 182 # invalidate session cookie 183 session->destroy; 184 185 schema('netdisco')->resultset('UserLog')->create({ 186 username => session('logged_in_user'), 187 userip => request->remote_address, 188 event => (sprintf 'Logout (%s)', ($api ? 'API' : 'WebUI')), 189 details => '', 190 }); 191 192 if ($api) { 193 header('Content-Type' => 'application/json'); 194 return to_json {}; 195 } 196 197 redirect uri_for(setting('web_home'))->path; 198}; 199 200# user redirected here (POST -> GET) when login fails 201get qr{^/(?:login(?:/denied)?)?} => sub { 202 my $api = ((request->accept and request->accept =~ m/(?:json|javascript)/) ? true : false); 203 204 if ($api) { 205 header('Content-Type' => 'application/json'); 206 status('unauthorized'); 207 return to_json { 208 error => 'not authorized', 209 return_url => param('return_url'), 210 }; 211 } 212 else { 213 template 'index', { 214 return_url => param('return_url') 215 }, { layout => 'main' }; 216 } 217}; 218 219true; 220