Bugzilla – Attachment 193684 Details for
Bug 39224
Migrate SAML/Shibboleth configuration into Identity Providers
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 39224: Add and Update Koha Object classes
14448a2.patch (text/plain), 32.44 KB, created by
Martin Renvoize (ashimema)
on 2026-02-24 07:33:46 UTC
(
hide
)
Description:
Bug 39224: Add and Update Koha Object classes
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2026-02-24 07:33:46 UTC
Size:
32.44 KB
patch
obsolete
>From 14448a2f6e8e7cb818af123c558095dc864d0edd Mon Sep 17 00:00:00 2001 >From: Martin Renvoize <martin.renvoize@openfifth.co.uk> >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 | 73 +++++++++++---- > Koha/Auth/Client.pm | 4 +- > Koha/Auth/Client/OAuth.pm | 25 ++++-- > 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 | 60 +++++++++++++ > Koha/Auth/Identity/Provider/Mappings.pm | 75 ++++++++++++++++ > Koha/Auth/Identity/Providers.pm | 70 ++++++++++++++- > Koha/Config/SysPref.pm | 9 +- > Koha/REST/V1/OAuth/Client.pm | 3 +- > Koha/Template/Plugin/AuthClient.pm | 17 +++- > Makefile.PL | 1 + > 16 files changed, 619 insertions(+), 117 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..f6fbcf99089 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 <https://www.gnu.org/licenses>. > > 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,50 @@ 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 = $provider->mappings->as_auth_mapping; >+ my $hostname_link = $provider->hostnames->search( >+ { 'hostname.hostname' => $hostname }, >+ { join => 'hostname' } >+ )->next; >+ my $matchpoint = $hostname_link ? $hostname_link->matchpoint : undef; >+ >+ 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 +431,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/Client.pm b/Koha/Auth/Client.pm >index 33647df8c7b..3fd4913e694 100644 >--- a/Koha/Auth/Client.pm >+++ b/Koha/Auth/Client.pm >@@ -59,11 +59,13 @@ sub get_user { > my $data = $params->{data}; > my $interface = $params->{interface}; > my $config = $params->{config}; >+ my $hostname = $params->{hostname}; > > my $provider = Koha::Auth::Identity::Providers->search( { code => $provider_code } )->next; > > my ( $mapped_data, $patron ) = >- $self->_get_data_and_patron( { provider => $provider, data => $data, config => $config } ); >+ $self->_get_data_and_patron( >+ { provider => $provider, data => $data, config => $config, hostname => $hostname } ); > > $mapped_data //= {}; > >diff --git a/Koha/Auth/Client/OAuth.pm b/Koha/Auth/Client/OAuth.pm >index 618b76162a4..df6ff275df1 100644 >--- a/Koha/Auth/Client/OAuth.pm >+++ b/Koha/Auth/Client/OAuth.pm >@@ -54,22 +54,29 @@ sub _get_data_and_patron { > my $provider = $params->{provider}; > my $data = $params->{data}; > my $config = $params->{config}; >+ my $hostname = $params->{hostname}; > > my $patron; > my $mapped_data; > >- my $mapping = decode_json( $provider->mapping ); >- my $matchpoint = $provider->matchpoint; >+ my $mapping = $provider->mappings->as_auth_mapping; >+ my $hostname_link = $hostname >+ ? $provider->hostnames->search( >+ { 'hostname.hostname' => $hostname }, >+ { join => 'hostname' } >+ )->next >+ : undef; >+ my $matchpoint = $hostname_link ? $hostname_link->matchpoint : undef; > > if ( $data->{id_token} ) { > my ( $header_part, $claims_part, $footer_part ) = split( /\./, $data->{id_token} ); > > my $claim = decode_json( decode_base64url($claims_part) ); > >- foreach my $key ( keys %$mapping ) { >- my $pkey = $mapping->{$key}; >- $mapped_data->{$key} = $claim->{$pkey} >- if defined $claim->{$pkey}; >+ foreach my $koha_field ( keys %$mapping ) { >+ my $pkey = $mapping->{$koha_field}{is}; >+ $mapped_data->{$koha_field} = $claim->{$pkey} >+ if defined $pkey && defined $claim->{$pkey}; > } > > $patron = $self->_find_patron_by_matchpoint( $matchpoint, $mapped_data->{$matchpoint} ); >@@ -87,10 +94,10 @@ sub _get_data_and_patron { > ? $tx->res->json > : Mojo::Parameters->new( $tx->res->body )->to_hash; > >- foreach my $key ( keys %$mapping ) { >- my $pkey = $mapping->{$key}; >+ foreach my $koha_field ( keys %$mapping ) { >+ my $pkey = $mapping->{$koha_field}{is}; > my $value = $self->_traverse_hash( { base => $claim, keys => $pkey } ); >- $mapped_data->{$key} = $value >+ $mapped_data->{$koha_field} = $value > if defined $value; > } > >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 <https://www.gnu.org/licenses>. >+ >+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<OPACBaseURL> is always stored as C<hostname_id = 1>; C<staffClientBaseURL> >+as C<hostname_id = 2>. 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<hostname_id = 1> >+and staffClientBaseURL to C<hostname_id = 2>. >+ >+=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 <https://www.gnu.org/licenses>. >+ >+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<Koha::Auth::Identity::Provider::Mappings> 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<Koha::Auth::Identity::Provider::Hostnames> 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<hashref> 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<protocol> codes and >-implementing I<classes>. To be used by B<upgrade_class>. >- >-=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 <https://www.gnu.org/licenses>. >+ >+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 <https://www.gnu.org/licenses>. >+ >+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..def4dd0646f >--- /dev/null >+++ b/Koha/Auth/Identity/Provider/Mapping.pm >@@ -0,0 +1,60 @@ >+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 <https://www.gnu.org/licenses>. >+ >+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 validate that at least one of C<provider_field> or >+C<default_content> 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' ); >+ } >+ >+ 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..2e97223f7ab >--- /dev/null >+++ b/Koha/Auth/Identity/Provider/Mappings.pm >@@ -0,0 +1,75 @@ >+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 <https://www.gnu.org/licenses>. >+ >+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 as_auth_mapping >+ >+ my $mapping_hashref = $mappings->as_auth_mapping; >+ >+Returns a hashref suitable for use by the authentication layer: >+C<{ koha_field => { is => provider_field, content => default_content }, ... }>. >+The matchpoint is stored on the provider itself (C<identity_providers.matchpoint>). >+ >+=cut >+ >+sub as_auth_mapping { >+ my ($self) = @_; >+ >+ my %mapping; >+ >+ while ( my $m = $self->next ) { >+ $mapping{ $m->koha_field } = { >+ is => $m->provider_field, >+ content => $m->default_content, >+ }; >+ } >+ >+ return \%mapping; >+} >+ >+=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<Koha::Auth::Identity::Provider> object if the given I<hostname> is configured to >+force SSO, or C<undef> 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/REST/V1/OAuth/Client.pm b/Koha/REST/V1/OAuth/Client.pm >index 3b5ea677d3a..9b24259617f 100644 >--- a/Koha/REST/V1/OAuth/Client.pm >+++ b/Koha/REST/V1/OAuth/Client.pm >@@ -131,7 +131,8 @@ sub login { > provider => $provider, > data => $response, > interface => $interface, >- config => $c->oauth2->providers->{$provider} >+ config => $c->oauth2->providers->{$provider}, >+ hostname => $c->req->url->base->host, > } > ); > >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 >
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 39224
:
189234
|
189235
|
189236
|
189237
|
189238
|
189239
|
189240
|
189241
|
189242
|
189243
|
189244
|
193618
|
193619
|
193620
|
193621
|
193622
|
193623
|
193624
|
193625
|
193626
|
193627
|
193681
|
193682
|
193683
| 193684 |
193685
|
193686
|
193687
|
193688
|
193689
|
193690
|
193691
|
193692