Bug 35918: Add test
[koha.git] / t / db_dependent / Auth.t
1 #!/usr/bin/perl
2
3 use Modern::Perl;
4
5 use CGI qw ( -utf8 );
6
7 use Test::MockObject;
8 use Test::MockModule;
9 use List::MoreUtils qw/all any none/;
10 use Test::More tests => 21;
11 use Test::Warn;
12 use t::lib::Mocks;
13 use t::lib::TestBuilder;
14
15 use C4::Auth;
16 use C4::Members;
17 use Koha::AuthUtils qw/hash_password/;
18 use Koha::Database;
19 use Koha::Patrons;
20 use Koha::Auth::TwoFactorAuth;
21
22 BEGIN {
23     use_ok(
24         'C4::Auth',
25         qw( checkauth haspermission track_login_daily checkpw get_template_and_user checkpw_hash get_cataloguing_page_permissions )
26     );
27 }
28
29 my $schema  = Koha::Database->schema;
30 my $builder = t::lib::TestBuilder->new;
31
32 # FIXME: SessionStorage defaults to mysql, but it seems to break transaction
33 # handling
34 t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' );
35 t::lib::Mocks::mock_preference( 'PrivacyPolicyConsent', '' ); # Disabled
36
37 # To silence useless warnings
38 $ENV{REMOTE_ADDR} = '127.0.0.1';
39
40 $schema->storage->txn_begin;
41
42 subtest 'checkauth() tests' => sub {
43
44     plan tests => 8;
45
46     my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { flags => undef } });
47
48     # Mock a CGI object with real userid param
49     my $cgi = Test::MockObject->new();
50     $cgi->mock(
51         'param',
52         sub {
53             my $var = shift;
54             if ( $var eq 'userid' ) { return $patron->userid; }
55         }
56     );
57     $cgi->mock( 'cookie', sub { return; } );
58     $cgi->mock( 'request_method', sub { return 'POST' } );
59
60     my $authnotrequired = 1;
61     my ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, $authnotrequired );
62
63     is( $userid, undef, 'checkauth() returns undef for userid if no logged in user (Bug 18275)' );
64
65     my $db_user_id = C4::Context->config('user');
66     my $db_user_pass = C4::Context->config('pass');
67     $cgi = Test::MockObject->new();
68     $cgi->mock( 'cookie', sub { return; } );
69     $cgi->mock( 'param', sub {
70             my ( $self, $param ) = @_;
71             if ( $param eq 'userid' ) { return $db_user_id; }
72             elsif ( $param eq 'password' ) { return $db_user_pass; }
73             else { return; }
74         });
75     $cgi->mock( 'request_method', sub { return 'POST' } );
76     ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, $authnotrequired );
77     is ( $userid, undef, 'If DB user is used, it should not be logged in' );
78
79     my $is_allowed = C4::Auth::haspermission( $db_user_id, { can_do => 'everything' } );
80
81     # FIXME This belongs to t/db_dependent/Auth/haspermission.t but we do not want to c/p the pervious mock statements
82     ok( !$is_allowed, 'DB user should not have any permissions');
83
84     subtest 'Prevent authentication when sending credential via GET' => sub {
85
86         plan tests => 2;
87
88         my $patron = $builder->build_object(
89             { class => 'Koha::Patrons', value => { flags => 1 } } );
90         my $password = 'password';
91         t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
92         $patron->set_password( { password => $password } );
93         $cgi = Test::MockObject->new();
94         $cgi->mock( 'cookie', sub { return; } );
95         $cgi->mock(
96             'param',
97             sub {
98                 my ( $self, $param ) = @_;
99                 if    ( $param eq 'userid' )   { return $patron->userid; }
100                 elsif ( $param eq 'password' ) { return $password; }
101                 else                           { return; }
102             }
103         );
104
105         $cgi->mock( 'request_method', sub { return 'POST' } );
106         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired' );
107         is( $userid, $patron->userid, 'If librarian user is used and password with POST, they should be logged in' );
108
109         $cgi->mock( 'request_method', sub { return 'GET' } );
110         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired' );
111         is( $userid, undef, 'If librarian user is used and password with GET, they should not be logged in' );
112     };
113
114     subtest 'Template params tests (password_expired)' => sub {
115
116         plan tests => 1;
117
118         my $password_expired;
119
120         my $patron_class = Test::MockModule->new('Koha::Patron');
121         $patron_class->mock( 'password_expired', sub { return $password_expired; } );
122
123         my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { flags => 1 } });
124         my $password = 'password';
125         t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
126         $patron->set_password( { password => $password } );
127
128         my $cgi_mock = Test::MockModule->new('CGI')->mock( 'request_method', 'POST' );
129         my $cgi = CGI->new;
130         $cgi->param( -name => 'userid',   -value => $patron->userid );
131         $cgi->param( -name => 'password', -value => $password );
132
133         my $auth = Test::MockModule->new( 'C4::Auth' );
134         # Tests will fail if we hit safe_exit
135         $auth->mock( 'safe_exit', sub { return } );
136
137         my ( $userid, $cookie, $sessionID, $flags );
138
139         {
140             t::lib::Mocks::mock_preference( 'DumpTemplateVarsOpac', 1 );
141             # checkauth will redirect and safe_exit if not authenticated and not authorized
142             local *STDOUT;
143             my $stdout;
144             open STDOUT, '>', \$stdout;
145
146             # Password has expired
147             $password_expired = 1;
148             C4::Auth::checkauth( $cgi, 0, { catalogue => 1 } );
149             like( $stdout, qr{'password_has_expired' => 1}, 'password_has_expired is set to 1' );
150
151             close STDOUT;
152         };
153     };
154
155     subtest 'Reset auth state when changing users' => sub {
156
157         #NOTE: It's easiest to detect this when changing to a non-existent user, since
158         #that should trigger a redirect to login (instead of returning a session cookie)
159         plan tests => 2;
160         my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { flags => undef } } );
161
162         my $session = C4::Auth::get_session();
163         $session->param( 'number',    $patron->id );
164         $session->param( 'id',        $patron->userid );
165         $session->param( 'ip',        '1.2.3.4' );
166         $session->param( 'lasttime',  time() );
167         $session->param( 'interface', 'intranet' );
168         $session->flush;
169         my $sessionID = $session->id;
170         C4::Context->_new_userenv($sessionID);
171
172         my ($return) =
173             C4::Auth::check_cookie_auth( $sessionID, undef, { skip_version_check => 1, remote_addr => '1.2.3.4' } );
174         is( $return, 'ok', 'Patron authenticated' );
175
176         my $mock1 = Test::MockModule->new('C4::Auth');
177         $mock1->mock( 'safe_exit', sub { return 'safe_exit_redirect' } );
178         my $mock2 = Test::MockModule->new('CGI');
179         $mock2->mock( 'request_method', 'POST' );
180         $mock2->mock( 'cookie',         sub { return $sessionID; } );    # oversimplified..
181         my $cgi = CGI->new;
182
183         $cgi->param( -name => 'userid',             -value => 'Bond' );
184         $cgi->param( -name => 'password',           -value => 'James Bond' );
185         $cgi->param( -name => 'koha_login_context', -value => 1 );
186         my ( @return, $stdout );
187         {
188             local *STDOUT;
189             local %ENV;
190             $ENV{REMOTE_ADDR} = '1.2.3.4';
191             open STDOUT, '>', \$stdout;
192             @return = C4::Auth::checkauth( $cgi, 0, {} );
193             close STDOUT;
194         }
195         is( $return[0], 'safe_exit_redirect', 'Changing to non-existent user causes a redirect to login' );
196     };
197
198
199     subtest 'While still logged in, relogin with another user' => sub {
200         plan tests => 6;
201
202         my $patron = $builder->build_object({ class => 'Koha::Patrons', value => {} });
203         my $patron2 = $builder->build_object({ class => 'Koha::Patrons', value => {} });
204         # Create 'former' session
205         my $session = C4::Auth::get_session();
206         $session->param( 'number',       $patron->id );
207         $session->param( 'id',           $patron->userid );
208         $session->param( 'ip',           '1.2.3.4' );
209         $session->param( 'lasttime',     time() );
210         $session->param( 'interface',    'opac' );
211         $session->flush;
212         my $sessionID = $session->id;
213         C4::Context->_new_userenv($sessionID);
214
215         my ( $return ) = C4::Auth::check_cookie_auth( $sessionID, undef, { skip_version_check => 1, remote_addr => '1.2.3.4' } );
216         is( $return, 'ok', 'Former session in shape now' );
217
218         my $mock1 = Test::MockModule->new('C4::Auth');
219         $mock1->mock( 'safe_exit', sub {} );
220         my $mock2 = Test::MockModule->new('CGI');
221         $mock2->mock( 'request_method', 'POST' );
222         $mock2->mock( 'cookie', sub { return $sessionID; } ); # oversimplified..
223         my $cgi = CGI->new;
224         my $password = 'Incr3d1blyZtr@ng93$';
225         $patron2->set_password({ password => $password });
226         $cgi->param( -name => 'userid',             -value => $patron2->userid );
227         $cgi->param( -name => 'password',           -value => $password );
228         $cgi->param( -name => 'koha_login_context', -value => 1 );
229         my ( @return, $stdout );
230         {
231             local *STDOUT;
232             local %ENV;
233             $ENV{REMOTE_ADDR} = '1.2.3.4';
234             open STDOUT, '>', \$stdout;
235             @return = C4::Auth::checkauth( $cgi, 0, {} );
236             close STDOUT;
237         }
238         # Note: We can test return values from checkauth here since we mocked the safe_exit after the Redirect 303
239         is( $return[0], $patron2->userid, 'Login of patron2 approved' );
240         isnt( $return[2], $sessionID, 'Did not return previous session ID' );
241         ok( $return[2], 'New session ID not empty' );
242
243         # Similar situation: Relogin with former session of $patron, new user $patron2 has no permissions
244         $patron2->flags(undef)->store;
245         $session->param( 'number',       $patron->id );
246         $session->param( 'id',           $patron->userid );
247         $session->param( 'interface',    'intranet' );
248         $session->flush;
249         $sessionID = $session->id;
250         C4::Context->_new_userenv($sessionID);
251         $cgi->param( -name => 'userid',             -value => $patron2->userid );
252         $cgi->param( -name => 'password',           -value => $password );
253         $cgi->param( -name => 'koha_login_context', -value => 1 );
254         {
255             local *STDOUT;
256             local %ENV;
257             $ENV{REMOTE_ADDR} = '1.2.3.4';
258             $stdout = q{};
259             open STDOUT, '>', \$stdout;
260             @return = C4::Auth::checkauth( $cgi, 0, { catalogue => 1 }, 'intranet' ); # patron2 has no catalogue perm
261             close STDOUT;
262         }
263         like( $stdout, qr/You do not have permission to access this page/, 'No permission response' );
264         is( @return, 0, 'checkauth returned failure' );
265     };
266
267     subtest 'Two-factor authentication' => sub {
268         plan tests => 18;
269
270         my $patron = $builder->build_object(
271             { class => 'Koha::Patrons', value => { flags => 1 } } );
272         my $password = 'password';
273         $patron->set_password( { password => $password } );
274         $cgi = Test::MockObject->new();
275
276         my $otp_token;
277         our ( $logout, $sessionID, $verified );
278         $cgi->mock(
279             'param',
280             sub {
281                 my ( $self, $param ) = @_;
282                 if    ( $param eq 'userid' )    { return $patron->userid; }
283                 elsif ( $param eq 'password' )  { return $password; }
284                 elsif ( $param eq 'otp_token' ) { return $otp_token; }
285                 elsif ( $param eq 'logout.x' )  { return $logout; }
286                 else                            { return; }
287             }
288         );
289         $cgi->mock( 'request_method', sub { return 'POST' } );
290         $cgi->mock( 'cookie', sub { return $sessionID } );
291
292         my $two_factor_auth = Test::MockModule->new( 'Koha::Auth::TwoFactorAuth' );
293         $two_factor_auth->mock( 'verify', sub {$verified} );
294
295         my ( $userid, $cookie, $flags );
296         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' );
297
298         sub logout {
299             my $cgi = shift;
300             $logout = 1;
301             undef $sessionID;
302             C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' );
303             $logout = 0;
304         }
305
306         t::lib::Mocks::mock_preference( 'TwoFactorAuthentication', 'disabled' );
307         $patron->auth_method('password')->store;
308         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' );
309         is( $userid, $patron->userid, 'Succesful login' );
310         is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), undef, 'Second auth not required' );
311         logout($cgi);
312
313         $patron->auth_method('two-factor')->store;
314         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' );
315         is( $userid, $patron->userid, 'Succesful login' );
316         is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), undef, 'Second auth not required' );
317         logout($cgi);
318
319         t::lib::Mocks::mock_preference( 'TwoFactorAuthentication', 'enabled' );
320         t::lib::Mocks::mock_config('encryption_key', '1234tH1s=t&st');
321         $patron->auth_method('password')->store;
322         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' );
323         is( $userid, $patron->userid, 'Succesful login' );
324         is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), undef, 'Second auth not required' );
325         logout($cgi);
326
327         $patron->encode_secret('one_secret');
328         $patron->auth_method('two-factor');
329         $patron->store;
330         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' );
331         is( $userid, $patron->userid, 'Succesful login' );
332         my $session = C4::Auth::get_session($sessionID);
333         is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), 1, 'Second auth required' );
334
335         # Wrong OTP token
336         $otp_token = "wrong";
337         $verified = 0;
338         $patron->auth_method('two-factor')->store;
339         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' );
340         is( $userid, $patron->userid, 'Succesful login' );
341         is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), 1, 'Second auth still required after wrong OTP token' );
342
343         $otp_token = "good";
344         $verified = 1;
345         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' );
346         is( $userid, $patron->userid, 'Succesful login' );
347         is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), 0, 'Second auth no longer required if OTP token has been verified' );
348         logout($cgi);
349
350         t::lib::Mocks::mock_preference( 'TwoFactorAuthentication', 'enforced' );
351         $patron->auth_method('password')->store;
352         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' );
353         is( $userid, $patron->userid, 'Succesful login' );
354         is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA-setup'), 1, 'Setup 2FA required' );
355         logout($cgi);
356
357         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'opac' );
358         is( $userid, $patron->userid, 'Succesful login at the OPAC' );
359         is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), undef, 'No second auth required at the OPAC' );
360
361         #
362         t::lib::Mocks::mock_preference( 'TwoFactorAuthentication', 'disabled' );
363         $session = C4::Auth::get_session($sessionID);
364         $session->param('waiting-for-2FA', 1);
365         $session->flush;
366         my ($auth_status, undef ) = C4::Auth::check_cookie_auth($sessionID, undef );
367         is( $auth_status, 'ok', 'User authenticated, pref was disabled, access OK' );
368         $session->param('waiting-for-2FA', 0);
369         $session->param('waiting-for-2FA-setup', 1);
370         $session->flush;
371         ($auth_status, undef ) = C4::Auth::check_cookie_auth($sessionID, undef );
372         is( $auth_status, 'ok', 'User waiting for 2FA setup, pref was disabled, access OK' );
373     };
374
375     C4::Context->_new_userenv; # For next tests
376
377 };
378
379 subtest 'track_login_daily tests' => sub {
380
381     plan tests => 5;
382
383     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
384     my $userid = $patron->userid;
385
386     $patron->lastseen( undef );
387     $patron->store();
388
389     my $cache     = Koha::Caches->get_instance();
390     my $cache_key = "track_login_" . $patron->userid;
391     $cache->clear_from_cache($cache_key);
392
393     t::lib::Mocks::mock_preference( 'TrackLastPatronActivity', '1' );
394
395     is( $patron->lastseen, undef, 'Patron should have not last seen when newly created' );
396
397     C4::Auth::track_login_daily( $userid );
398     $patron->_result()->discard_changes();
399     isnt( $patron->lastseen, undef, 'Patron should have last seen set when TrackLastPatronActivity = 1' );
400
401     sleep(1); # We need to wait a tiny bit to make sure the timestamp will be different
402     my $last_seen = $patron->lastseen;
403     C4::Auth::track_login_daily( $userid );
404     $patron->_result()->discard_changes();
405     is( $patron->lastseen, $last_seen, 'Patron last seen should still be unchanged' );
406
407     $cache->clear_from_cache($cache_key);
408     C4::Auth::track_login_daily( $userid );
409     $patron->_result()->discard_changes();
410     isnt( $patron->lastseen, $last_seen, 'Patron last seen should be changed if we cleared the cache' );
411
412     t::lib::Mocks::mock_preference( 'TrackLastPatronActivity', '0' );
413     $patron->lastseen( undef )->store;
414     $cache->clear_from_cache($cache_key);
415     C4::Auth::track_login_daily( $userid );
416     $patron->_result()->discard_changes();
417     is( $patron->lastseen, undef, 'Patron should still have last seen unchanged when TrackLastPatronActivity = 0' );
418
419 };
420
421 subtest 'no_set_userenv parameter tests' => sub {
422
423     plan tests => 7;
424
425     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
426     my $patron  = $builder->build_object( { class => 'Koha::Patrons' } );
427     my $password = 'password';
428
429     t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
430     $patron->set_password({ password => $password });
431
432     ok( checkpw( $patron->userid, $password, undef, undef, 1 ), 'checkpw returns true' );
433     is( C4::Context->userenv, undef, 'Userenv should be undef as required' );
434     C4::Context->_new_userenv('DUMMY SESSION');
435     C4::Context->set_userenv(0,0,0,'firstname','surname', $library->branchcode, 'Library 1', 0, '', '');
436     is( C4::Context->userenv->{branch}, $library->branchcode, 'Userenv gives correct branch' );
437     ok( checkpw( $patron->userid, $password, undef, undef, 1 ), 'checkpw returns true' );
438     is( C4::Context->userenv->{branch}, $library->branchcode, 'Userenv branch is preserved if no_set_userenv is true' );
439     ok( checkpw( $patron->userid, $password, undef, undef, 0 ), 'checkpw still returns true' );
440     isnt( C4::Context->userenv->{branch}, $library->branchcode, 'Userenv branch is overwritten if no_set_userenv is false' );
441 };
442
443 subtest 'checkpw lockout tests' => sub {
444
445     plan tests => 5;
446
447     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
448     my $patron  = $builder->build_object( { class => 'Koha::Patrons' } );
449     my $password = 'password';
450     t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
451     t::lib::Mocks::mock_preference( 'FailedLoginAttempts', 1 );
452     $patron->set_password({ password => $password });
453
454     my ( $checkpw, undef, undef ) = checkpw( $patron->cardnumber, $password, undef, undef, 1 );
455     ok( $checkpw, 'checkpw returns true with right password when logging in via cardnumber' );
456     ( $checkpw, undef, undef ) = checkpw( $patron->userid, "wrong_password", undef, undef, 1 );
457     is( $checkpw, 0, 'checkpw returns false when given wrong password' );
458     $patron = $patron->get_from_storage;
459     is( $patron->account_locked, 1, "Account is locked from failed login");
460     ( $checkpw, undef, undef ) = checkpw( $patron->userid, $password, undef, undef, 1 );
461     is( $checkpw, undef, 'checkpw returns undef with right password when account locked' );
462     ( $checkpw, undef, undef ) = checkpw( $patron->cardnumber, $password, undef, undef, 1 );
463     is( $checkpw, undef, 'checkpw returns undefwith right password when logging in via cardnumber if account locked' );
464
465 };
466
467 # get_template_and_user tests
468
469 subtest 'get_template_and_user' => sub {   # Tests for the language URL parameter
470
471     sub MockedCheckauth {
472         my ($query,$authnotrequired,$flagsrequired,$type) = @_;
473         # return vars
474         my $userid = 'cobain';
475         my $sessionID = 234;
476         # we don't need to bother about permissions for this test
477         my $flags = {
478             superlibrarian    => 1, acquisition       => 0,
479             borrowers         => 0,
480             catalogue         => 1, circulate         => 0,
481             coursereserves    => 0, editauthorities   => 0,
482             editcatalogue     => 0,
483             parameters        => 0, permissions       => 0,
484             plugins           => 0, reports           => 0,
485             reserveforothers  => 0, serials           => 0,
486             staffaccess       => 0, tools             => 0,
487             updatecharges     => 0
488         };
489
490         my $session_cookie = $query->cookie(
491             -name => 'CGISESSID',
492             -value    => 'nirvana',
493             -HttpOnly => 1
494         );
495
496         return ( $userid, [ $session_cookie ], $sessionID, $flags );
497     }
498
499     # Mock checkauth, build the scenario
500     my $auth = Test::MockModule->new( 'C4::Auth' );
501     $auth->mock( 'checkauth', \&MockedCheckauth );
502
503     # Make sure 'EnableOpacSearchHistory' is set
504     t::lib::Mocks::mock_preference('EnableOpacSearchHistory',1);
505     # Enable es-ES for the OPAC and staff interfaces
506     t::lib::Mocks::mock_preference('OPACLanguages','en,es-ES');
507     t::lib::Mocks::mock_preference('language','en,es-ES');
508
509     # we need a session cookie
510     $ENV{"SERVER_PORT"} = 80;
511     $ENV{"HTTP_COOKIE"} = 'CGISESSID=nirvana';
512
513     my $query = CGI->new;
514     $query->param('language','es-ES');
515
516     my ( $template, $loggedinuser, $cookies ) = get_template_and_user(
517         {
518             template_name   => "about.tt",
519             query           => $query,
520             type            => "opac",
521             authnotrequired => 1,
522             flagsrequired   => { catalogue => 1 },
523             debug           => 1
524         }
525     );
526
527     ok ( ( all { ref($_) eq 'CGI::Cookie' } @$cookies ),
528             'BZ9735: the cookies array is flat' );
529
530     # new query, with non-existent language (we only have en and es-ES)
531     $query->param('language','tomas');
532
533     ( $template, $loggedinuser, $cookies ) = get_template_and_user(
534         {
535             template_name   => "about.tt",
536             query           => $query,
537             type            => "opac",
538             authnotrequired => 1,
539             flagsrequired   => { catalogue => 1 },
540             debug           => 1
541         }
542     );
543
544     ok( ( none { $_->name eq 'KohaOpacLanguage' and $_->value eq 'tomas' } @$cookies ),
545         'BZ9735: invalid language, it is not set');
546
547     ok( ( any { $_->name eq 'KohaOpacLanguage' and $_->value eq 'en' } @$cookies ),
548         'BZ9735: invalid language, then default to en');
549
550     for my $template_name (
551         qw(
552             ../../../../../../../../../../../../../../../etc/passwd
553             test/../../../../../../../../../../../../../../etc/passwd
554             /etc/passwd
555             test/does_not_finished_by_tt_t
556         )
557     ) {
558         eval {
559             ( $template, $loggedinuser, $cookies ) = get_template_and_user(
560                 {
561                     template_name   => $template_name,
562                     query           => $query,
563                     type            => "intranet",
564                     authnotrequired => 1,
565                     flagsrequired   => { catalogue => 1 },
566                 }
567             );
568         };
569         like ( $@, qr(bad template path), "The file $template_name should not be accessible" );
570     }
571     ( $template, $loggedinuser, $cookies ) = get_template_and_user(
572         {
573             template_name   => 'errors/errorpage.tt',
574             query           => $query,
575             type            => "intranet",
576             authnotrequired => 1,
577             flagsrequired   => { catalogue => 1 },
578         }
579     );
580     my $file_exists = ( -f $template->{filename} ) ? 1 : 0;
581     is ( $file_exists, 1, 'The file errors/errorpage.tt should be accessible (contains integers)' );
582
583     # Regression test for env opac search limit override
584     $ENV{"OPAC_SEARCH_LIMIT"} = "branch:CPL";
585     $ENV{"OPAC_LIMIT_OVERRIDE"} = 1;
586
587     ( $template, $loggedinuser, $cookies) = get_template_and_user(
588         {
589             template_name => 'opac-main.tt',
590             query => $query,
591             type => 'opac',
592             authnotrequired => 1,
593         }
594     );
595     is($template->{VARS}->{'opac_name'}, "CPL", "Opac name was set correctly");
596     is($template->{VARS}->{'opac_search_limit'}, "branch:CPL", "Search limit was set correctly");
597
598     $ENV{"OPAC_SEARCH_LIMIT"} = "branch:multibranch-19";
599
600     ( $template, $loggedinuser, $cookies) = get_template_and_user(
601         {
602             template_name => 'opac-main.tt',
603             query => $query,
604             type => 'opac',
605             authnotrequired => 1,
606         }
607     );
608     is($template->{VARS}->{'opac_name'}, "multibranch-19", "Opac name was set correctly");
609     is($template->{VARS}->{'opac_search_limit'}, "branch:multibranch-19", "Search limit was set correctly");
610
611     delete $ENV{"HTTP_COOKIE"};
612 };
613
614 # Check that there is always an OPACBaseURL set.
615 my $input = CGI->new();
616 my ( $template1, $borrowernumber, $cookie );
617 ( $template1, $borrowernumber, $cookie ) = get_template_and_user(
618     {
619         template_name => "opac-detail.tt",
620         type => "opac",
621         query => $input,
622         authnotrequired => 1,
623     }
624 );
625
626 ok( ( any { 'OPACBaseURL' eq $_ } keys %{$template1->{VARS}} ),
627     'OPACBaseURL is in OPAC template' );
628
629 my ( $template2 );
630 ( $template2, $borrowernumber, $cookie ) = get_template_and_user(
631     {
632         template_name => "catalogue/detail.tt",
633         type => "intranet",
634         query => $input,
635         authnotrequired => 1,
636     }
637 );
638
639 ok( ( any { 'OPACBaseURL' eq $_ } keys %{$template2->{VARS}} ),
640     'OPACBaseURL is in Staff template' );
641
642 my $hash1 = hash_password('password');
643 my $hash2 = hash_password('password');
644
645 ok(C4::Auth::checkpw_hash('password', $hash1), 'password validates with first hash');
646 ok(C4::Auth::checkpw_hash('password', $hash2), 'password validates with second hash');
647
648 subtest 'Check value of login_attempts in checkpw' => sub {
649     plan tests => 11;
650
651     t::lib::Mocks::mock_preference('FailedLoginAttempts', 3);
652
653     # Only interested here in regular login
654     $C4::Auth::cas  = 0;
655     $C4::Auth::ldap = 0;
656
657     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
658     $patron->login_attempts(2);
659     $patron->password('123')->store; # yes, deliberately not hashed
660
661     is( $patron->account_locked, 0, 'Patron not locked' );
662     my @test = checkpw( $patron->userid, '123', undef, 'opac', 1 );
663         # Note: 123 will not be hashed to 123 !
664     is( $test[0], 0, 'checkpw should have failed' );
665     $patron->discard_changes; # refresh
666     is( $patron->login_attempts, 3, 'Login attempts increased' );
667     is( $patron->account_locked, 1, 'Check locked status' );
668
669     # And another try to go over the limit: different return value!
670     @test = checkpw( $patron->userid, '123', undef, 'opac', 1 );
671     is( @test, 0, 'checkpw failed again and returns nothing now' );
672     $patron->discard_changes; # refresh
673     is( $patron->login_attempts, 3, 'Login attempts not increased anymore' );
674
675     # Administrative lockout cannot be undone?
676     # Pass the right password now (or: add a nice mock).
677     my $auth = Test::MockModule->new( 'C4::Auth' );
678     $auth->mock( 'checkpw_hash', sub { return 1; } ); # not for production :)
679     $patron->login_attempts(0)->store;
680     @test = checkpw( $patron->userid, '123', undef, 'opac', 1 );
681     is( $test[0], 1, 'Build confidence in the mock' );
682     $patron->login_attempts(-1)->store;
683     is( $patron->account_locked, 1, 'Check administrative lockout' );
684     @test = checkpw( $patron->userid, '123', undef, 'opac', 1 );
685     is( @test, 0, 'checkpw gave red' );
686     $patron->discard_changes; # refresh
687     is( $patron->login_attempts, -1, 'Still locked out' );
688     t::lib::Mocks::mock_preference('FailedLoginAttempts', ''); # disable
689     is( $patron->account_locked, 1, 'Check administrative lockout without pref' );
690 };
691
692 subtest 'Check value of login_attempts in checkpw' => sub {
693     plan tests => 2;
694
695     t::lib::Mocks::mock_preference('FailedLoginAttempts', 3);
696     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
697     $patron->set_password({ password => '123', skip_validation => 1 });
698
699     my @test = checkpw( $patron->userid, '123', undef, 'opac', 1 );
700     is( $test[0], 1, 'Patron authenticated correctly' );
701
702     $patron->password_expiration_date('2020-01-01')->store;
703     @test = checkpw( $patron->userid, '123', undef, 'opac', 1 );
704     is( $test[0], -2, 'Patron returned as expired correctly' );
705
706 };
707
708 subtest '_timeout_syspref' => sub {
709
710     plan tests => 6;
711
712     t::lib::Mocks::mock_preference('timeout', "100");
713     is( C4::Auth::_timeout_syspref, 100, );
714
715     t::lib::Mocks::mock_preference('timeout', "2d");
716     is( C4::Auth::_timeout_syspref, 2*86400, );
717
718     t::lib::Mocks::mock_preference('timeout', "2D");
719     is( C4::Auth::_timeout_syspref, 2*86400, );
720
721     t::lib::Mocks::mock_preference('timeout', "10h");
722     is( C4::Auth::_timeout_syspref, 10*3600, );
723
724     t::lib::Mocks::mock_preference('timeout', "10x");
725     warning_is
726         { is( C4::Auth::_timeout_syspref, 600, ); }
727         "The value of the system preference 'timeout' is not correct, defaulting to 600",
728         'Bad values throw a warning and fallback to 600';
729 };
730
731 subtest 'check_cookie_auth' => sub {
732     plan tests => 4;
733
734     t::lib::Mocks::mock_preference('timeout', "1d"); # back to default
735
736     my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { flags => 1 } });
737
738     # Mock a CGI object with real userid param
739     my $cgi = Test::MockObject->new();
740     $cgi->mock(
741         'param',
742         sub {
743             my $var = shift;
744             if ( $var eq 'userid' ) { return $patron->userid; }
745         }
746     );
747     $cgi->mock('multi_param', sub {return q{}} );
748     $cgi->mock( 'cookie', sub { return; } );
749     $cgi->mock( 'request_method', sub { return 'POST' } );
750
751     $ENV{REMOTE_ADDR} = '127.0.0.1';
752
753     # Setting authnotrequired=1 or we wont' hit the return but the end of the sub that prints headers
754     my ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 1 );
755
756     my ($auth_status, $session) = C4::Auth::check_cookie_auth($sessionID);
757     isnt( $auth_status, 'ok', 'check_cookie_auth should not return ok if the user has not been authenticated before if no permissions needed' );
758     is( $auth_status, 'anon', 'check_cookie_auth should return anon if the user has not been authenticated before and no permissions needed' );
759
760     ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 1 );
761
762     ($auth_status, $session) = C4::Auth::check_cookie_auth($sessionID, {catalogue => 1});
763     isnt( $auth_status, 'ok', 'check_cookie_auth should not return ok if the user has not been authenticated before and permissions needed' );
764     is( $auth_status, 'anon', 'check_cookie_auth should return anon if the user has not been authenticated before and permissions needed' );
765
766     #FIXME We should have a test to cover 'failed' status when a user has logged in, but doesn't have permission
767 };
768
769 subtest 'checkauth & check_cookie_auth' => sub {
770     plan tests => 35;
771
772     # flags = 4 => { catalogue => 1 }
773     my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { flags => 4 } });
774     my $password = 'password';
775     t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
776     $patron->set_password( { password => $password } );
777
778     my $cgi_mock = Test::MockModule->new('CGI');
779     $cgi_mock->mock( 'request_method', sub { return 'POST' } );
780
781     my $cgi = CGI->new;
782
783     my $auth = Test::MockModule->new( 'C4::Auth' );
784     # Tests will fail if we hit safe_exit
785     $auth->mock( 'safe_exit', sub { return } );
786
787     my ( $userid, $cookie, $sessionID, $flags );
788     {
789         # checkauth will redirect and safe_exit if not authenticated and not authorized
790         local *STDOUT;
791         my $stdout;
792         open STDOUT, '>', \$stdout;
793         C4::Auth::checkauth($cgi, 0, {catalogue => 1});
794         like( $stdout, qr{<title>\s*Log in to your account} );
795         $sessionID = ( $stdout =~ m{Set-Cookie: CGISESSID=((\d|\w)+);} ) ? $1 : undef;
796         ok($sessionID);
797         close STDOUT;
798     };
799
800     my $first_sessionID = $sessionID;
801
802     $ENV{"HTTP_COOKIE"} = "CGISESSID=$sessionID";
803     # Not authenticated yet, checkauth didn't return the session
804     {
805         local *STDOUT;
806         my $stdout;
807         open STDOUT, '>', \$stdout;
808         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth($cgi, 0, {catalogue => 1} );
809         close STDOUT;
810     }
811     is( $sessionID, undef);
812     is( $userid, undef);
813
814     # Sending undefined fails obviously
815     my ( $auth_status, $session ) = C4::Auth::check_cookie_auth($sessionID, {catalogue => 1} );
816     is( $auth_status, 'failed' );
817     is( $session, undef );
818
819     # Simulating the login form submission
820     $cgi->param('userid', $patron->userid);
821     $cgi->param('password', $password);
822
823     # Logged in!
824     ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth($cgi, 0, {catalogue => 1});
825     is( $sessionID, $first_sessionID );
826     is( $userid, $patron->userid );
827
828     ( $auth_status, $session ) = C4::Auth::check_cookie_auth($sessionID, {catalogue => 1});
829     is( $auth_status, 'ok' );
830     is( $session->id, $first_sessionID );
831
832     my $patron_to_delete = $builder->build_object({ class => 'Koha::Patrons' });
833     my $fresh_userid = $patron_to_delete->userid;
834     $patron_to_delete->delete;
835     my $old_userid   = $patron->userid;
836
837     # change the current session user's userid
838     $patron->userid( $fresh_userid )->store;
839     ( $auth_status, $session ) = C4::Auth::check_cookie_auth($sessionID, {catalogue => 1});
840     is( $auth_status, 'expired' );
841     is( $session, undef );
842
843     # restore userid and generate a new session
844     $patron->userid($old_userid)->store;
845     ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth($cgi, 0, {catalogue => 1});
846     is( $sessionID, $first_sessionID );
847     is( $userid, $patron->userid );
848
849     # Logging out!
850     $cgi->param('logout.x', 1);
851     $cgi->delete( 'userid', 'password' );
852     {
853         local *STDOUT;
854         my $stdout;
855         open STDOUT, '>', \$stdout;
856         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth($cgi, 0, {catalogue => 1});
857         close STDOUT;
858     }
859     is( $sessionID, undef );
860     is( $ENV{"HTTP_COOKIE"}, "CGISESSID=$first_sessionID", 'HTTP_COOKIE not unset' );
861     ( $auth_status, $session) = C4::Auth::check_cookie_auth( $first_sessionID, {catalogue => 1} );
862     is( $auth_status, "expired");
863     is( $session, undef );
864
865     {
866         # Trying to access without sessionID
867         $cgi = CGI->new;
868         ( $auth_status, $session) = C4::Auth::check_cookie_auth(undef, {catalogue => 1});
869         is( $auth_status, 'failed' );
870         is( $session, undef );
871
872         # This will fail on permissions
873         undef $ENV{"HTTP_COOKIE"};
874         {
875             local *STDOUT;
876             my $stdout;
877             open STDOUT, '>', \$stdout;
878             ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth($cgi, 0, {catalogue => 1} );
879             close STDOUT;
880         }
881         is( $userid, undef );
882         is( $sessionID, undef );
883     }
884
885     {
886         # First logging in
887         $cgi = CGI->new;
888         $cgi->param('userid', $patron->userid);
889         $cgi->param('password', $password);
890         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth($cgi, 0, {catalogue => 1});
891         is( $userid, $patron->userid );
892         $first_sessionID = $sessionID;
893
894         # Patron does not have the borrowers permission
895         # $ENV{"HTTP_COOKIE"} = "CGISESSID=$sessionID"; # not needed, we use $cgi here
896         {
897             local *STDOUT;
898             my $stdout;
899             open STDOUT, '>', \$stdout;
900             ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth($cgi, 0, {borrowers => 1} );
901             close STDOUT;
902         }
903         is( $userid, undef );
904         is( $sessionID, undef );
905
906         # When calling check_cookie_auth, the session will be deleted
907         ( $auth_status, $session) = C4::Auth::check_cookie_auth( $first_sessionID, { borrowers => 1 } );
908         is( $auth_status, "failed" );
909         is( $session, undef );
910         ( $auth_status, $session) = C4::Auth::check_cookie_auth( $first_sessionID, { borrowers => 1 } );
911         is( $auth_status, 'expired', 'Session no longer exists' );
912
913         # NOTE: It is not what the UI is doing.
914         # From the UI we are allowed to hit an unauthorized page then reuse the session to hit back authorized area.
915         # It is because check_cookie_auth is ALWAYS called from checkauth WITHOUT $flagsrequired
916         # It then return "ok", when the previous called got "failed"
917
918         # Try reusing the deleted session: since it does not exist, we should get a new one now when passing correct permissions
919         $cgi->cookie( -name => 'CGISESSID', value => $first_sessionID );
920         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth($cgi, 0, {catalogue => 1});
921         is( $userid, $patron->userid );
922         isnt( $sessionID, undef, 'Check if we have a sessionID' );
923         isnt( $sessionID, $first_sessionID, 'New value expected' );
924         ( $auth_status, $session) = C4::Auth::check_cookie_auth( $sessionID, {catalogue => 1} );
925         is( $auth_status, "ok" );
926         is( $session->id, $sessionID, 'Same session' );
927         # Two additional tests on userenv
928         is( $C4::Context::context->{activeuser}, $session->id, 'Check if environment has been setup for session' );
929         is( C4::Context->userenv->{id}, $userid, 'Check userid in userenv' );
930     }
931 };
932
933 subtest 'Userenv clearing in check_cookie_auth' => sub {
934     # Note: We did already test userenv for a logged-in user in previous subtest
935     plan tests => 9;
936
937     t::lib::Mocks::mock_preference( 'timeout', 600 );
938     my $cgi = CGI->new;
939
940     # Create a new anonymous session by passing a fake session ID
941     $cgi->cookie( -name => 'CGISESSID', -value => 'fake_sessionID' );
942     my ($userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth($cgi, 1);
943     my ( $auth_status, $session) = C4::Auth::check_cookie_auth( $sessionID );
944     is( $auth_status, 'anon', 'Should be anonymous' );
945     is( $C4::Context::context->{activeuser}, $session->id, 'Check activeuser' );
946     is( defined C4::Context->userenv, 1, 'There should be a userenv' );
947     is(  C4::Context->userenv->{id}, q{}, 'userid should be empty string' );
948
949     # Make the session expire now, check_cookie_auth will delete it
950     $session->param('lasttime', time() - 1200 );
951     $session->flush;
952     ( $auth_status, $session) = C4::Auth::check_cookie_auth( $sessionID );
953     is( $auth_status, 'expired', 'Should be expired' );
954     is( C4::Context->userenv, undef, 'Environment should be cleared too' );
955
956     # Show that we clear the userenv again: set up env and check deleted session
957     C4::Context->_new_userenv( $sessionID );
958     C4::Context->set_userenv; # empty
959     is( defined C4::Context->userenv, 1, 'There should be an empty userenv again' );
960     ( $auth_status, $session) = C4::Auth::check_cookie_auth( $sessionID );
961     is( $auth_status, 'expired', 'Should be expired already' );
962     is( C4::Context->userenv, undef, 'Environment should be cleared again' );
963 };
964
965 subtest 'create_basic_session tests' => sub {
966     plan tests => 13;
967
968     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
969
970     my $session = C4::Auth::create_basic_session({ patron => $patron, interface => 'opac' });
971
972     isnt($session->id, undef, 'A new sessionID was created');
973     is( $session->param('number'), $patron->borrowernumber, 'Session parameter number matches' );
974     is( $session->param('id'), $patron->userid, 'Session parameter id matches' );
975     is( $session->param('cardnumber'), $patron->cardnumber, 'Session parameter cardnumber matches' );
976     is( $session->param('firstname'), $patron->firstname, 'Session parameter firstname matches' );
977     is( $session->param('surname'), $patron->surname, 'Session parameter surname matches' );
978     is( $session->param('branch'), $patron->branchcode, 'Session parameter branch matches' );
979     is( $session->param('branchname'), $patron->library->branchname, 'Session parameter branchname matches' );
980     is( $session->param('flags'), $patron->flags, 'Session parameter flags matches' );
981     is( $session->param('emailaddress'), $patron->email, 'Session parameter emailaddress matches' );
982     is( $session->param('ip'), $session->remote_addr(), 'Session parameter ip matches' );
983     is( $session->param('interface'), 'opac', 'Session parameter interface matches' );
984
985     $session = C4::Auth::create_basic_session({ patron => $patron, interface => 'staff' });
986     is( $session->param('interface'), 'intranet', 'Staff interface gets converted to intranet' );
987 };
988
989 subtest 'check_cookie_auth overwriting interface already set' => sub {
990     plan tests => 2;
991
992     t::lib::Mocks::mock_preference( 'SessionRestrictionByIP', 0 );
993
994     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
995     my $session = C4::Auth::get_session();
996     $session->param( 'number',       $patron->id );
997     $session->param( 'id',           $patron->userid );
998     $session->param( 'ip',           '1.2.3.4' );
999     $session->param( 'lasttime',     time() );
1000     $session->param( 'interface',    'opac' );
1001     $session->flush;
1002
1003     C4::Context->interface('intranet');
1004     C4::Auth::check_cookie_auth( $session->id );
1005     is( C4::Context->interface, 'intranet', 'check_cookie_auth did not overwrite' );
1006     delete $C4::Context::context->{interface}; # clear context interface
1007     C4::Auth::check_cookie_auth( $session->id );
1008     is( C4::Context->interface, 'opac', 'check_cookie_auth used interface from session when context interface was empty' );
1009
1010     t::lib::Mocks::mock_preference( 'SessionRestrictionByIP', 1 );
1011 };
1012
1013 $schema->storage->txn_rollback;
1014
1015 subtest 'get_cataloguing_page_permissions() tests' => sub {
1016
1017     plan tests => 6;
1018
1019     $schema->storage->txn_begin;
1020
1021     my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { flags => 2**2 } } );    # catalogue
1022
1023     ok(
1024         !C4::Auth::haspermission( $patron->userid, get_cataloguing_page_permissions() ),
1025         '"catalogue" is not enough to see the cataloguing page'
1026     );
1027
1028     $builder->build(
1029         {
1030             source => 'UserPermission',
1031             value  => {
1032                 borrowernumber => $patron->id,
1033                 module_bit     => 24,               # stockrotation
1034                 code           => 'manage_rotas',
1035             },
1036         }
1037     );
1038
1039     t::lib::Mocks::mock_preference( 'StockRotation', 1 );
1040     ok(
1041         C4::Auth::haspermission( $patron->userid, get_cataloguing_page_permissions() ),
1042         '"stockrotation => manage_rotas" is enough'
1043     );
1044
1045     t::lib::Mocks::mock_preference( 'StockRotation', 0 );
1046     ok(
1047         !C4::Auth::haspermission( $patron->userid, get_cataloguing_page_permissions() ),
1048         '"stockrotation => manage_rotas" is not enough when `StockRotation` is disabled'
1049     );
1050
1051     $builder->build(
1052         {
1053             source => 'UserPermission',
1054             value  => {
1055                 borrowernumber => $patron->id,
1056                 module_bit     => 13,                     # tools
1057                 code           => 'manage_staged_marc',
1058             },
1059         }
1060     );
1061
1062     ok(
1063         C4::Auth::haspermission( $patron->userid, get_cataloguing_page_permissions() ),
1064         'Having one of the listed `tools` subperm is enough'
1065     );
1066
1067     $schema->resultset('UserPermission')->search( { borrowernumber => $patron->id } )->delete;
1068
1069     ok(
1070         !C4::Auth::haspermission( $patron->userid, get_cataloguing_page_permissions() ),
1071         'Permission removed, no access'
1072     );
1073
1074     $builder->build(
1075         {
1076             source => 'UserPermission',
1077             value  => {
1078                 borrowernumber => $patron->id,
1079                 module_bit     => 9,                    # editcatalogue
1080                 code           => 'delete_all_items',
1081             },
1082         }
1083     );
1084
1085     ok(
1086         C4::Auth::haspermission( $patron->userid, get_cataloguing_page_permissions() ),
1087         'Having any `editcatalogue` subperm is enough'
1088     );
1089
1090     $schema->storage->txn_rollback;
1091 };
1092
1093 subtest 'checkpw() return values tests' => sub {
1094
1095     plan tests => 3;
1096
1097     subtest 'Internal check tests' => sub {
1098
1099         plan tests => 25;
1100
1101         $schema->storage->txn_begin;
1102
1103         my $account_locked;
1104         my $password_expired;
1105
1106         my $mock_patron = Test::MockModule->new('Koha::Patron');
1107         $mock_patron->mock( 'account_locked',   sub { return $account_locked; } );
1108         $mock_patron->mock( 'password_expired', sub { return $password_expired; } );
1109
1110         # Only interested here in regular login
1111         t::lib::Mocks::mock_config( 'useshibboleth', undef );
1112         $C4::Auth::cas  = 0;
1113         $C4::Auth::ldap = 0;
1114
1115         my $patron   = $builder->build_object( { class => 'Koha::Patrons' } );
1116         my $password = 'thePassword123';
1117         $patron->set_password( { password => $password, skip_validation => 1 } );
1118
1119         my $patron_to_delete  = $builder->build_object( { class => 'Koha::Patrons' } );
1120         my $unused_userid     = $patron_to_delete->userid;
1121         my $unused_cardnumber = $patron_to_delete->cardnumber;
1122         $patron_to_delete->delete;
1123
1124         $account_locked = 1;
1125         my @return = checkpw( $patron->userid, $password, undef, );
1126         is_deeply( \@return, [], 'If the account is locked, empty list is returned' );
1127
1128         $account_locked = 0;
1129
1130         my @matchpoints = qw(userid cardnumber);
1131         foreach my $matchpoint (@matchpoints) {
1132
1133             @return = checkpw( $patron->$matchpoint, $password, undef, );
1134
1135             is( $return[0],        1,                   "Password validation successful returns 1 ($matchpoint)" );
1136             is( $return[1],        $patron->cardnumber, '`cardnumber` returned' );
1137             is( $return[2],        $patron->userid,     '`userid` returned' );
1138             is( ref( $return[3] ), 'Koha::Patron',      'Koha::Patron object reference returned' );
1139             is( $return[3]->id,    $patron->id,         'Correct patron returned' );
1140         }
1141
1142         @return = checkpw( $patron->userid, $password . 'hey', undef, );
1143
1144         is( scalar @return,    2, "Two results on invalid password scenario" );
1145         is( $return[0],        0, '0 returned on invalid password' );
1146         is( ref( $return[1] ), 'Koha::Patron' );
1147         is( $return[1]->id,    $patron->id, 'Patron matched correctly' );
1148
1149         $password_expired = 1;
1150         @return           = checkpw( $patron->userid, $password, undef, );
1151
1152         is( scalar @return,    2,  "Two results on expired password scenario" );
1153         is( $return[0],        -2, '-2 returned' );
1154         is( ref( $return[1] ), 'Koha::Patron' );
1155         is( $return[1]->id,    $patron->id, 'Patron matched correctly' );
1156
1157         @return = checkpw( $unused_userid, $password, undef, );
1158
1159         is( scalar @return, 2,     "Two results on non-existing userid scenario" );
1160         is( $return[0],     0,     '0 returned' );
1161         is( $return[1],     undef, 'Undef returned, representing no match' );
1162
1163         @return = checkpw( $unused_cardnumber, $password, undef, );
1164
1165         is( scalar @return, 2,     "Only one result on non-existing cardnumber scenario" );
1166         is( $return[0],     0,     '0 returned' );
1167         is( $return[1],     undef, 'Undef returned, representing no match' );
1168
1169         $schema->storage->txn_rollback;
1170     };
1171
1172     subtest 'CAS check (mocked) tests' => sub {
1173
1174         plan tests => 25;
1175
1176         $schema->storage->txn_begin;
1177
1178         my $account_locked;
1179         my $password_expired;
1180
1181         my $mock_patron = Test::MockModule->new('Koha::Patron');
1182         $mock_patron->mock( 'account_locked',   sub { return $account_locked; } );
1183         $mock_patron->mock( 'password_expired', sub { return $password_expired; } );
1184
1185         # Only interested here in regular login
1186         t::lib::Mocks::mock_config( 'useshibboleth', undef );
1187         $C4::Auth::cas  = 1;
1188         $C4::Auth::ldap = 0;
1189
1190         my $patron   = $builder->build_object( { class => 'Koha::Patrons' } );
1191         my $password = 'thePassword123';
1192         $patron->set_password( { password => $password, skip_validation => 1 } );
1193
1194         my $patron_to_delete  = $builder->build_object( { class => 'Koha::Patrons' } );
1195         my $unused_userid     = $patron_to_delete->userid;
1196         my $unused_cardnumber = $patron_to_delete->cardnumber;
1197         $patron_to_delete->delete;
1198
1199         my $ticket = '123456';
1200         my $query  = CGI->new;
1201         $query->param( -name => 'ticket', -value => $ticket );
1202
1203         my @cas_return = ( 1, $patron->cardnumber, $patron->userid, $ticket, Koha::Patrons->find( $patron->id ) );
1204
1205         my $cas_mock = Test::MockModule->new('C4::Auth');
1206         $cas_mock->mock( 'checkpw_cas', sub { return @cas_return; } );
1207
1208         $account_locked = 1;
1209         my @return = checkpw( $patron->userid, $password, $query, );
1210         is_deeply( \@return, [], 'If the account is locked, empty list is returned' );
1211
1212         $account_locked = 0;
1213
1214         my @matchpoints = qw(userid cardnumber);
1215         foreach my $matchpoint (@matchpoints) {
1216
1217             @return = checkpw( $patron->$matchpoint, $password, $query, );
1218
1219             is( $return[0],        1,                   "Password validation successful returns 1 ($matchpoint)" );
1220             is( $return[1],        $patron->cardnumber, '`cardnumber` returned' );
1221             is( $return[2],        $patron->userid,     '`userid` returned' );
1222             is( ref( $return[3] ), 'Koha::Patron',      'Koha::Patron object reference returned' );
1223             is( $return[3]->id,    $patron->id,         'Correct patron returned' );
1224         }
1225
1226         @return = checkpw( $patron->userid, $password . 'hey', $query, );
1227
1228         is( scalar @return,    2, "Two results on invalid password scenario" );
1229         is( $return[0],        0, '0 returned on invalid password' );
1230         is( ref( $return[1] ), 'Koha::Patron' );
1231         is( $return[1]->id,    $patron->id, 'Patron matched correctly' );
1232
1233         $password_expired = 1;
1234         @return           = checkpw( $patron->userid, $password, $query, );
1235
1236         is( scalar @return,    2,  "Two results on expired password scenario" );
1237         is( $return[0],        -2, '-2 returned' );
1238         is( ref( $return[1] ), 'Koha::Patron' );
1239         is( $return[1]->id,    $patron->id, 'Patron matched correctly' );
1240
1241         @return = checkpw( $unused_userid, $password, $query, );
1242
1243         is( scalar @return, 2,     "Two results on non-existing userid scenario" );
1244         is( $return[0],     0,     '0 returned' );
1245         is( $return[1],     undef, 'Undef returned, representing no match' );
1246
1247         @return = checkpw( $unused_cardnumber, $password, $query, );
1248
1249         is( scalar @return, 2,     "Only one result on non-existing cardnumber scenario" );
1250         is( $return[0],     0,     '0 returned' );
1251         is( $return[1],     undef, 'Undef returned, representing no match' );
1252
1253         $schema->storage->txn_rollback;
1254     };
1255
1256     subtest 'Shibboleth check (mocked) tests' => sub {
1257
1258         plan tests => 6;
1259
1260         $schema->storage->txn_begin;
1261
1262         my $account_locked;
1263         my $password_expired;
1264
1265         my $mock_patron = Test::MockModule->new('Koha::Patron');
1266         $mock_patron->mock( 'account_locked',   sub { return $account_locked; } );
1267         $mock_patron->mock( 'password_expired', sub { return $password_expired; } );
1268
1269         # Only interested here in regular login
1270         t::lib::Mocks::mock_config( 'useshibboleth', 1 );
1271         $C4::Auth::cas  = 0;
1272         $C4::Auth::ldap = 0;
1273
1274         my $patron   = $builder->build_object( { class => 'Koha::Patrons' } );
1275         my $password = 'thePassword123';
1276         $patron->set_password( { password => $password, skip_validation => 1 } );
1277
1278         my $patron_to_delete  = $builder->build_object( { class => 'Koha::Patrons' } );
1279         my $unused_userid     = $patron_to_delete->userid;
1280         my $unused_cardnumber = $patron_to_delete->cardnumber;
1281         $patron_to_delete->delete;
1282
1283         my @shib_return = ( 1, $patron->cardnumber, $patron->userid, Koha::Patrons->find( $patron->id ) );
1284
1285         my $auth_mock = Test::MockModule->new('C4::Auth');
1286         $auth_mock->mock( 'shib_ok',        1 );
1287         $auth_mock->mock( 'get_login_shib', 1 );
1288
1289         my $shib_mock = Test::MockModule->new('C4::Auth_with_shibboleth');
1290         $shib_mock->mock( 'checkpw_shib', sub { return @shib_return; } );
1291
1292         $account_locked = 1;
1293         my @return = checkpw( $patron->userid );
1294         is_deeply( \@return, [], 'If the account is locked, empty list is returned' );
1295
1296         $account_locked = 0;
1297
1298         @return = checkpw();
1299
1300         is( $return[0],        1,                   "Password validation successful returns 1" );
1301         is( $return[1],        $patron->cardnumber, '`cardnumber` returned' );
1302         is( $return[2],        $patron->userid,     '`userid` returned' );
1303         is( ref( $return[3] ), 'Koha::Patron',      'Koha::Patron object reference returned' );
1304         is( $return[3]->id,    $patron->id,         'Correct patron returned' );
1305
1306         $schema->storage->txn_rollback;
1307     };
1308 };
1309
1310 subtest 'AutoLocation' => sub {
1311
1312     plan tests => 6;
1313
1314     $schema->storage->txn_begin;
1315
1316     t::lib::Mocks::mock_preference( 'AutoLocation', 0 );
1317
1318     my $patron   = $builder->build_object( { class => 'Koha::Patrons', value => { flags => 1 } } );
1319     my $password = 'password';
1320     t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
1321     $patron->set_password( { password => $password } );
1322
1323     my $cgi_mock = Test::MockModule->new('CGI');
1324     $cgi_mock->mock( 'request_method', sub { return 'POST' } );
1325     my $cgi  = CGI->new;
1326     my $auth = Test::MockModule->new('C4::Auth');
1327     # Tests will fail if we hit safe_exit
1328     $auth->mock( 'safe_exit', sub { return } );
1329
1330     # Simulating the login form submission
1331     $cgi->param( 'userid',   $patron->userid );
1332     $cgi->param( 'password', $password );
1333
1334     $ENV{REMOTE_ADDR} = '127.0.0.1';
1335     my ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 0, { catalogue => 1 }, 'intranet' );
1336     is( $userid, $patron->userid );
1337
1338     my $template;
1339     t::lib::Mocks::mock_preference( 'AutoLocation', 1 );
1340
1341     # AutoLocation: "Require staff to log in from a computer in the IP address range specified by their library (if any)"
1342     $patron->library->branchip('')->store;    # There is none, allow access from anywhere
1343     ( $userid, $cookie, $sessionID, $flags, $template ) =
1344         C4::Auth::checkauth( $cgi, 0, { catalogue => 1 }, 'intranet' );
1345     is( $userid,   $patron->userid );
1346     is( $template, undef );
1347
1348     $patron->library->branchip('1.2.3.4')->store;
1349     ( $userid, $cookie, $sessionID, $flags, $template ) =
1350         C4::Auth::checkauth( $cgi, 0, { catalogue => 1 }, 'intranet', undef, undef, { do_not_print => 1 } );
1351     #is( $template->{VARS}->{wrongip}, 1 );
1352
1353     $patron->library->branchip('127.0.0.1')->store;
1354     ( $userid, $cookie, $sessionID, $flags, $template ) =
1355         C4::Auth::checkauth( $cgi, 0, { catalogue => 1 }, 'intranet' );
1356     is( $userid,   $patron->userid );
1357     is( $template, undef );
1358
1359     my $other_library = $builder->build_object( { class => 'Koha::Libraries', value => { branchip => '127.0.0.1' } } );
1360     $patron->library->branchip('127.0.0.1')->store;
1361     ( $userid, $cookie, $sessionID, $flags, $template ) =
1362         C4::Auth::checkauth( $cgi, 0, { catalogue => 1 }, 'intranet' );
1363     my $session = C4::Auth::get_session($sessionID);
1364     is( $session->param('branch'), $patron->branchcode );
1365
1366     $schema->storage->txn_rollback;
1367
1368 };
1369
1370 sub set_weak_password {
1371     my ($patron) = @_;
1372     my $password = 'password';
1373     t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
1374     $patron->set_password( { password => $password } );
1375     return $password;
1376 }