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; |