From 9cad6778d43d04ae602ba363cad677d4f5151a5d Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Sun, 22 Feb 2026 20:18:30 +0000 Subject: [PATCH] Bug 39224: Add and Update Koha Object classes Updates the Koha object layer for unified identity providers: - Koha::Auth::Hostname(s): Manage library hostnames - Koha::Auth::Identity::Provider::Hostname(s): Link providers to hostnames with force_sso support - Koha::Auth::Identity::Provider::Mapping(s): Map provider fields to patron attributes - Updates Koha::Auth::Identity::Provider with mappings/hostnames accessors and simplified to_api - Updates Koha::Auth::Identity::Providers with find_forced_provider and polymorphic dispatch via object_class - Updates C4::Auth for hostname-based provider selection - Updates C4::Auth_with_shibboleth for new provider structure - Adds ForceIdentityProvider support in Koha::Config::SysPref - Updates AuthClient template plugin for hostname-aware login Test plan: 1. Run: prove t/db_dependent/Koha/Auth/Identity/Provider.t 2. Verify provider objects can be created and linked to hostnames Sponsored-by: ByWater Solutions --- C4/Auth.pm | 46 +++++----- C4/Auth_with_shibboleth.pm | 68 ++++++++++---- Koha/Auth/Hostname.pm | 87 ++++++++++++++++++ Koha/Auth/Hostnames.pm | 50 +++++++++++ Koha/Auth/Identity/Provider.pm | 109 ++++++++++------------- Koha/Auth/Identity/Provider/Hostname.pm | 56 ++++++++++++ Koha/Auth/Identity/Provider/Hostnames.pm | 51 +++++++++++ Koha/Auth/Identity/Provider/Mapping.pm | 72 +++++++++++++++ Koha/Auth/Identity/Provider/Mappings.pm | 89 ++++++++++++++++++ Koha/Auth/Identity/Providers.pm | 70 ++++++++++++++- Koha/Config/SysPref.pm | 9 +- Koha/Template/Plugin/AuthClient.pm | 17 +++- Makefile.PL | 1 + 13 files changed, 619 insertions(+), 106 deletions(-) create mode 100644 Koha/Auth/Hostname.pm create mode 100644 Koha/Auth/Hostnames.pm create mode 100644 Koha/Auth/Identity/Provider/Hostname.pm create mode 100644 Koha/Auth/Identity/Provider/Hostnames.pm create mode 100644 Koha/Auth/Identity/Provider/Mapping.pm create mode 100644 Koha/Auth/Identity/Provider/Mappings.pm diff --git a/C4/Auth.pm b/C4/Auth.pm index b5308ae4a42..bae5ebe4150 100644 --- a/C4/Auth.pm +++ b/C4/Auth.pm @@ -173,7 +173,7 @@ sub get_template_and_user { my $cookie_mgr = Koha::CookieManager->new; # Get shibboleth login attribute - my $shib = C4::Context->config('useshibboleth') && shib_ok(); + my $shib = C4::Context->preference('ShibbolethAuthentication') && shib_ok(); my $shib_login = $shib ? get_login_shib() : undef; C4::Context->interface( $in->{type} ); @@ -811,7 +811,7 @@ sub checkauth { my $query = shift; # Get shibboleth login attribute - my $shib = C4::Context->config('useshibboleth') && shib_ok(); + my $shib = C4::Context->preference('ShibbolethAuthentication') && shib_ok(); my $shib_login = $shib ? get_login_shib() : undef; # $authnotrequired will be set for scripts which will run without authentication @@ -1021,6 +1021,13 @@ sub checkauth { $auth_state = 'logout'; } + my $is_sco_sci = + ( $query->param('sco_user_login') + || $query->param('sci_user_login') + || ( $template_name && $template_name =~ m|sc[io]/| ) ); + my $forced_provider = + $is_sco_sci ? undef : Koha::Auth::Identity::Providers->find_forced_provider( $ENV{SERVER_NAME} ); + unless ($userid) { #we initiate a session prior to checking for a username to allow for anonymous sessions... @@ -1151,15 +1158,9 @@ sub checkauth { } # If shib configured and shibOnly enabled, we should ignore anything other than a shibboleth type login. - if ( - $shib + if ( $shib && !$shibSuccess - && ( - ( ( $type eq 'opac' ) && C4::Context->preference('OPACShibOnly') ) - || ( ( $type ne 'opac' ) - && C4::Context->preference('staffShibOnly') ) - ) - ) + && $forced_provider ) { $return = 0; } @@ -1522,6 +1523,20 @@ sub checkauth { ); } + if ($forced_provider) { + my $redirect_url; + if ( $forced_provider->protocol eq 'SAML2' ) { + $redirect_url = login_shib_url($query); + } elsif ( $forced_provider->protocol eq 'OIDC' || $forced_provider->protocol eq 'OAuth' ) { + my $base = ( $type eq 'opac' ) ? "/api/v1/public/oauth/login" : "/api/v1/oauth/login"; + $redirect_url = "$base/" . $forced_provider->code . "/$type"; + } + if ($redirect_url) { + print $query->redirect( -uri => $redirect_url, -status => 303 ); + safe_exit; + } + } + if ($cas) { # Is authentication against multiple CAS servers enabled? @@ -1544,15 +1559,6 @@ sub checkauth { if ($shib) { - #If shibOnly is enabled just go ahead and redirect directly - if ( ( ( $type eq 'opac' ) && C4::Context->preference('OPACShibOnly') ) - || ( ( $type ne 'opac' ) && C4::Context->preference('staffShibOnly') ) ) - { - my $redirect_url = login_shib_url($query); - print $query->redirect( -uri => "$redirect_url", -status => 303 ); - safe_exit; - } - $template->param( shibbolethAuthentication => $shib, shibbolethLoginUrl => login_shib_url($query), @@ -2003,7 +2009,7 @@ sub checkpw { $type = 'opac' unless $type; # Get shibboleth login attribute - my $shib = C4::Context->config('useshibboleth') && shib_ok(); + my $shib = C4::Context->preference('ShibbolethAuthentication') && shib_ok(); my $shib_login = $shib ? get_login_shib() : undef; my $anonymous_patron = C4::Context->preference('AnonymousPatron'); diff --git a/C4/Auth_with_shibboleth.pm b/C4/Auth_with_shibboleth.pm index dd58ec747eb..d5449b925f6 100644 --- a/C4/Auth_with_shibboleth.pm +++ b/C4/Auth_with_shibboleth.pm @@ -18,10 +18,13 @@ package C4::Auth_with_shibboleth; # along with Koha; if not, see . use Modern::Perl; -use base 'Exporter'; + +our ( @ISA, @EXPORT_OK ); BEGIN { - our @EXPORT_OK = qw(shib_ok logout_shib login_shib_url checkpw_shib get_login_shib); + require Exporter; + @ISA = qw(Exporter); + @EXPORT_OK = qw(shib_ok logout_shib login_shib_url checkpw_shib get_login_shib); } use C4::Context; @@ -34,6 +37,7 @@ use Carp qw( carp ); use List::MoreUtils qw( any ); use Koha::Logger; +use Koha::Auth::Identity::Providers; # Check that shib config is not malformed @@ -241,28 +245,45 @@ sub _get_return { } sub _get_shib_config { - my $config = C4::Context->config('shibboleth'); - - if ( !$config ) { - Koha::Logger->get->warn('shibboleth config not defined'); + my $hostname = $ENV{SERVER_NAME}; + my $provider = Koha::Auth::Identity::Providers->search( + { + 'me.protocol' => 'SAML2', + 'me.enabled' => 1, + 'hostname.hostname' => $hostname, + 'hostnames.is_enabled' => 1, + }, + { prefetch => [ 'mappings', { 'hostnames' => 'hostname' } ], rows => 1 } + )->next; + return 0 unless $provider; + + my ( $mapping, $matchpoint ) = $provider->mappings->as_auth_mapping; + + unless ($matchpoint) { + carp 'shibboleth matchpoint not defined'; return 0; } - if ( $config->{matchpoint} - && defined( $config->{mapping}->{ $config->{matchpoint} }->{is} ) ) - { - my $logger = Koha::Logger->get; - $logger->debug( "koha borrower field to match: " . $config->{matchpoint} ); - $logger->debug( "shibboleth attribute to match: " . $config->{mapping}->{ $config->{matchpoint} }->{is} ); - return $config; - } else { - if ( !$config->{matchpoint} ) { - carp 'shibboleth matchpoint not defined'; - } else { - carp 'shibboleth matchpoint not mapped'; - } + unless ( defined $mapping->{$matchpoint}->{is} ) { + carp 'shibboleth matchpoint not mapped'; return 0; } + + my $saml2_config = $provider->get_config // {}; + + my $config = { + matchpoint => $matchpoint, + mapping => $mapping, + autocreate => $saml2_config->{autocreate} || 0, + sync => $saml2_config->{sync} || 0, + welcome => $saml2_config->{welcome} || 0, + }; + + my $logger = Koha::Logger->get; + $logger->debug( "koha borrower field to match: " . $config->{matchpoint} ); + $logger->debug( "shibboleth attribute to match: " . $mapping->{ $config->{matchpoint} }->{is} ); + + return $config; } 1; @@ -405,6 +426,15 @@ A sugar function to that simply returns the current page URI with appropriate pr This routine is NOT exported +=head2 get_force_sso_setting + + my $force_sso = get_force_sso_setting($interface); + +Returns the force SSO setting for the given interface (opac or intranet). +Returns 0 if shibboleth is not configured or the setting is disabled. + + $interface - 'opac' or 'intranet' + =head2 _get_shib_config my $config = _get_shib_config(); diff --git a/Koha/Auth/Hostname.pm b/Koha/Auth/Hostname.pm new file mode 100644 index 00000000000..dfa7c11f659 --- /dev/null +++ b/Koha/Auth/Hostname.pm @@ -0,0 +1,87 @@ +package Koha::Auth::Hostname; + +# Copyright Koha Community 2026 +# +# This file is part of Koha. +# +# Koha 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. +# +# Koha 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 Koha; if not, see . + +use Modern::Perl; + +use base qw(Koha::Object); + +use C4::Context; +use Koha::Database; + +=head1 NAME + +Koha::Auth::Hostname - Koha Auth Hostname Object class + +=head1 API + +=head2 Class methods + +=head3 sync_from_syspref + + Koha::Auth::Hostname->sync_from_syspref( 'OPACBaseURL', $url ); + +Updates (or inserts) the reserved hostname row for the given URL syspref. +C is always stored as C; C +as C. Does nothing if C<$value> is empty or does not contain +a recognisable hostname. + +=cut + +my %SYSPREF_HOSTNAME_ID = ( + opacbaseurl => 1, + staffclientbaseurl => 2, +); + +sub sync_from_syspref { + my ( $class, $pref_name, $value ) = @_; + $value //= ''; + my $hostname_id = $SYSPREF_HOSTNAME_ID{ lc $pref_name } or return; + my ($hostname) = $value =~ m{^https?://([^/:?#]+)} or return; + Koha::Database->new->schema->resultset('Hostname')->update_or_create( + { hostname_id => $hostname_id, hostname => $hostname }, + { key => 'primary' } + ); +} + +=head3 sync_from_sysprefs + +Ensures that the hostnames derived from OPACBaseURL and staffClientBaseURL +are present in the hostnames table. OPACBaseURL maps to C +and staffClientBaseURL to C. + +=cut + +sub sync_from_sysprefs { + my ($class) = @_; + for my $pref ( keys %SYSPREF_HOSTNAME_ID ) { + $class->sync_from_syspref( $pref, C4::Context->preference($pref) ); + } +} + +=head2 Internal methods + +=head3 _type + +=cut + +sub _type { + return 'Hostname'; +} + +1; diff --git a/Koha/Auth/Hostnames.pm b/Koha/Auth/Hostnames.pm new file mode 100644 index 00000000000..95b200bb9a3 --- /dev/null +++ b/Koha/Auth/Hostnames.pm @@ -0,0 +1,50 @@ +package Koha::Auth::Hostnames; + +# Copyright Koha Community 2026 +# +# This file is part of Koha. +# +# Koha 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. +# +# Koha 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 Koha; if not, see . + +use Modern::Perl; + +use base qw(Koha::Objects); + +use Koha::Auth::Hostname; + +=head1 NAME + +Koha::Auth::Hostnames - Koha Auth Hostnames Object set class + +=head1 API + +=head2 Internal methods + +=head3 _type + +=cut + +sub _type { + return 'Hostname'; +} + +=head3 object_class + +=cut + +sub object_class { + return 'Koha::Auth::Hostname'; +} + +1; diff --git a/Koha/Auth/Identity/Provider.pm b/Koha/Auth/Identity/Provider.pm index f9f3d51339d..c9788c7df4a 100644 --- a/Koha/Auth/Identity/Provider.pm +++ b/Koha/Auth/Identity/Provider.pm @@ -22,6 +22,8 @@ use Modern::Perl; use base qw(Koha::Object Koha::Object::JSONFields); use Koha::Auth::Identity::Provider::Domains; +use Koha::Auth::Identity::Provider::Hostnames; +use Koha::Auth::Identity::Provider::Mappings; use Koha::Exceptions; =head1 NAME @@ -46,6 +48,35 @@ sub domains { return Koha::Auth::Identity::Provider::Domains->_new_from_dbic( scalar $self->_result->domains ); } +=head3 mappings + + my $mappings = $provider->mappings; + +Returns the related I iterator. + +=cut + +sub mappings { + my ($self) = @_; + + return Koha::Auth::Identity::Provider::Mappings->_new_from_dbic( + scalar $self->_result->identity_provider_mappings ); +} + +=head3 hostnames + + my $hostnames = $provider->hostnames; + +Returns the related I iterator. + +=cut + +sub hostnames { + my ($self) = @_; + + return Koha::Auth::Identity::Provider::Hostnames->_new_from_dbic( scalar $self->_result->hostnames ); +} + =head3 get_config my $config = $provider->get_config; @@ -81,6 +112,15 @@ sub get_config { } ); + # SAML2 (config holds protocol-specific settings like autocreate/sync/welcome) + $provider->set_config( + { + autocreate => 1, + sync => 1, + welcome => 0, + } + ); + This method stores the passed config in JSON format. =cut @@ -99,53 +139,16 @@ sub set_config { return $self->set_encoded_json_field( { data => $config, field => 'config' } ); } -=head3 get_mapping +=head3 store - my $mapping = $provider->get_mapping; - -Returns a I containing the attribute mapping for the provider. +Identity provider specific store method to ensure config population before saving to db =cut -sub get_mapping { +sub store { my ($self) = @_; - - return $self->decode_json_field( { field => 'mapping' } ); -} - -=head3 set_mapping - - $provider->mapping( $mapping ); - -This method stores the passed mappings in JSON format. - -=cut - -sub set_mapping { - my ( $self, $mapping ) = @_; - - return $self->set_encoded_json_field( { data => $mapping, field => 'mapping' } ); -} - -=head3 upgrade_class - - my $upgraded_object = $provider->upgrade_class - -Returns a new instance of the object, with the right class. - -=cut - -sub upgrade_class { - my ($self) = @_; - my $protocol = $self->protocol; - - my $class = $self->protocol_to_class_mapping->{$protocol}; - - Koha::Exception->throw( $protocol . ' is not a valid protocol' ) - unless $class; - - eval "require $class"; - return $class->_new_from_dbic( $self->_result ); + $self->config( $self->config // '{}' ); + return $self->SUPER::store(); } =head2 Internal methods @@ -162,12 +165,10 @@ suitable for API output. sub to_api { my ( $self, $params ) = @_; - my $config = $self->get_config; - my $mapping = $self->get_mapping; + my $config = $self->get_config; my $json = $self->SUPER::to_api($params); - $json->{config} = $config; - $json->{mapping} = $mapping; + $json->{config} = $config; return $json; } @@ -180,22 +181,6 @@ sub _type { return 'IdentityProvider'; } -=head3 protocol_to_class_mapping - - my $mapping = Koha::Auth::Identity::Provider::protocol_to_class_mapping - -Internal method that returns a mapping between I codes and -implementing I. To be used by B. - -=cut - -sub protocol_to_class_mapping { - return { - OAuth => 'Koha::Auth::Identity::Provider::OAuth', - OIDC => 'Koha::Auth::Identity::Provider::OIDC', - }; -} - =head3 mandatory_config_attributes Stub method for raising exceptions on invalid protocols. diff --git a/Koha/Auth/Identity/Provider/Hostname.pm b/Koha/Auth/Identity/Provider/Hostname.pm new file mode 100644 index 00000000000..824aa98bc1f --- /dev/null +++ b/Koha/Auth/Identity/Provider/Hostname.pm @@ -0,0 +1,56 @@ +package Koha::Auth::Identity::Provider::Hostname; + +# Copyright Koha Community 2026 +# +# This file is part of Koha. +# +# Koha 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. +# +# Koha 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 Koha; if not, see . + +use Modern::Perl; + +use base qw(Koha::Object); + +=head1 NAME + +Koha::Auth::Identity::Provider::Hostname - Koha Auth Provider Hostname Object class + +=head1 API + +=head2 Class methods + +=head3 to_api + +Overrides the default serialization to embed the hostname string from the +related Hostname record alongside the hostname_id FK. + +=cut + +sub to_api { + my ( $self, $params ) = @_; + my $data = $self->SUPER::to_api($params); + $data->{hostname} = $self->_result->hostname->hostname; + return $data; +} + +=head2 Internal methods + +=head3 _type + +=cut + +sub _type { + return 'IdentityProviderHostname'; +} + +1; diff --git a/Koha/Auth/Identity/Provider/Hostnames.pm b/Koha/Auth/Identity/Provider/Hostnames.pm new file mode 100644 index 00000000000..e22fd1f2fb8 --- /dev/null +++ b/Koha/Auth/Identity/Provider/Hostnames.pm @@ -0,0 +1,51 @@ +package Koha::Auth::Identity::Provider::Hostnames; + +# Copyright Koha Community 2026 +# +# This file is part of Koha. +# +# Koha 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. +# +# Koha 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 Koha; if not, see . + +use Modern::Perl; + +use Koha::Database; +use Koha::Auth::Identity::Provider::Hostname; + +use base qw(Koha::Objects); + +=head1 NAME + +Koha::Auth::Identity::Provider::Hostnames - Koha Auth Provider Hostnames Object set class + +=head1 API + +=head2 Internal methods + +=head3 _type + +=cut + +sub _type { + return 'IdentityProviderHostname'; +} + +=head3 object_class + +=cut + +sub object_class { + return 'Koha::Auth::Identity::Provider::Hostname'; +} + +1; diff --git a/Koha/Auth/Identity/Provider/Mapping.pm b/Koha/Auth/Identity/Provider/Mapping.pm new file mode 100644 index 00000000000..721f700993f --- /dev/null +++ b/Koha/Auth/Identity/Provider/Mapping.pm @@ -0,0 +1,72 @@ +package Koha::Auth::Identity::Provider::Mapping; + +# This file is part of Koha. +# +# Koha 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. +# +# Koha 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 Koha; if not, see . + +use Modern::Perl; + +use base qw(Koha::Object); + +use Koha::Exceptions; +use Koha::Auth::Identity::Provider::Mappings; + +=head1 NAME + +Koha::Auth::Identity::Provider::Mapping - Koha Identity Provider field mapping object + +=head1 API + +=head2 Class methods + +=head3 store + +Override store to enforce the single-matchpoint rule and validate that at least +one of C or C is supplied. + +=cut + +sub store { + my ($self) = @_; + + unless ( defined $self->provider_field || defined $self->default_content ) { + Koha::Exceptions::MissingParameter->throw( parameter => 'provider_field or default_content' ); + } + + if ( $self->is_matchpoint ) { + + # Clear any existing matchpoint for this provider + Koha::Auth::Identity::Provider::Mappings->search( + { + identity_provider_id => $self->identity_provider_id, + is_matchpoint => 1, + ( $self->mapping_id ? ( mapping_id => { '!=' => $self->mapping_id } ) : () ), + } + )->update( { is_matchpoint => 0 } ); + } + + return $self->SUPER::store(); +} + +=head2 Internal methods + +=head3 _type + +=cut + +sub _type { + return 'IdentityProviderMapping'; +} + +1; diff --git a/Koha/Auth/Identity/Provider/Mappings.pm b/Koha/Auth/Identity/Provider/Mappings.pm new file mode 100644 index 00000000000..4607ba38368 --- /dev/null +++ b/Koha/Auth/Identity/Provider/Mappings.pm @@ -0,0 +1,89 @@ +package Koha::Auth::Identity::Provider::Mappings; + +# This file is part of Koha. +# +# Koha 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. +# +# Koha 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 Koha; if not, see . + +use Modern::Perl; + +use base qw(Koha::Objects); + +use Koha::Auth::Identity::Provider::Mapping; + +=head1 NAME + +Koha::Auth::Identity::Provider::Mappings - Koha Identity Provider field mappings collection + +=head1 API + +=head2 Class methods + +=head3 get_matchpoint + + my $matchpoint_mapping = $mappings->get_matchpoint; + +Returns the mapping row flagged as the matchpoint, or undef if none. + +=cut + +sub get_matchpoint { + my ($self) = @_; + return $self->search( { is_matchpoint => 1 } )->next; +} + +=head3 as_auth_mapping + + my $mapping_hashref = $mappings->as_auth_mapping; + +Returns a hashref suitable for use by the authentication layer: +C<{ koha_field => provider_field, ... }> plus C key. + +=cut + +sub as_auth_mapping { + my ($self) = @_; + + my %mapping; + my $matchpoint; + + while ( my $m = $self->next ) { + $mapping{ $m->koha_field } = { + is => $m->provider_field, + content => $m->default_content, + }; + $matchpoint = $m->koha_field if $m->is_matchpoint; + } + + return ( \%mapping, $matchpoint ); +} + +=head2 Internal methods + +=head3 _type + +=cut + +sub _type { + return 'IdentityProviderMapping'; +} + +=head3 object_class + +=cut + +sub object_class { + return 'Koha::Auth::Identity::Provider::Mapping'; +} + +1; diff --git a/Koha/Auth/Identity/Providers.pm b/Koha/Auth/Identity/Providers.pm index 0bedd81880c..1cd47471d5b 100644 --- a/Koha/Auth/Identity/Providers.pm +++ b/Koha/Auth/Identity/Providers.pm @@ -19,8 +19,10 @@ package Koha::Auth::Identity::Providers; use Modern::Perl; -use Koha::Database; use Koha::Auth::Identity::Provider; +use Koha::Auth::Identity::Provider::Hostnames; +use Koha::Auth::Identity::Provider::OAuth; +use Koha::Auth::Identity::Provider::OIDC; use base qw(Koha::Objects); @@ -30,6 +32,37 @@ Koha::Auth::Identity::Providers - Koha Auth Provider Object class =head1 API +=head2 Class methods + +=head3 find_forced_provider + + my $provider = Koha::Auth::Identity::Providers->find_forced_provider($hostname); + +Returns the I object if the given I is configured to +force SSO, or C if no forced provider is found. + +=cut + +sub find_forced_provider { + my ( $self, $hostname ) = @_; + return unless $hostname; + + my $hostname_link = Koha::Auth::Identity::Provider::Hostnames->search( + { + 'hostname.hostname' => $hostname, + 'identity_provider.enabled' => 1, + 'me.is_enabled' => 1, + 'me.force_sso' => 1, + }, + { join => [ 'hostname', 'identity_provider' ] } + )->next; + + return unless $hostname_link; + + my $ip_result = $hostname_link->_result->identity_provider; + return $self->object_class($ip_result)->_new_from_dbic($ip_result); +} + =head2 Internal methods =cut @@ -42,12 +75,45 @@ sub _type { return 'IdentityProvider'; } +=head3 _polymorphic_field + +Return the field in the table that defines the polymorphic class to be built + +=cut + +sub _polymorphic_field { + return 'protocol'; +} + +=head3 _polymorphic_map + +Return the mapping from protocol value to implementing class name + +=cut + +sub _polymorphic_map { + return { + OAuth => 'Koha::Auth::Identity::Provider::OAuth', + OIDC => 'Koha::Auth::Identity::Provider::OIDC', + }; +} + =head3 object_class +Return object class dynamically based on protocol + =cut sub object_class { - return 'Koha::Auth::Identity::Provider'; + my ( $self, $object ) = @_; + + return 'Koha::Auth::Identity::Provider' unless $object; + + my $field = $self->_polymorphic_field; + my $map = $self->_polymorphic_map; + + return $map->{ $object->$field } || 'Koha::Auth::Identity::Provider'; } 1; + diff --git a/Koha/Config/SysPref.pm b/Koha/Config/SysPref.pm index 229a291553c..976707597dc 100644 --- a/Koha/Config/SysPref.pm +++ b/Koha/Config/SysPref.pm @@ -46,7 +46,14 @@ sub store { C4::Log::logaction( 'SYSTEMPREFERENCE', $action, undef, $self->variable . ' | ' . $self->value ); - return $self->SUPER::store($self); + my $result = $self->SUPER::store($self); + + if ( $self->variable =~ /\A(?:opacbaseurl|staffclientbaseurl)\z/i ) { + require Koha::Auth::Hostname; + Koha::Auth::Hostname->sync_from_syspref( $self->variable, $self->value ); + } + + return $result; } =head3 delete diff --git a/Koha/Template/Plugin/AuthClient.pm b/Koha/Template/Plugin/AuthClient.pm index 7b28a3facd4..ccd858e0d3b 100644 --- a/Koha/Template/Plugin/AuthClient.pm +++ b/Koha/Template/Plugin/AuthClient.pm @@ -55,8 +55,21 @@ sub get_providers { # Handle database upgrade state where schema might be out of sync try { - my $providers = - Koha::Auth::Identity::Providers->search( { "domains.allow_$interface" => 1 }, { prefetch => 'domains' } ); + my $hostname = $ENV{SERVER_NAME}; + my $providers = Koha::Auth::Identity::Providers->search( + { + "domains.allow_$interface" => 1, + "me.enabled" => 1, + -or => [ + { 'hostname.hostname' => $hostname, 'hostnames.is_enabled' => 1 }, + { 'hostnames.identity_provider_hostname_id' => undef } + ] + }, + { + join => [ 'domains', { 'hostnames' => 'hostname' } ], + distinct => 1 + } + ); my $base_url = ( $interface eq 'staff' ) ? "/api/v1/oauth/login" : "/api/v1/public/oauth/login"; while ( my $provider = $providers->next ) { diff --git a/Makefile.PL b/Makefile.PL index 6e849fe9911..5b85970c796 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -363,6 +363,7 @@ my $target_map = { './serials' => 'INTRANET_CGI_DIR', './services' => 'INTRANET_CGI_DIR', './sip2' => 'INTRANET_CGI_DIR', + './shibboleth' => 'INTRANET_CGI_DIR', './skel' => 'NONE', './skel/var/lock/koha' => { target => 'LOCK_DIR', trimdir => -1 }, './skel/var/log/koha' => { target => 'LOG_DIR', trimdir => -1 }, -- 2.53.0