|
Line 0
Link Here
|
| 0 |
- |
1 |
package Koha::Auth::ShibbolethCompat; |
|
|
2 |
|
| 3 |
# Copyright 2024 Koha Development team |
| 4 |
# |
| 5 |
# This file is part of Koha. |
| 6 |
# |
| 7 |
# Koha is free software; you can redistribute it and/or modify it |
| 8 |
# under the terms of the GNU General Public License as published by |
| 9 |
# the Free Software Foundation; either version 3 of the License, or |
| 10 |
# (at your option) any later version. |
| 11 |
# |
| 12 |
# Koha is distributed in the hope that it will be useful, but |
| 13 |
# WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 15 |
# GNU General Public License for more details. |
| 16 |
# |
| 17 |
# You should have received a copy of the GNU General Public License |
| 18 |
# along with Koha; if not, see <http://www.gnu.org/licenses>. |
| 19 |
|
| 20 |
use Modern::Perl; |
| 21 |
|
| 22 |
use C4::Context; |
| 23 |
use JSON qw( encode_json ); |
| 24 |
use Try::Tiny qw( catch try ); |
| 25 |
|
| 26 |
use Koha::Auth::Client::Shibboleth; |
| 27 |
use Koha::Auth::Identity::Providers; |
| 28 |
use Koha::Logger; |
| 29 |
use Koha::Patrons; |
| 30 |
|
| 31 |
=head1 NAME |
| 32 |
|
| 33 |
Koha::Auth::ShibbolethCompat - Shibboleth compatibility layer |
| 34 |
|
| 35 |
=head1 SYNOPSIS |
| 36 |
|
| 37 |
use Koha::Auth::ShibbolethCompat; |
| 38 |
|
| 39 |
my ($success, $cardnumber, $userid, $patron) = |
| 40 |
Koha::Auth::ShibbolethCompat::authenticate_shibboleth_user($shib_login); |
| 41 |
|
| 42 |
=head1 DESCRIPTION |
| 43 |
|
| 44 |
This module provides a compatibility layer between the legacy Shibboleth authentication |
| 45 |
system and the modern identity provider architecture. It allows existing code |
| 46 |
to continue working while using the new client-based authentication internally. |
| 47 |
|
| 48 |
=head1 FUNCTIONS |
| 49 |
|
| 50 |
=head2 get_shibboleth_provider |
| 51 |
|
| 52 |
my $provider = Koha::Auth::ShibbolethCompat::get_shibboleth_provider(); |
| 53 |
|
| 54 |
Returns a Shibboleth identity provider, either modern or created from legacy config. |
| 55 |
|
| 56 |
=cut |
| 57 |
|
| 58 |
sub get_shibboleth_provider { |
| 59 |
my $logger = Koha::Logger->get(); |
| 60 |
|
| 61 |
# Try to find existing identity provider |
| 62 |
my $provider = Koha::Auth::Identity::Providers->search( { protocol => 'Shibboleth' } )->next; |
| 63 |
|
| 64 |
if ($provider) { |
| 65 |
$logger->debug( "Found Shibboleth identity provider: " . $provider->code ); |
| 66 |
return $provider->upgrade_class; |
| 67 |
} |
| 68 |
|
| 69 |
# Fall back to creating temporary provider from legacy config |
| 70 |
my $shibboleth_config = C4::Context->config('shibboleth'); |
| 71 |
|
| 72 |
if ($shibboleth_config) { |
| 73 |
$logger->debug("Using legacy Shibboleth configuration"); |
| 74 |
|
| 75 |
# Create temporary provider object (not stored in database) |
| 76 |
require Koha::Auth::Identity::Provider::Shibboleth; |
| 77 |
my $temp_provider = Koha::Auth::Identity::Provider::Shibboleth->new( |
| 78 |
{ |
| 79 |
code => 'shibboleth', |
| 80 |
description => "Legacy Shibboleth SSO", |
| 81 |
config => encode_json( |
| 82 |
{ |
| 83 |
sso_url => $shibboleth_config->{ssoUrl} || '/Shibboleth.sso/Login', |
| 84 |
slo_url => $shibboleth_config->{sloUrl} || '/Shibboleth.sso/Logout', |
| 85 |
entity_id => $shibboleth_config->{entityID} || 'koha' |
| 86 |
} |
| 87 |
), |
| 88 |
mapping => encode_json( |
| 89 |
{ |
| 90 |
userid => $shibboleth_config->{mapping}->{userid} || 'HTTP_REMOTE_USER', |
| 91 |
email => $shibboleth_config->{mapping}->{email} || 'HTTP_MAIL', |
| 92 |
firstname => $shibboleth_config->{mapping}->{firstname} || 'HTTP_GIVENNAME', |
| 93 |
surname => $shibboleth_config->{mapping}->{surname} || 'HTTP_SN' |
| 94 |
} |
| 95 |
), |
| 96 |
matchpoint => $shibboleth_config->{matchpoint} || 'userid' |
| 97 |
} |
| 98 |
); |
| 99 |
|
| 100 |
# Don't store - this is just for compatibility |
| 101 |
return $temp_provider; |
| 102 |
} |
| 103 |
|
| 104 |
$logger->warn("No Shibboleth configuration found"); |
| 105 |
return; |
| 106 |
} |
| 107 |
|
| 108 |
=head2 authenticate_shibboleth_user |
| 109 |
|
| 110 |
my ($success, $cardnumber, $userid, $patron) = |
| 111 |
Koha::Auth::ShibbolethCompat::authenticate_shibboleth_user($shib_login); |
| 112 |
|
| 113 |
Authenticates a Shibboleth user using the modern implementation but returns data |
| 114 |
in the format expected by the legacy interface. |
| 115 |
|
| 116 |
=cut |
| 117 |
|
| 118 |
sub authenticate_shibboleth_user { |
| 119 |
my ($shib_login) = @_; |
| 120 |
|
| 121 |
my $logger = Koha::Logger->get(); |
| 122 |
|
| 123 |
return (0) unless $shib_login; |
| 124 |
|
| 125 |
# Get legacy Shibboleth configuration |
| 126 |
my $config = get_shibboleth_config(); |
| 127 |
return (0) unless $config; |
| 128 |
|
| 129 |
# Extract Shibboleth attributes from environment |
| 130 |
my $mapped_data = {}; |
| 131 |
my $mapping = $config->{mapping} || {}; |
| 132 |
|
| 133 |
foreach my $koha_field ( keys %$mapping ) { |
| 134 |
my $shib_attribute_config = $mapping->{$koha_field}; |
| 135 |
|
| 136 |
# Handle legacy format: { 'is' => 'attribute_name' } |
| 137 |
my $shib_attribute; |
| 138 |
if ( ref($shib_attribute_config) eq 'HASH' && exists $shib_attribute_config->{is} ) { |
| 139 |
$shib_attribute = $shib_attribute_config->{is}; |
| 140 |
} else { |
| 141 |
|
| 142 |
# Handle simple string format |
| 143 |
$shib_attribute = $shib_attribute_config; |
| 144 |
} |
| 145 |
|
| 146 |
if ( exists $ENV{$shib_attribute} ) { |
| 147 |
$mapped_data->{$koha_field} = $ENV{$shib_attribute}; |
| 148 |
} |
| 149 |
} |
| 150 |
|
| 151 |
return (0) unless %$mapped_data; |
| 152 |
|
| 153 |
# Find existing patron using matchpoint |
| 154 |
my $matchpoint = $config->{matchpoint} || 'userid'; |
| 155 |
my $patron; |
| 156 |
|
| 157 |
if ( my $match_value = $mapped_data->{$matchpoint} ) { |
| 158 |
my $patron_rs = Koha::Patrons->search( { $matchpoint => $match_value } ); |
| 159 |
if ( $patron_rs->count ) { |
| 160 |
$patron = $patron_rs->next; |
| 161 |
$logger->debug( "Found existing patron: " . $patron->borrowernumber ); |
| 162 |
|
| 163 |
# Update patron if sync is enabled |
| 164 |
if ( $config->{sync} ) { |
| 165 |
|
| 166 |
# Remove fields that shouldn't be updated |
| 167 |
my $update_data = {%$mapped_data}; |
| 168 |
delete $update_data->{borrowernumber}; |
| 169 |
delete $update_data->{categorycode}; |
| 170 |
delete $update_data->{branchcode}; |
| 171 |
|
| 172 |
$patron->set($update_data)->store; |
| 173 |
$logger->debug( "Updated patron data for: " . $patron->userid ); |
| 174 |
} |
| 175 |
} |
| 176 |
} |
| 177 |
|
| 178 |
# Auto-create user if enabled and no existing patron found |
| 179 |
if ( !$patron && $config->{autocreate} ) { |
| 180 |
$logger->debug("Auto-creating new patron for: $shib_login"); |
| 181 |
|
| 182 |
# Set required fields from environment or defaults |
| 183 |
$mapped_data->{categorycode} = $ENV{cat} || $ENV{categorycode} || 'PT'; # Default patron category |
| 184 |
$mapped_data->{branchcode} = $ENV{branchcode} || C4::Context->userenv->{branch} || 'CPL'; |
| 185 |
|
| 186 |
try { |
| 187 |
$patron = Koha::Patron->new($mapped_data)->store; |
| 188 |
$logger->info( "Auto-created patron: " . $patron->userid ); |
| 189 |
} catch { |
| 190 |
$logger->error("Failed to auto-create patron: $_"); |
| 191 |
return (0); |
| 192 |
}; |
| 193 |
} |
| 194 |
|
| 195 |
if ($patron) { |
| 196 |
$logger->info( "Shibboleth authentication successful for user: " . $patron->userid ); |
| 197 |
return ( |
| 198 |
1, # success |
| 199 |
$patron->cardnumber, # cardnumber |
| 200 |
$patron->userid, # userid |
| 201 |
$patron # patron object |
| 202 |
); |
| 203 |
} else { |
| 204 |
$logger->warn("Shibboleth authentication failed - no patron found/created for: $shib_login"); |
| 205 |
} |
| 206 |
|
| 207 |
return (0); # failure |
| 208 |
} |
| 209 |
|
| 210 |
=head2 get_shibboleth_config |
| 211 |
|
| 212 |
my $config = Koha::Auth::ShibbolethCompat::get_shibboleth_config(); |
| 213 |
|
| 214 |
Returns Shibboleth configuration in the legacy format expected by C4::Auth_with_shibboleth. |
| 215 |
|
| 216 |
=cut |
| 217 |
|
| 218 |
sub get_shibboleth_config { |
| 219 |
|
| 220 |
# Try modern provider first |
| 221 |
my $providers = Koha::Auth::Identity::Providers->search( { protocol => 'Shibboleth' } ); |
| 222 |
if ( $providers->count ) { |
| 223 |
my $provider = $providers->next->upgrade_class; |
| 224 |
my $config = $provider->get_config; |
| 225 |
my $mapping = $provider->get_mapping; |
| 226 |
|
| 227 |
# Convert to legacy format |
| 228 |
return { |
| 229 |
matchpoint => $provider->matchpoint, |
| 230 |
mapping => $mapping, |
| 231 |
ssoUrl => $config->{sso_url}, |
| 232 |
sloUrl => $config->{slo_url}, |
| 233 |
entityID => $config->{entity_id} |
| 234 |
}; |
| 235 |
} |
| 236 |
|
| 237 |
# Fall back to legacy configuration directly |
| 238 |
# The test mocks C4::Context->config to return the shibboleth config directly |
| 239 |
my $config = C4::Context->config('shibboleth') || C4::Context->config(); |
| 240 |
|
| 241 |
return $config; |
| 242 |
} |
| 243 |
|
| 244 |
1; |