diff --git a/VERSION b/VERSION index a602fc9..78bc1ab 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.4 +0.10.0 diff --git a/etc/extopus.cfg.dist b/etc/extopus.cfg.dist index 136fd93..69d0797 100644 --- a/etc/extopus.cfg.dist +++ b/etc/extopus.cfg.dist @@ -5,6 +5,12 @@ mojo_secret = d0duj3mfjfviasasdfasdf log_file = /scratch/extopus-demo/extopus-full.log default_user = admin update_interval = 5 +openid_url = http://keycloak.example.com +openid_realm = extopus-realm +openid_client_id = extopus-client +openid_client_secret = 1234567890 +openid_callback = http://extopus.example.com/openid/callback +openid_epuser_attribute = ep_user *** FRONTEND *** #logo_large = http://www.upc-cablecom.biz/en/cablecom_logo_b2b.jpg diff --git a/lib/EP.pm b/lib/EP.pm index 25838bb..cc0b369 100644 --- a/lib/EP.pm +++ b/lib/EP.pm @@ -34,6 +34,8 @@ use EP::RpcService; use EP::Config; use EP::DocPlugin; use EP::Visualizer; +use EP::Controller::OpenId; + use Mojo::Base 'Mojolicious'; =head2 cfg @@ -80,7 +82,6 @@ Mojolicious calls the startup method at initialization time. sub startup { my $app = shift; - @{$app->commands->namespaces} = (__PACKAGE__.'::Command'); my $gcfg = $app->cfg->{GENERAL}; @@ -135,7 +136,18 @@ sub startup { $self->redirect_to('/'.$app->prefix); }); } - + if ($gcfg->{openid_url}) { + # load openid config + EP::Controller::OpenId::loadConfig($app); + $routes->get('/openid/auth')->to( + controller => 'OpenId', + action => 'auth', + ); + $routes->get('/openid/callback')->to( + controller => 'OpenId', + action => 'callback', + ); + } $routes->get('/' => sub { shift->redirect_to('/'.$app->prefix)}); $app->plugin('EP::DocPlugin', { diff --git a/lib/EP/Config.pm b/lib/EP/Config.pm index a9d21ec..381d783 100644 --- a/lib/EP/Config.pm +++ b/lib/EP/Config.pm @@ -120,6 +120,12 @@ ${E}head1 SYNOPSIS localguide = /home/doc/extopusguide.pod update_interval = 86400 # default_user = admin + openid_url = http://keycloak.example.com + openid_realm = extopus-realm + openid_client_id = extopus-client + openid_client_secret = 1234567890 + openid_callback = http://extopus.example.com/openid/callback + openid_epuser_attribute = ep_user *** FRONTEND *** logo_large = http://www.upc-cablecom.biz/en/cablecom_logo_b2b.jpg @@ -241,7 +247,7 @@ sub _make_parser { _mandatory => [qw(GENERAL FRONTEND ATTRIBUTES TABLES)], GENERAL => { _doc => 'Global configuration settings for Extopus', - _vars => [ qw(cache_dir mojo_secret log_file log_level default_user update_interval localguide auto_update) ], + _vars => [ qw(cache_dir mojo_secret log_file log_level default_user update_interval localguide auto_update openid_url openid_realm openid_client_id openid_client_secret openid_callback openid_epuser_attribute) ], _mandatory => [ qw(cache_dir mojo_secret log_file) ], cache_dir => { _doc => 'directory to cache information gathered via the inventory plugins', _sub => sub { @@ -260,6 +266,13 @@ sub _make_parser { _doc => 'check for updates every x seconds. 1 day by default. Set the update_interval to 0 to disable automatic updateing and relie on the populate command.', _default => 24*3600, }, + openid_url => { _doc => 'url to the openid provider' }, + openid_realm => { _doc => 'realm to use for openid authentication' }, + openid_client_id => { _doc => 'client id for openid authentication' }, + openid_client_secret => { _doc => 'client secret for openid authentication' }, + openid_callback => { _doc => 'callback url for openid authentication' }, + openid_epuser_attribute => { _doc => 'attribute to use for the user name' }, + auto_update => { _doc => 'automatically update the inventory when the app starts' }, }, FRONTEND => { _doc => 'Frontend tuneing parameters', diff --git a/lib/EP/Controller/OpenId.pm b/lib/EP/Controller/OpenId.pm new file mode 100644 index 0000000..fac0582 --- /dev/null +++ b/lib/EP/Controller/OpenId.pm @@ -0,0 +1,168 @@ +package EP::Controller::OpenId; + +use Mojo::Base 'Mojolicious::Controller', -signatures; +use Mojo::URL; +use Mojo::Util qw(dumper); + +=head1 NAME + +EP::Controller::OpenId - OpenID Controller + +=head1 SYNOPSIS + + $routes->get('/openid/auth')->to( + controller => 'OpenId', + action => 'auth', + ); + $routes->get('/openid/callback')->( + controller => 'OpenId', + action => 'cb', + ); + +=head1 DESCRIPTION + +Provide OpenID authentication for EP using the OpenID Connect protocol. + +=head1 ATTRIBUTES + +All attributes inherited from L. As well as the following: + +=cut + +=head2 gcfg + +Access the GENERAL configuraiton. See L for details.especially the C attributes. + +Examples: + + openid_url = http://keycloak.example.com + openid_realm = extopus-realm + openid_client_id = extopus-client + openid_client_secret = 1234567890 + openid_callback = http://extopus.example.com/openid/callback + openid_epuser_attribute = ep_user + +=cut + +has gcfg => sub ($self) { $self->app->cfg->{GENERAL} }; + +=head1 METHODS + +All methods inherited from L as well as the following: + +=head2 loadConfig($app) + +Load the OpenID configuration from the OpenID provider. This should be called +once at startup time. + +=cut + +my $openIdCfg; + +sub loadConfig ($app) { + return if $openIdCfg; + my $ua = $app->ua; + my $gcfg = $app->cfg->{GENERAL}; + my $cfgurl = Mojo::URL->new($gcfg->{openid_url} + .'/realms/'.$gcfg->{openid_realm} + .'/.well-known/openid-configuration'); + $app->log->debug("loading openid config from $cfgurl"); + my $cfg = $ua->get($cfgurl)->res; + if (not $cfg->is_success) { + $app->log->error($cfg->to_string); + return; + } + # $self->log->debug($cfg->to_string); + $openIdCfg = $cfg->json; +}; + +=head2 auth + +Redirect the user to the OpenID provider for authentication. + +=cut + +sub auth ($self) { + my $url = Mojo::URL->new($openIdCfg->{authorization_endpoint}); + $self->redirect_to( + $url->query( + client_id => $self->gcfg->{openid_client_id}, + response_type => 'code', + scope => 'openid', + redirect_uri => $self->gcfg->{openid_callback} + ) + ); +} + +=head2 callback + +Handle the callback from the OpenID provider. This will extract the user +information from the OpenID provider and store it in the session. + +=cut + +sub callback ($self) { + my $ua = $self->app->ua; + my $gcfg = $self->gcfg; + my $auth = $ua->post($openIdCfg->{token_endpoint} => form => { + code => $self->param('code'), + client_id => $gcfg->{openid_client_id}, + client_secret => $gcfg->{openid_client_secret}, + redirect_uri => $gcfg->{openid_callback}, + grant_type => 'authorization_code' + })->res; + if (not $auth->is_success) { + $self->log->error($auth->to_string); + return $self->render(text => 'auth error', code => 403); + } + my $userInfo = $ua->get($openIdCfg->{userinfo_endpoint} => { + authorization => 'Bearer '.$auth->json->{access_token} + })->res; + if (not $userInfo->is_success) { + $self->log->error($userInfo->to_string); + return $self->render(text => 'userinfo error', code => 403); + } + $self->log->debug(dumper $userInfo->json); + my ($user,$login) = split /:/, ($userInfo->json->{$gcfg->{openid_epuser_attribute}} // ''); + if (not $user) { + $self->log->error("no $gcfg->{openid_epuser_attribute} attribute found in userinfo (".dumper($userInfo->json).")"); + return $self->render(text => 'userinfo not found', code => 403); + } + $self->session->{epUser} = $user; + $self->session->{epLogin} =$login; + $self->redirect_to('/'.$self->app->prefix); +} + +1; + +=head1 COPYRIGHT + +Copyright (c) 2023 by OETIKER+PARTNER AG. All rights reserved. + +=head1 LICENSE + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +=head1 AUTHOR + +Stobi@oetiker.chE> + +=head1 HISTORY + + 2023-02-20 to 1.0 first version + +=cut + +