Bug 23042: Add tests to catch POST params in return URL
[koha.git] / t / Auth_with_shibboleth.t
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 $| = 1;
21 use Module::Load::Conditional qw/check_install/;
22 use Test::More;
23 use Test::MockModule;
24 use Test::Warn;
25 use File::Temp qw(tempdir);
26
27 use CGI;
28 use C4::Context;
29
30 BEGIN {
31     if ( check_install( module => 'Test::DBIx::Class' ) ) {
32         plan tests => 17;
33     }
34     else {
35         plan skip_all => "Need Test::DBIx::Class";
36     }
37 }
38
39 use Test::DBIx::Class {
40     schema_class => 'Koha::Schema',
41     connect_info => [ 'dbi:SQLite:dbname=:memory:', '', '' ]
42 };
43
44 # Mock Variables
45 my $matchpoint = 'userid';
46 my $autocreate = 0;
47 my $sync = 0;
48 my %mapping    = (
49     'userid'       => { 'is' => 'uid' },
50     'surname'      => { 'is' => 'sn' },
51     'dateexpiry'   => { 'is' => 'exp' },
52     'categorycode' => { 'is' => 'cat' },
53     'address'      => { 'is' => 'add' },
54     'city'         => { 'is' => 'city' },
55 );
56 $ENV{'uid'}  = "test1234";
57 $ENV{'sn'}   = undef;
58 $ENV{'exp'}  = undef;
59 $ENV{'cat'}  = undef;
60 $ENV{'add'}  = undef;
61 $ENV{'city'} = undef;
62
63 # Setup Mocks
64 ## Mock Context
65 my $context = new Test::MockModule('C4::Context');
66
67 ### Mock ->config
68 $context->mock( 'config', \&mockedConfig );
69
70 ### Mock ->preference
71 my $OPACBaseURL = "testopac.com";
72 my $staffClientBaseURL = "teststaff.com";
73 $context->mock( 'preference', \&mockedPref );
74
75 ### Mock ->tz
76 $context->mock( 'timezone', sub { return 'local'; } );
77
78 ### Mock ->interface
79 my $interface = 'opac';
80 $context->mock( 'interface', \&mockedInterface );
81
82 ## Mock Database
83 my $database = new Test::MockModule('Koha::Database');
84
85 ### Mock ->schema
86 $database->mock( 'schema', \&mockedSchema );
87
88 # Tests
89 ##############################################################
90
91 # Can module load
92 use C4::Auth_with_shibboleth;
93 require_ok('C4::Auth_with_shibboleth');
94 $C4::Auth_with_shibboleth::debug = '0';
95
96 # Subroutine tests
97 ## shib_ok
98 subtest "shib_ok tests" => sub {
99     plan tests => 5;
100     my $result;
101
102     # correct config, no debug
103     is( shib_ok(), '1', "good config" );
104
105     # bad config, no debug
106     $matchpoint = undef;
107     warnings_are { $result = shib_ok() }
108     [ { carped => 'shibboleth matchpoint not defined' }, ],
109       "undefined matchpoint = fatal config, warning given";
110     is( $result, '0', "bad config" );
111
112     $matchpoint = 'email';
113     warnings_are { $result = shib_ok() }
114     [ { carped => 'shibboleth matchpoint not mapped' }, ],
115       "unmapped matchpoint = fatal config, warning given";
116     is( $result, '0', "bad config" );
117
118     # add test for undefined shibboleth block
119
120     reset_config();
121 };
122
123 ## logout_shib
124 #my $query = CGI->new();
125 #is(logout_shib($query),"https://".$opac."/Shibboleth.sso/Logout?return="."https://".$opac,"logout_shib");
126
127 ## login_shib_url
128 subtest "login_shib_url tests" => sub {
129     plan tests => 2;
130
131     my $query_string = 'language=en-GB';
132
133     local $ENV{REQUEST_METHOD} = 'GET';
134     local $ENV{QUERY_STRING}   = $query_string;
135     local $ENV{SCRIPT_NAME}    = '/cgi-bin/koha/opac-user.pl';
136     my $query = CGI->new($query_string);
137     is(
138         login_shib_url($query),
139         'https://testopac.com'
140           . '/Shibboleth.sso/Login?target='
141           . 'https://testopac.com/cgi-bin/koha/opac-user.pl' . '%3F'
142           . $query_string,
143         "login shib url"
144     );
145
146     my $post_params = 'user=bob&password=wideopen';
147     local $ENV{REQUEST_METHOD} = 'POST';
148     local $ENV{CONTENT_LENGTH} = length($post_params);
149
150     my $dir = tempdir( CLEANUP => 1 );
151     my $infile = "$dir/in.txt";
152     open my $fh_write, '>', $infile or die "Could not open '$infile' $!";
153     print $fh_write $post_params;
154     close $fh_write;
155
156     open my $fh_read, '<', $infile or die "Could not open '$infile' $!";
157
158     $query = CGI->new($fh_read);
159     is(
160         login_shib_url($query),
161         'https://testopac.com'
162           . '/Shibboleth.sso/Login?target='
163           . 'https://testopac.com/cgi-bin/koha/opac-user.pl',
164         "login shib url"
165     );
166
167     close $fh_read;
168 };
169
170 ## get_login_shib
171 subtest "get_login_shib tests" => sub {
172     plan tests => 4;
173     my $login;
174
175     # good config
176     ## debug off
177     $C4::Auth_with_shibboleth::debug = '0';
178     warnings_are { $login = get_login_shib() }[],
179       "good config with debug off, no warnings received";
180     is( $login, "test1234",
181         "good config with debug off, attribute value returned" );
182
183     ## debug on
184     $C4::Auth_with_shibboleth::debug = '1';
185     warnings_are { $login = get_login_shib() }[
186         "koha borrower field to match: userid",
187         "shibboleth attribute to match: uid",
188         "uid value: test1234"
189     ],
190       "good config with debug enabled, correct warnings received";
191     is( $login, "test1234",
192         "good config with debug enabled, attribute value returned" );
193
194 # bad config - with shib_ok implemented, we should never reach this sub with a bad config
195 };
196
197 ## checkpw_shib
198 subtest "checkpw_shib tests" => sub {
199     plan tests => 21;
200
201     my $shib_login;
202     my ( $retval, $retcard, $retuserid );
203
204     # Setup Mock Database Data
205     fixtures_ok [
206         'Borrower' => [
207             [qw/cardnumber userid surname address city/],
208             [qw/testcardnumber test1234 renvoize myaddress johnston/],
209         ],
210         'Category' => [ [qw/categorycode default_privacy/], [qw/S never/], ]
211       ],
212       'Installed some custom fixtures via the Populate fixture class';
213
214     # debug off
215     $C4::Auth_with_shibboleth::debug = '0';
216
217     # good user
218     $shib_login = "test1234";
219     warnings_are {
220         ( $retval, $retcard, $retuserid ) = checkpw_shib($shib_login);
221     }
222     [], "good user with no debug";
223     is( $retval,    "1",              "user authenticated" );
224     is( $retcard,   "testcardnumber", "expected cardnumber returned" );
225     is( $retuserid, "test1234",       "expected userid returned" );
226
227     # bad user
228     $shib_login = 'martin';
229     warnings_are {
230         ( $retval, $retcard, $retuserid ) = checkpw_shib($shib_login);
231     }
232     [], "bad user with no debug";
233     is( $retval, "0", "user not authenticated" );
234
235     # autocreate user
236     $autocreate  = 1;
237     $shib_login  = 'test4321';
238     $ENV{'uid'}  = 'test4321';
239     $ENV{'sn'}   = "pika";
240     $ENV{'exp'}  = "2017";
241     $ENV{'cat'}  = "S";
242     $ENV{'add'}  = 'Address';
243     $ENV{'city'} = 'City';
244     warnings_are {
245         ( $retval, $retcard, $retuserid ) = checkpw_shib($shib_login);
246     }
247     [], "new user added with no debug";
248     is( $retval,    "1",        "user authenticated" );
249     is( $retuserid, "test4321", "expected userid returned" );
250     ok my $new_user = ResultSet('Borrower')
251       ->search( { 'userid' => 'test4321' }, { rows => 1 } ), "new user found";
252     is_fields [qw/surname dateexpiry address city/], $new_user->next,
253       [qw/pika 2017 Address City/],
254       'Found $new_users surname';
255     $autocreate = 0;
256
257     # sync user
258     $sync = 1;
259     $ENV{'city'} = 'AnotherCity';
260     warnings_are {
261         ( $retval, $retcard, $retuserid ) = checkpw_shib($shib_login);
262     }
263     [], "good user with sync";
264
265     ok my $sync_user = ResultSet('Borrower')
266       ->search( { 'userid' => 'test4321' }, { rows => 1 } ), "sync user found";
267
268     is_fields [qw/surname dateexpiry address city/], $sync_user->next,
269       [qw/pika 2017 Address AnotherCity/],
270       'Found $sync_user synced city';
271     $sync = 0;
272
273     # debug on
274     $C4::Auth_with_shibboleth::debug = '1';
275
276     # good user
277     $shib_login = "test1234";
278     warnings_exist {
279         ( $retval, $retcard, $retuserid ) = checkpw_shib($shib_login);
280     }
281     [
282         qr/checkpw_shib/,
283         qr/koha borrower field to match: userid/,
284         qr/shibboleth attribute to match: uid/,
285         qr/User Shibboleth-authenticated as:/
286     ],
287       "good user with debug enabled";
288     is( $retval,    "1",              "user authenticated" );
289     is( $retcard,   "testcardnumber", "expected cardnumber returned" );
290     is( $retuserid, "test1234",       "expected userid returned" );
291
292     # bad user
293     $shib_login = "martin";
294     warnings_exist {
295         ( $retval, $retcard, $retuserid ) = checkpw_shib($shib_login);
296     }
297     [
298         qr/checkpw_shib/,
299         qr/koha borrower field to match: userid/,
300         qr/shibboleth attribute to match: uid/,
301         qr/User Shibboleth-authenticated as:/,
302         qr/not a valid Koha user/
303     ],
304       "bad user with debug enabled";
305     is( $retval, "0", "user not authenticated" );
306
307 };
308
309 ## _get_uri - opac
310 $OPACBaseURL = "testopac.com";
311 is( C4::Auth_with_shibboleth::_get_uri(),
312     "https://testopac.com", "https opac uri returned" );
313
314 $OPACBaseURL = "http://testopac.com";
315 my $result;
316 warnings_are { $result = C4::Auth_with_shibboleth::_get_uri() }[
317     "shibboleth interface: $interface",
318 "Shibboleth requires OPACBaseURL/staffClientBaseURL to use the https protocol!"
319 ],
320   "improper protocol - received expected warning";
321 is( $result, "https://testopac.com", "https opac uri returned" );
322
323 $OPACBaseURL = "https://testopac.com";
324 is( C4::Auth_with_shibboleth::_get_uri(),
325     "https://testopac.com", "https opac uri returned" );
326
327 $OPACBaseURL = undef;
328 warnings_are { $result = C4::Auth_with_shibboleth::_get_uri() }
329 [ "shibboleth interface: $interface", "OPACBaseURL not set!" ],
330   "undefined OPACBaseURL - received expected warning";
331 is( $result, "https://", "https $interface uri returned" );
332
333 ## _get_uri - intranet
334 $interface = 'intranet';
335 $staffClientBaseURL = "teststaff.com";
336 is( C4::Auth_with_shibboleth::_get_uri(),
337     "https://teststaff.com", "https $interface uri returned" );
338
339 $staffClientBaseURL = "http://teststaff.com";
340 warnings_are { $result = C4::Auth_with_shibboleth::_get_uri() }[
341     "shibboleth interface: $interface",
342 "Shibboleth requires OPACBaseURL/staffClientBaseURL to use the https protocol!"
343 ],
344   "improper protocol - received expected warning";
345 is( $result, "https://teststaff.com", "https $interface uri returned" );
346
347 $staffClientBaseURL = "https://teststaff.com";
348 is( C4::Auth_with_shibboleth::_get_uri(),
349     "https://teststaff.com", "https $interface uri returned" );
350
351 $staffClientBaseURL = undef;
352 warnings_are { $result = C4::Auth_with_shibboleth::_get_uri() }
353 [ "shibboleth interface: $interface", "staffClientBaseURL not set!" ],
354   "undefined staffClientBaseURL - received expected warning";
355 is( $result, "https://", "https $interface uri returned" );
356
357 ## _get_shib_config
358 # Internal helper function, covered in tests above
359
360 sub mockedConfig {
361     my $param = shift;
362
363     my %shibboleth = (
364         'autocreate' => $autocreate,
365         'sync'       => $sync,
366         'matchpoint' => $matchpoint,
367         'mapping'    => \%mapping
368     );
369
370     return \%shibboleth;
371 }
372
373 sub mockedPref {
374     my $param = $_[1];
375     my $return;
376
377     if ( $param eq 'OPACBaseURL' ) {
378         $return = $OPACBaseURL;
379     }
380
381     if ( $param eq 'staffClientBaseURL' ) {
382         $return = $staffClientBaseURL;
383     }
384
385     return $return;
386 }
387
388 sub mockedInterface {
389     return $interface;
390 }
391
392 sub mockedSchema {
393     return Schema();
394 }
395
396 ## Convenience method to reset config
397 sub reset_config {
398     $matchpoint = 'userid';
399     $autocreate = 0;
400     $sync = 0;
401     %mapping    = (
402         'userid'       => { 'is' => 'uid' },
403         'surname'      => { 'is' => 'sn' },
404         'dateexpiry'   => { 'is' => 'exp' },
405         'categorycode' => { 'is' => 'cat' },
406         'address'      => { 'is' => 'add' },
407         'city'         => { 'is' => 'city' },
408     );
409     $ENV{'uid'}  = "test1234";
410     $ENV{'sn'}   = undef;
411     $ENV{'exp'}  = undef;
412     $ENV{'cat'}  = undef;
413     $ENV{'add'}  = undef;
414     $ENV{'city'} = undef;
415
416     return 1;
417 }
418