From 65a908dcd31a2830007b3dd25bb4d6cbd6001d86 Mon Sep 17 00:00:00 2001 From: Oliver Gorwits Date: Mon, 5 Jun 2023 17:02:20 +0100 Subject: [PATCH] #1036 skip API login for trust_remote_user, trust_x_remote_user, no_auth --- .../Netdisco/DB/Result/Virtual/UserRole.pm | 11 ++- lib/App/Netdisco/Web.pm | 25 +++-- lib/App/Netdisco/Web/Auth/Provider/DBIC.pm | 14 ++- lib/App/Netdisco/Web/AuthN.pm | 95 +++++++++++-------- 4 files changed, 88 insertions(+), 57 deletions(-) diff --git a/lib/App/Netdisco/DB/Result/Virtual/UserRole.pm b/lib/App/Netdisco/DB/Result/Virtual/UserRole.pm index 3877ea30..6c9441a8 100644 --- a/lib/App/Netdisco/DB/Result/Virtual/UserRole.pm +++ b/lib/App/Netdisco/DB/Result/Virtual/UserRole.pm @@ -27,13 +27,14 @@ __PACKAGE__->result_source_instance->view_definition(< (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) - ?) + WHERE ( ? ::boolean = false ) OR + ( token IS NOT NULL AND token_from IS NOT NULL + AND token_from > (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) - ?) ) UNION SELECT username, 'api_admin' AS role FROM users - WHERE token IS NOT NULL AND token_from IS NOT NULL - AND token_from > (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) - ?) - AND admin + WHERE admin AND (( ? ::boolean = false ) OR + ( token IS NOT NULL AND token_from IS NOT NULL + AND token_from > (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) - ?) )) ENDSQL ); diff --git a/lib/App/Netdisco/Web.pm b/lib/App/Netdisco/Web.pm index e67d6084..24788087 100644 --- a/lib/App/Netdisco/Web.pm +++ b/lib/App/Netdisco/Web.pm @@ -391,6 +391,10 @@ hook 'after' => sub { } }; +my $api_requires_key = + (setting('trust_remote_user') or setting('trust_x_remote_user') or setting('no_auth')) + eq '1' ? false : true; + # setup for swagger API my $swagger = Dancer::Plugin::Swagger->instance; my $swagger_doc = $swagger->doc; @@ -398,8 +402,8 @@ my $swagger_doc = $swagger->doc; $swagger_doc->{consumes} = 'application/json'; $swagger_doc->{produces} = 'application/json'; $swagger_doc->{tags} = [ - {name => 'General', - description => 'Log in and Log out'}, + ($api_requires_key ? + ({name => 'General', description => 'Log in and Log out'}) : ()), {name => 'Search', description => 'Search Operations'}, {name => 'Objects', @@ -409,13 +413,16 @@ $swagger_doc->{tags} = [ {name => 'Queue', description => 'Operations on the Job Queue'}, ]; -$swagger_doc->{securityDefinitions} = { - APIKeyHeader => - { type => 'apiKey', name => 'Authorization', in => 'header' }, - BasicAuth => - { type => 'basic' }, -}; -$swagger_doc->{security} = [ { APIKeyHeader => [] } ]; + +if ($api_requires_key) { + $swagger_doc->{securityDefinitions} = { + APIKeyHeader => + { type => 'apiKey', name => 'Authorization', in => 'header' }, + BasicAuth => + { type => 'basic' }, + }; + $swagger_doc->{security} = [ { APIKeyHeader => [] } ]; +} # manually install Swagger UI routes because plugin doesn't handle non-root # hosting, so we cannot use show_ui(1) diff --git a/lib/App/Netdisco/Web/Auth/Provider/DBIC.pm b/lib/App/Netdisco/Web/Auth/Provider/DBIC.pm index 2fe5bb66..4b3d7640 100644 --- a/lib/App/Netdisco/Web/Auth/Provider/DBIC.pm +++ b/lib/App/Netdisco/Web/Auth/Provider/DBIC.pm @@ -95,9 +95,19 @@ sub get_user_roles { my $roles = $settings->{roles_relationship} || 'roles'; my $role_column = $settings->{role_column} || 'role'; + # this method returns a list of current user roles + # but for API with trust_remote_user, trust_x_remote_user, and no_auth + # we need to fake that there is a valid API key + + my $api_requires_key = + (setting('trust_remote_user') or setting('trust_x_remote_user') or setting('no_auth')) + eq '1' ? 'false' : 'true'; + return [ try { - $user->$roles->search({}, { bind => [setting('api_token_lifetime'), setting('api_token_lifetime')] }) - ->get_column( $role_column )->all; + $user->$roles->search({}, { bind => [ + $api_requires_key, setting('api_token_lifetime'), + $api_requires_key, setting('api_token_lifetime'), + ] })->get_column( $role_column )->all; } ]; } diff --git a/lib/App/Netdisco/Web/AuthN.pm b/lib/App/Netdisco/Web/AuthN.pm index 9f3f74b4..5e76106c 100644 --- a/lib/App/Netdisco/Web/AuthN.pm +++ b/lib/App/Netdisco/Web/AuthN.pm @@ -32,22 +32,11 @@ hook 'before' => sub { # from the internals of Dancer::Plugin::Auth::Extensible my $provider = Dancer::Plugin::Auth::Extensible::auth_provider('users'); - # API calls must conform strictly to path and header requirements - if (request_is_api) { - # Dancer will issue a cookie to the client which could be returned and - # cause API calls to succeed without passing token. Kill the session. - session->destroy; + # Dancer will issue a cookie to the client which could be returned and + # cause API calls to succeed without passing token. Kill the session. + session->destroy if request_is_api; - 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'); - return; - } - - # after checking API, we can short circuit if Dancer reads its cookie OK + # ...otherwise, we can short circuit if Dancer reads its cookie OK return if session('logged_in_user'); if (setting('trust_x_remote_user') @@ -72,27 +61,27 @@ hook 'before' => sub { session(logged_in_user => $user); session(logged_in_user_realm => 'users'); } + # this works for API calls, too elsif (setting('no_auth')) { session(logged_in_user => 'guest'); session(logged_in_user_realm => 'users'); } + # API calls must conform strictly to path and header requirements + elsif (request_is_api) { + 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('/'); } }; -# override default login_handler so we can log access in the database -swagger_path { - description => 'Obtain an API Key', - tags => ['General'], - path => (setting('url_base') ? setting('url_base')->with('/login')->path : '/login'), - parameters => [], - responses => { default => { examples => { - 'application/json' => { api_key => 'cc9d5c02d8898e5728b7d7a0339c0785' } } }, - }, -}, -post '/login' => sub { +my $login_sub = sub { my $api = ((request->accept and request->accept =~ m/(?:json|javascript)/) ? true : false); # get authN data from BasicAuth header used by API, put into params @@ -161,21 +150,7 @@ 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}->{ (setting('url_base') ? setting('url_base')->with('/login')->path : '/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 => ['General'], - path => (setting('url_base') ? setting('url_base')->with('/logout')->path : '/logout'), - parameters => [], - responses => { default => { examples => { 'application/json' => {} } } }, -}, -get '/logout' => sub { +my $logout_sub = sub { my $api = ((request->accept and request->accept =~ m/(?:json|javascript)/) ? true : false); # clear out API token @@ -202,6 +177,44 @@ get '/logout' => sub { redirect uri_for(setting('web_home'))->path; }; +my $api_requires_key = + (setting('trust_remote_user') or setting('trust_x_remote_user') or setting('no_auth')) + eq '1' ? false : true; + +if ($api_requires_key) { + # override default login_handler so we can log access in the database + swagger_path { + description => 'Obtain an API Key', + tags => ['General'], + path => (setting('url_base') ? setting('url_base')->with('/login')->path : '/login'), + parameters => [], + responses => { default => { examples => { + 'application/json' => { api_key => 'cc9d5c02d8898e5728b7d7a0339c0785' } } }, + }, + }, + post '/login' => $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}->{ (setting('url_base') ? setting('url_base')->with('/login')->path : '/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 => ['General'], + path => (setting('url_base') ? setting('url_base')->with('/logout')->path : '/logout'), + parameters => [], + responses => { default => { examples => { 'application/json' => {} } } }, + }, + get '/logout' => $logout_sub; +} +else { + post '/login' => $login_sub; + get '/logout' => $logout_sub; +} + # user redirected here when require_role does not succeed any qr{^/(?:login(?:/denied)?)?} => sub { my $api = ((request->accept and request->accept =~ m/(?:json|javascript)/) ? true : false);