View | Details | Raw Unified | Return to bug 18768
Collapse All | Expand All

(-)a/C4/Auth.pm (-1 / +11 lines)
Lines 41-47 use List::MoreUtils qw/ any /; Link Here
41
use Encode qw( encode is_utf8);
41
use Encode qw( encode is_utf8);
42
42
43
# use utf8;
43
# use utf8;
44
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $debug $ldap $cas $caslogout $shib $shib_login);
44
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $debug $ldap $cas $caslogout $shib $shib_login $sip);
45
45
46
BEGIN {
46
BEGIN {
47
    sub psgi_env { any { /^psgi\./ } keys %ENV }
47
    sub psgi_env { any { /^psgi\./ } keys %ENV }
Lines 58-69 BEGIN { Link Here
58
      &get_all_subpermissions &get_user_subpermissions
58
      &get_all_subpermissions &get_user_subpermissions
59
    );
59
    );
60
    %EXPORT_TAGS = ( EditPermissions => [qw(get_all_subpermissions get_user_subpermissions)] );
60
    %EXPORT_TAGS = ( EditPermissions => [qw(get_all_subpermissions get_user_subpermissions)] );
61
    $sip       = C4::Context->config('use_sip') || 0;
61
    $ldap      = C4::Context->config('useldapserver') || 0;
62
    $ldap      = C4::Context->config('useldapserver') || 0;
62
    $cas       = C4::Context->preference('casAuthentication');
63
    $cas       = C4::Context->preference('casAuthentication');
63
    $shib      = C4::Context->config('useshibboleth') || 0;
64
    $shib      = C4::Context->config('useshibboleth') || 0;
64
    $caslogout = C4::Context->preference('casLogout');
65
    $caslogout = C4::Context->preference('casLogout');
65
    require C4::Auth_with_cas;    # no import
66
    require C4::Auth_with_cas;    # no import
66
67
68
    if ($sip) {
69
        require C4::Auth_with_sip;
70
        import C4::Auth_with_sip qw(checkpw_sip);
71
    }
67
    if ($ldap) {
72
    if ($ldap) {
68
        require C4::Auth_with_ldap;
73
        require C4::Auth_with_ldap;
69
        import C4::Auth_with_ldap qw(checkpw_ldap);
74
        import C4::Auth_with_ldap qw(checkpw_ldap);
Lines 1786-1791 sub checkpw { Link Here
1786
        } else {
1791
        } else {
1787
            @return = (0);
1792
            @return = (0);
1788
        }
1793
        }
1794
    } elsif ($sip) {
1795
        $debug and print STDERR "## checkpw - checking SIP\n";
1796
        my ( $retval, $retcard, $retuserid ) = checkpw_sip( $userid, $password );   # EXTERNAL AUTH
1797
        return 0 if $retval == -1;                                                  # Incorrect userid/password for SIP server login attempt
1798
        ($retval) and return ( $retval, $retcard, $retuserid );
1789
    }
1799
    }
1790
1800
1791
    # If we are in a shibboleth session (shibboleth is enabled, and a shibboleth match attribute is present)
1801
    # If we are in a shibboleth session (shibboleth is enabled, and a shibboleth match attribute is present)
(-)a/C4/Auth_with_sip.pm (+387 lines)
Line 0 Link Here
1
package C4::Auth_with_sip;
2
3
# Copyright 2017 Kyle M Hall
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 C4::Members qw(AddMember);
24
use C4::Members::Messaging;
25
26
use Koha::SIP::Client;
27
use Koha::Patrons;
28
use Koha::Patron::Categories;
29
30
use vars qw( @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS );
31
32
BEGIN {
33
    require Exporter;
34
    @ISA    = qw(Exporter);
35
    @EXPORT = qw(checkpw_sip);
36
}
37
38
my $sip = C4::Context->config("sip_servers")
39
  or die 'No "sip_servers" in server hash from KOHA_CONF: ' . $ENV{KOHA_CONF};
40
41
my @sip_servers = $sip->{id} ? $sip : map { $sip->{sip_server}->{$_} }
42
  keys %{ $sip->{sip_server} };
43
44
@sip_servers = sort { $a->{precedence} cmp $b->{precedence} } @sip_servers;
45
46
=head1 checkpw_sip
47
48
    my ( $success, $cardnumber, $userid ) = checkpw_sip( $userid, $password );
49
50
    Checks the given user id and password against a set of SIP servers. Returns
51
    $success if the credentials were verified, along with the cardnumber and userid
52
    to be used for the patron.
53
54
=cut
55
56
sub checkpw_sip {
57
    my ( $userid, $password ) = @_;
58
59
    my $server = select_server( $userid, \@sip_servers );
60
61
    return 0 unless $server;
62
63
    my $sip_patron;
64
    $sip_patron = fetch_patron( $userid, $password, $server );
65
66
    return 0
67
      unless $sip_patron
68
      && $sip_patron->{BL} eq 'Y'
69
      && $sip_patron->{CQ} eq 'Y';
70
71
    my $patron = Koha::Patrons->find( { userid => $userid } );
72
73
    my $patron_hashref;
74
    if (   ( $patron && $server->{update} )
75
        || ( !$patron && $server->{replicate} ) )
76
    {
77
        $patron_hashref = sip_to_koha_patron( $sip_patron, $server, $userid );
78
    }
79
80
    if ($patron) {
81
        $patron->update_password( $userid, $password )
82
          if $server->{update_password};
83
84
        if ( $server->{update} ) {    # A1, B1
85
            $patron->set($patron_hashref);
86
            $patron->store();
87
        }
88
        else {                        # C1, D1
89
            return ( 1, $patron->cardnumber, $patron->userid );
90
        }
91
    }
92
    elsif ( $server->{replicate} ) {    # A2, C2
93
        $patron_hashref->{password} = $password;
94
        $patron_hashref->{cardnumber} //= $userid;
95
96
        my $borrowernumber = C4::Members::AddMember(%$patron_hashref)
97
          or die "AddMember failed";
98
99
        C4::Members::Messaging::SetMessagingPreferencesFromDefaults(
100
            {
101
                borrowernumber => $borrowernumber,
102
                categorycode   => $patron_hashref->{categorycode}
103
            }
104
        );
105
    }
106
    else {
107
        return 0;    # B2, D2
108
    }
109
}
110
111
=head1 sip_to_koha_patron
112
113
my $patron_hashref = sip_to_koha_patron( $sip_patron_hashref, $sip_server, $patron_cardnumber );
114
115
Returns a Koha-style patron hashref derived from a SIP patron information response hashref and a server mapping definition.
116
117
=cut
118
119
sub sip_to_koha_patron {
120
    my $sip_patron = shift;
121
    my $server     = shift;
122
    my $cardnumber = shift;
123
124
    my $patron_hashref = { cardnumber => $cardnumber, userid => $cardnumber };
125
126
    my $mapping = $server->{mapping};
127
128
    foreach my $key ( keys %$mapping ) {
129
        my $data = $sip_patron->{ $mapping->{$key}->{is} };
130
        unless ( defined $data ) {
131
            $data = $mapping->{$key}->{content};
132
        }
133
134
        $patron_hashref->{$key} = $data;
135
    }
136
137
    # Fall back to default patron category if the supplied one doesn't exit
138
    my $patron_category =
139
      Koha::Patron::Categories->find( $patron_hashref->{categorycode} );
140
    unless ($patron_category) {
141
        my $default = $mapping->{categorycode}->{content};
142
        $patron_hashref->{categorycode} = $default;
143
    }
144
145
    return $patron_hashref;
146
}
147
148
=head1 sip_message_to_hashref
149
150
    my $sip_patron_hashref = sip_message_to_hashref( $sip_patron_information_response );
151
152
    Converts a raw SIP patron response message to a more useful hashref of fixed and variable fields.
153
154
=cut
155
156
sub sip_message_to_hashref {
157
    my ($data) = @_;
158
159
    my @parts = split( /\|/, $data );
160
161
    my $fixed_fields = shift(@parts);
162
    my $patron_status_field = substr( $fixed_fields, 2, 14 );
163
    my $patron_status;
164
    $patron_status->{charge_privileges_denied} =
165
      substr( $patron_status_field, 0, 1 );
166
    $patron_status->{renewal_privileges_denied} =
167
      substr( $patron_status_field, 1, 1 );
168
    $patron_status->{recall_privileges_denied} =
169
      substr( $patron_status_field, 2, 1 );
170
    $patron_status->{hold_privileges_denied} =
171
      substr( $patron_status_field, 3, 1 );
172
    $patron_status->{card_reported_lost} = substr( $patron_status_field, 4, 1 );
173
    $patron_status->{too_many_items_charged} =
174
      substr( $patron_status_field, 5, 1 );
175
    $patron_status->{too_many_items_overdue} =
176
      substr( $patron_status_field, 6, 1 );
177
    $patron_status->{too_many_renewals} = substr( $patron_status_field, 7, 1 );
178
    $patron_status->{too_many_claims_of_items_returned} =
179
      substr( $patron_status_field, 8, 1 );
180
    $patron_status->{too_many_items_lost} =
181
      substr( $patron_status_field, 9, 1 );
182
    $patron_status->{excessive_outstanding_fines} =
183
      substr( $patron_status_field, 10, 1 );
184
    $patron_status->{excessive_outstanding_fees} =
185
      substr( $patron_status_field, 11, 1 );
186
    $patron_status->{recall_overdue} = substr( $patron_status_field, 12, 1 );
187
    $patron_status->{too_many_items_billed} =
188
      substr( $patron_status_field, 13, 1 );
189
190
    my $hold_items_count = substr( $fixed_fields, 37, 4 );
191
192
    pop(@parts);
193
194
    my %fields = map { substr( $_, 0, 2 ) => substr( $_, 2 ) } @parts;
195
    $fields{patron_status}    = $patron_status;
196
    $fields{hold_items_count} = $hold_items_count;
197
198
    return \%fields;
199
}
200
201
=head1 select_server
202
203
    my $server = select_server( $userid, \@sip_servers );
204
205
    Selects the correct SIP server to be checked for a given userid
206
    and a set of ordered SIP servers.
207
208
=cut
209
210
sub select_server {
211
    my ( $userid, $sip_servers ) = @_;
212
213
    foreach my $server (@$sip_servers) {
214
        my $prefix = $server->{prefix};
215
216
        # First server without prefix we come across is an automatic match
217
        return $server unless $prefix;
218
219
        # If cardnumber starts with the server prefix, we have a match
220
        my $match = substr( $userid, 0, length($prefix) ) eq $prefix;
221
        return $server if $match;
222
    }
223
}
224
225
=head1 fetch_patron
226
227
    my $response = fetch_patron( $userid, $password, $server );
228
229
    Returns a raw patron information response for a given user id, password, and server.
230
231
=cut
232
233
sub fetch_patron {
234
    my ( $userid, $password, $server ) = @_;
235
236
    my $prefix = $server->{prefix};
237
238
    # If a cardnumber prefix is set for this server, strip it off the userid
239
    if ($prefix) {
240
        my $prefix_length = length($prefix);
241
        $userid = substr( $userid, $prefix_length );
242
    }
243
244
    my $host       = $server->{host};
245
    my $port       = $server->{port};
246
    my $sip_user   = $server->{userid};
247
    my $sip_pass   = $server->{password};
248
    my $location   = $server->{location};
249
    my $terminator = $server->{terminator};
250
251
    # Initial Login
252
    my $client = Koha::SIP::Client->new(
253
        {
254
            host       => $host,          # sip server ip
255
            port       => $port,          # sip server port
256
            sip_user   => $sip_user,      # sip user
257
            sip_pass   => $sip_pass,      # sip password
258
            location   => $location,      # sip location code
259
            terminator => $terminator,    # CR or CRLF
260
        }
261
    );
262
263
    my $response = $client->send( { message => 'login' } );
264
265
    # Incorrect userid/password for SIP server login attempt
266
    unless ( substr( $response, 0, 3 ) eq '941' ) {
267
        return -1;
268
    }
269
270
    # Patron Information request
271
    $response = $client->send(
272
        {
273
            message  => 'patron_information',
274
            patron   => $userid,
275
            password => $password,
276
        }
277
    );
278
279
    $response = sip_message_to_hashref($response);
280
281
    return $response;
282
}
283
284
1;
285
__END__
286
287
=head1 NAME
288
289
C4::Auth_with_sip - Authenticates Koha users against external SIP servers
290
291
=head1 SYNOPSIS
292
293
  use C4::Auth_with_sip;
294
295
=head1 Configuration
296
297
  This module is specific to SIP authentication. It requires one or more working external SIP servers.
298
299
  To use it:
300
    * Add a sip_servers block in KOHA_CONF
301
    * Establish field mapping in <mapping> element to map SIP2 field codes to Koha field names
302
303
  Make sure that ALL required fields are populated by your LDAP database (and mapped in KOHA_CONF).  
304
305
=head1 KOHA_CONF and field mapping
306
307
Example XML stanza for LDAP configuration in KOHA_CONF.
308
309
<config>
310
 <use_sip>1</use_sip> <!-- enables / disables the entire auth by SIP feature -->
311
312
 <sip_servers>
313
  <sip_server id="sip_server1">
314
   <precedence>1</precedence>    <!-- precedence is used to set the order in which sip servers will be checked for use -->
315
316
   <host>sip.example.com</host>
317
   <port>6001</port>
318
   <userid>koha_sip</userid>
319
   <password>PASSWORD</password>
320
   <location>LOC</location>
321
322
   <prefix>MPL</prefix>
323
324
   <replicate>1</replicate>                <!-- add new users from SIP to Koha database -->
325
   <update>1</update>                      <!-- update existing users in Koha database -->
326
   <update_password>1</update_password>    <!-- set to 0 if you don't want SIP passwords synced to the local database -->
327
328
   <mapping>                               <!-- match koha SQL field names to your SIP field names -->
329
    <surname      is="AE"                 ></surname>    <!-- Set surname to AE, no default -->
330
    <branchcode   is=""                   >MPL</branchcode>  <!-- Set branch code to MPL, always -->
331
    <categorycode is="PC"                 >PT</categorycode> <!-- Set patron category to PC, default to PT if not set or invalid -->
332
    <email        is="BE"                 ></email>    <!-- Set email to BE, no default -->
333
   </mapping>
334
  </sip_server>
335
 </sip_servers>
336
</config>
337
338
The <mapping> subelements establish the relationship between database table columns and SIP fields.
339
The element name is the database table column, with the "is" being set to the SIP field name.
340
Optionally, any content between the element tags is taken as a default value.
341
342
=head1 CONFIGURATION
343
344
Once a user has been accepted by the LDAP server, there are several possibilities for how Koha will behave, depending on 
345
your configuration and the presence of a matching Koha user in your local DB:
346
347
                          LOCAL
348
                          USER
349
 OPTION UPDATE REPLICATE  EXISTS?  RESULT
350
   A1      1       1        1      OK : We're updating them anyway.
351
   A2      1       1        0      OK : We're adding them anyway.
352
   B1      1       0        1      OK : We update them.
353
   B2      1       0        0     FAIL: We cannot add new user.
354
   C1      0       1        1      OK : We do nothing.  (Except possibly update password)
355
   C2      0       1        0      OK : We add the new user.
356
   D1      0       0        1      OK : We do nothing.  (Except possibly update password)
357
   D2      0       0        0     FAIL: We cannot add new user.
358
359
Note: failure here just means that Koha will fallback to checking the local DB.  That is, a given user could login with
360
their SIP password OR their local one.  If this is a problem, then you should enable update and supply a mapping for 
361
password.  Then the local value will be updated at successful LDAP login and the passwords will be synced.
362
363
If you choose NOT to update local users, the borrowers table will not be affected at all.
364
Note that this means that patron passwords may appear to change if LDAP is ever disabled, because
365
the local table never contained the SIP values.
366
367
=head2 replicate
368
369
If this tag is set to true, then the patron record in Koha will be created based on the recieved
370
values from the SIP server if the patron does not exists in the database already.
371
372
=head2 update
373
374
If this tag is set to true, then the patron record in Koha will be updated based on the
375
values recieved from the SIP server if a matching patron already exists.
376
377
=head2 update_password
378
379
If this tag is set to a true value, then the user's SIP password
380
will be stored (hashed) in the local Koha database. If you don't want this
381
to happen, then set the value of this to '0'. Note that if passwords are not
382
stored locally, and the connection to the SIP server fails, then the users
383
will not be able to log in at all.
384
385
This option works independently of 'update' and 'replicate'.
386
387
=cut
(-)a/t/Auth_with_sip.t (-1 / +114 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use Test::More tests => 9;
21
22
use_ok('C4::Auth_with_sip');
23
can_ok( 'C4::Auth_with_sip', 'checkpw_sip' );
24
25
my $sip_message =
26
q{64              00120170516    103344000000000001000000000000AOMPL|AA9876543210|AEKyle M. Hall|BLY|CQY|CC3|BD123 Liberty Ave Natrona Heights PA 15065|BEkyle@kylehall.info|BF1234567890|PB19810610|PCST|PIY|AFGreetings from Koha. |}
27
  . "\n";
28
my $sip_hashref_expected = {
29
    'BL'            => 'Y',
30
    'AA'            => '9876543210',
31
    'AF'            => 'Greetings from Koha. ',
32
    'PI'            => 'Y',
33
    'patron_status' => {
34
        'too_many_items_overdue'            => ' ',
35
        'too_many_items_lost'               => ' ',
36
        'too_many_claims_of_items_returned' => ' ',
37
        'recall_overdue'                    => ' ',
38
        'excessive_outstanding_fees'        => ' ',
39
        'hold_privileges_denied'            => ' ',
40
        'too_many_items_billed'             => ' ',
41
        'excessive_outstanding_fines'       => ' ',
42
        'too_many_renewals'                 => ' ',
43
        'renewal_privileges_denied'         => ' ',
44
        'recall_privileges_denied'          => ' ',
45
        'charge_privileges_denied'          => ' ',
46
        'card_reported_lost'                => ' ',
47
        'too_many_items_charged'            => ' '
48
    },
49
    'PC'               => 'ST',
50
    'hold_items_count' => '0000',
51
    'CQ'               => 'Y',
52
    'PB'               => '19810610',
53
    'CC'               => '3',
54
    'AE'               => 'Kyle M. Hall',
55
    'BE'               => 'kyle@kylehall.info',
56
    'BF'               => '1234567890',
57
    'BD'               => '123 Liberty Ave Natrona Heights PA 15065'
58
};
59
my $sip_hashref = C4::Auth_with_sip::sip_message_to_hashref($sip_message);
60
is_deeply( $sip_hashref, $sip_hashref_expected,
61
    'SIP message converted to hashref correctly' );
62
63
my $servers = [
64
    { priority => 1, prefix => 'AAA' },
65
    { priority => 2, prefix => 'BBB' },
66
    { priority => 3, prefix => 'CCC' },
67
    { priority => 4, prefix => undef },
68
    { priority => 4, prefix => 'XXX' },
69
];
70
my $server = C4::Auth_with_sip::select_server( 'AAA123', $servers );
71
is( $server->{priority}, 1, 'Got correct server with prefix AAA' );
72
$server = C4::Auth_with_sip::select_server( 'BBB123', $servers );
73
is( $server->{priority}, 2, 'Got correct server with prefix BBB' );
74
$server = C4::Auth_with_sip::select_server( 'CCC123', $servers );
75
is( $server->{priority}, 3, 'Got correct server with prefix CCC' );
76
$server = C4::Auth_with_sip::select_server( '123', $servers );
77
is( $server->{priority}, 4, 'Got correct server with no prefix' );
78
$server = C4::Auth_with_sip::select_server( 'XXX123', $servers );
79
is( $server->{priority}, 4,
80
    'Got correct server with prefix XXX, server with no prefix is a catch-all'
81
);
82
83
$server = {
84
    'precedence' => '1',
85
    'prefix'     => 'MPL',
86
    'mapping'    => {
87
        'categorycode' => {
88
            'content' => 'PT',
89
            'is'      => 'PC'
90
        },
91
        'email' => {
92
            'is' => 'BE'
93
        },
94
        'branchcode' => {
95
            'is'      => '',
96
            'content' => 'MPL'
97
        },
98
        'surname' => {
99
            'is' => 'AE'
100
        }
101
    },
102
};
103
my $patron_hashref_expected = {
104
    'email'        => 'kyle@kylehall.info',
105
    'surname'      => 'Kyle M. Hall',
106
    'branchcode'   => 'MPL',
107
    'categorycode' => 'ST',
108
    'cardnumber'   => 'MPL9876543210',
109
    'userid'       => 'MPL9876543210'
110
};
111
my $patron_hashref =
112
  C4::Auth_with_sip::sip_to_koha_patron( $sip_hashref, $server,
113
    'MPL9876543210' );
114
is_deeply( $patron_hashref, $patron_hashref_expected, 'Koha-style patron hashref matches expected patron hashref' );

Return to bug 18768