synch'ing 2.2 and head
[koha.git] / C4 / Auth_with_ldap.pm
1 # -*- tab-width: 8 -*-
2 # NOTE: This file uses 8-character tabs; do not change the tab size!
3
4 package C4::Auth;
5
6 # Copyright 2000-2002 Katipo Communications
7 #
8 # This file is part of Koha.
9 #
10 # Koha is free software; you can redistribute it and/or modify it under the
11 # terms of the GNU General Public License as published by the Free Software
12 # Foundation; either version 2 of the License, or (at your option) any later
13 # version.
14 #
15 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
16 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
17 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License along with
20 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
21 # Suite 330, Boston, MA  02111-1307 USA
22
23 use strict;
24 use Digest::MD5 qw(md5_base64);
25
26 require Exporter;
27 use C4::Context;
28 use C4::Output;              # to get the template
29 use C4::Interface::CGI::Output;
30 use C4::Circulation::Circ2;  # getpatroninformation
31 use C4::Members;
32 use Net::LDAP;
33 use Net::LDAP qw(:all);
34
35 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
36
37 # set the version for version checking
38 $VERSION = 0.01;
39
40 =head1 NAME
41
42 C4::Auth - Authenticates Koha users
43
44 =head1 SYNOPSIS
45
46   use CGI;
47   use C4::Auth;
48
49   my $query = new CGI;
50
51   my ($template, $borrowernumber, $cookie) 
52     = get_template_and_user({template_name   => "opac-main.tmpl",
53                              query           => $query,
54                              type            => "opac",
55                              authnotrequired => 1,
56                              flagsrequired   => {borrow => 1},
57                           });
58
59   print $query->header(
60     -type => guesstype($template->output),
61     -cookie => $cookie
62   ), $template->output;
63
64
65 =head1 DESCRIPTION
66
67     The main function of this module is to provide
68     authentification. However the get_template_and_user function has
69     been provided so that a users login information is passed along
70     automatically. This gets loaded into the template.
71
72 =head1 LDAP specific
73
74     This module is specific to LDAP authentification. It requires Net::LDAP package and a working LDAP server.
75         To use it :
76            * move initial Auth.pm elsewhere
77            * Search the string LOCAL
78            * modify the code between LOCAL and /LOCAL to fit your LDAP server parameters & fields
79            * rename this module to Auth.pm
80         That should be enough.
81
82 =head1 FUNCTIONS
83
84 =over 2
85
86 =cut
87
88
89
90 @ISA = qw(Exporter);
91 @EXPORT = qw(
92              &checkauth
93              &get_template_and_user
94 );
95
96 =item get_template_and_user
97
98   my ($template, $borrowernumber, $cookie)
99     = get_template_and_user({template_name   => "opac-main.tmpl",
100                              query           => $query,
101                              type            => "opac",
102                              authnotrequired => 1,
103                              flagsrequired   => {borrow => 1},
104                           });
105
106     This call passes the C<query>, C<flagsrequired> and C<authnotrequired>
107     to C<&checkauth> (in this module) to perform authentification.
108     See C<&checkauth> for an explanation of these parameters.
109
110     The C<template_name> is then used to find the correct template for
111     the page. The authenticated users details are loaded onto the
112     template in the HTML::Template LOOP variable C<USER_INFO>. Also the
113     C<sessionID> is passed to the template. This can be used in templates
114     if cookies are disabled. It needs to be put as and input to every
115     authenticated page.
116
117     More information on the C<gettemplate> sub can be found in the
118     Output.pm module.
119
120 =cut
121
122
123 sub get_template_and_user {
124         my $in = shift;
125         my $template = gettemplate($in->{'template_name'}, $in->{'type'},$in->{'query'});
126         my ($user, $cookie, $sessionID, $flags)
127                 = checkauth($in->{'query'}, $in->{'authnotrequired'}, $in->{'flagsrequired'}, $in->{'type'});
128
129         my $borrowernumber;
130         if ($user) {
131                 $template->param(loggedinusername => $user);
132                 $template->param(sessionID => $sessionID);
133
134                 $borrowernumber = getborrowernumber($user);
135                 my ($borr, $alternativeflags) = getpatroninformation(undef, $borrowernumber);
136                 my @bordat;
137                 $bordat[0] = $borr;
138                 $template->param(USER_INFO => \@bordat,
139                 );
140                 # We are going to use the $flags returned by checkauth
141                 # to create the template's parameters that will indicate
142                 # which menus the user can access.
143                 if ($flags->{superlibrarian} == 1)
144                 {
145                         $template->param(CAN_user_circulate => 1);
146                         $template->param(CAN_user_catalogue => 1);
147                         $template->param(CAN_user_parameters => 1);
148                         $template->param(CAN_user_borrowers => 1);
149                         $template->param(CAN_user_permission => 1);
150                         $template->param(CAN_user_reserveforothers => 1);
151                         $template->param(CAN_user_borrow => 1);
152                         $template->param(CAN_user_reserveforself => 1);
153                         $template->param(CAN_user_editcatalogue => 1);
154                         $template->param(CAN_user_updatecharge => 1);
155                         $template->param(CAN_user_acquisition => 1);
156                         $template->param(CAN_user_management => 1);
157                         $template->param(CAN_user_tools => 1); }
158                 
159                 if ($flags->{circulate} == 1) {
160                         $template->param(CAN_user_circulate => 1); }
161
162                 if ($flags->{catalogue} == 1) {
163                         $template->param(CAN_user_catalogue => 1); }
164                 
165
166                 if ($flags->{parameters} == 1) {
167                         $template->param(CAN_user_parameters => 1);     
168                         $template->param(CAN_user_management => 1);
169                         $template->param(CAN_user_tools => 1); }
170                 
171
172                 if ($flags->{borrowers} == 1) {
173                         $template->param(CAN_user_borrowers => 1); }
174                 
175
176                 if ($flags->{permissions} == 1) {
177                         $template->param(CAN_user_permission => 1); }
178                 
179                 if ($flags->{reserveforothers} == 1) {
180                         $template->param(CAN_user_reserveforothers => 1); }
181                 
182
183                 if ($flags->{borrow} == 1) {
184                         $template->param(CAN_user_borrow => 1); }
185                 
186
187                 if ($flags->{reserveforself} == 1) {
188                         $template->param(CAN_user_reserveforself => 1); }
189                 
190
191                 if ($flags->{editcatalogue} == 1) {
192                         $template->param(CAN_user_editcatalogue => 1); }
193                 
194
195                 if ($flags->{updatecharges} == 1) {
196                         $template->param(CAN_user_updatecharge => 1); }
197                 
198                 if ($flags->{acquisition} == 1) {
199                         $template->param(CAN_user_acquisition => 1); }
200                 
201                 if ($flags->{management} == 1) {
202                         $template->param(CAN_user_management => 1);
203                         $template->param(CAN_user_tools => 1); }
204                 
205                 if ($flags->{tools} == 1) {
206                         $template->param(CAN_user_tools => 1); }
207                 
208         }
209         $template->param(
210                              LibraryName => C4::Context->preference("LibraryName"),
211                 );
212         return ($template, $borrowernumber, $cookie);
213 }
214
215
216 =item checkauth
217
218   ($userid, $cookie, $sessionID) = &checkauth($query, $noauth, $flagsrequired, $type);
219
220 Verifies that the user is authorized to run this script.  If
221 the user is authorized, a (userid, cookie, session-id, flags)
222 quadruple is returned.  If the user is not authorized but does
223 not have the required privilege (see $flagsrequired below), it
224 displays an error page and exits.  Otherwise, it displays the
225 login page and exits.
226
227 Note that C<&checkauth> will return if and only if the user
228 is authorized, so it should be called early on, before any
229 unfinished operations (e.g., if you've opened a file, then
230 C<&checkauth> won't close it for you).
231
232 C<$query> is the CGI object for the script calling C<&checkauth>.
233
234 The C<$noauth> argument is optional. If it is set, then no
235 authorization is required for the script.
236
237 C<&checkauth> fetches user and session information from C<$query> and
238 ensures that the user is authorized to run scripts that require
239 authorization.
240
241 The C<$flagsrequired> argument specifies the required privileges
242 the user must have if the username and password are correct.
243 It should be specified as a reference-to-hash; keys in the hash
244 should be the "flags" for the user, as specified in the Members
245 intranet module. Any key specified must correspond to a "flag"
246 in the userflags table. E.g., { circulate => 1 } would specify
247 that the user must have the "circulate" privilege in order to
248 proceed. To make sure that access control is correct, the
249 C<$flagsrequired> parameter must be specified correctly.
250
251 The C<$type> argument specifies whether the template should be
252 retrieved from the opac or intranet directory tree.  "opac" is
253 assumed if it is not specified; however, if C<$type> is specified,
254 "intranet" is assumed if it is not "opac".
255
256 If C<$query> does not have a valid session ID associated with it
257 (i.e., the user has not logged in) or if the session has expired,
258 C<&checkauth> presents the user with a login page (from the point of
259 view of the original script, C<&checkauth> does not return). Once the
260 user has authenticated, C<&checkauth> restarts the original script
261 (this time, C<&checkauth> returns).
262
263 The login page is provided using a HTML::Template, which is set in the
264 systempreferences table or at the top of this file. The variable C<$type>
265 selects which template to use, either the opac or the intranet 
266 authentification template.
267
268 C<&checkauth> returns a user ID, a cookie, and a session ID. The
269 cookie should be sent back to the browser; it verifies that the user
270 has authenticated.
271
272 =cut
273
274
275
276 sub checkauth {
277         my $query=shift;
278         # $authnotrequired will be set for scripts which will run without authentication
279         my $authnotrequired = shift;
280         my $flagsrequired = shift;
281         my $type = shift;
282         $type = 'opac' unless $type;
283
284         my $dbh = C4::Context->dbh;
285         my $timeout = C4::Context->preference('timeout');
286         $timeout = 600 unless $timeout;
287
288         my $template_name;
289         if ($type eq 'opac') {
290                 $template_name = "opac-auth.tmpl";
291         } else {
292                 $template_name = "auth.tmpl";
293         }
294
295         # state variables
296         my $loggedin = 0;
297         my %info;
298         my ($userid, $cookie, $sessionID, $flags);
299         my $logout = $query->param('logout.x');
300         if ($userid = $ENV{'REMOTE_USER'}) {
301                 # Using Basic Authentication, no cookies required
302                 $cookie=$query->cookie(-name => 'sessionID',
303                                 -value => '',
304                                 -expires => '');
305                 $loggedin = 1;
306         } elsif ($sessionID=$query->cookie('sessionID')) {
307                 my ($ip , $lasttime);
308                 ($userid, $ip, $lasttime) = $dbh->selectrow_array(
309                                 "SELECT userid,ip,lasttime FROM sessions WHERE sessionid=?",
310                                                                 undef, $sessionID);
311                 if ($logout) {
312                 # voluntary logout the user
313                 $dbh->do("DELETE FROM sessions WHERE sessionID=?", undef, $sessionID);
314                 $sessionID = undef;
315                 $userid = undef;
316                 open L, ">>/tmp/sessionlog";
317                 my $time=localtime(time());
318                 printf L "%20s from %16s logged out at %30s (manually).\n", $userid, $ip, $time;
319                 close L;
320                 }
321                 if ($userid) {
322                 if ($lasttime<time()-$timeout) {
323                         # timed logout
324                         $info{'timed_out'} = 1;
325                         $dbh->do("DELETE FROM sessions WHERE sessionID=?", undef, $sessionID);
326                         $userid = undef;
327                         $sessionID = undef;
328                         open L, ">>/tmp/sessionlog";
329                         my $time=localtime(time());
330                         printf L "%20s from %16s logged out at %30s (inactivity).\n", $userid, $ip, $time;
331                         close L;
332                 } elsif ($ip ne $ENV{'REMOTE_ADDR'}) {
333                         # Different ip than originally logged in from
334                         $info{'oldip'} = $ip;
335                         $info{'newip'} = $ENV{'REMOTE_ADDR'};
336                         $info{'different_ip'} = 1;
337                         $dbh->do("DELETE FROM sessions WHERE sessionID=?", undef, $sessionID);
338                         $sessionID = undef;
339                         $userid = undef;
340                         open L, ">>/tmp/sessionlog";
341                         my $time=localtime(time());
342                         printf L "%20s from logged out at %30s (ip changed from %16s to %16s).\n", $userid, $time, $ip, $info{'newip'};
343                         close L;
344                 } else {
345                         $cookie=$query->cookie(-name => 'sessionID',
346                                         -value => $sessionID,
347                                         -expires => '');
348                         $dbh->do("UPDATE sessions SET lasttime=? WHERE sessionID=?",
349                                 undef, (time(), $sessionID));
350                         $flags = haspermission($dbh, $userid, $flagsrequired);
351                         if ($flags) {
352                         $loggedin = 1;
353                         } else {
354                         $info{'nopermission'} = 1;
355                         }
356                 }
357                 }
358         }
359         unless ($userid) {
360                 $sessionID=int(rand()*100000).'-'.time();
361                 $userid=$query->param('userid');
362                 my $password=$query->param('password');
363                 my ($return, $cardnumber) = checkpw($dbh,$userid,$password);
364                 if ($return) {
365                 $dbh->do("DELETE FROM sessions WHERE sessionID=? AND userid=?",
366                         undef, ($sessionID, $userid));
367                 $dbh->do("INSERT INTO sessions (sessionID, userid, ip,lasttime) VALUES (?, ?, ?, ?)",
368                         undef, ($sessionID, $userid, $ENV{'REMOTE_ADDR'}, time()));
369                 open L, ">>/tmp/sessionlog";
370                 my $time=localtime(time());
371                 printf L "%20s from %16s logged in  at %30s.\n", $userid, $ENV{'REMOTE_ADDR'}, $time;
372                 close L;
373                 $cookie=$query->cookie(-name => 'sessionID',
374                                         -value => $sessionID,
375                                         -expires => '');
376                 if ($flags = haspermission($dbh, $userid, $flagsrequired)) {
377                         $loggedin = 1;
378                 } else {
379                         $info{'nopermission'} = 1;
380                 }
381                 } else {
382                 if ($userid) {
383                         $info{'invalid_username_or_password'} = 1;
384                 }
385                 }
386         }
387         my $insecure = C4::Context->boolean_preference('insecure');
388         # finished authentification, now respond
389         if ($loggedin || $authnotrequired || (defined($insecure) && $insecure)) {
390                 # successful login
391                 unless ($cookie) {
392                 $cookie=$query->cookie(-name => 'sessionID',
393                                         -value => '',
394                                         -expires => '');
395                 }
396                 return ($userid, $cookie, $sessionID, $flags);
397         }
398         # else we have a problem...
399         # get the inputs from the incoming query
400         my @inputs =();
401         foreach my $name (param $query) {
402                 (next) if ($name eq 'userid' || $name eq 'password');
403                 my $value = $query->param($name);
404                 push @inputs, {name => $name , value => $value};
405         }
406
407         my $template = gettemplate($template_name, $type,$query);
408         $template->param(INPUTS => \@inputs);
409         $template->param(loginprompt => 1) unless $info{'nopermission'};
410
411         my $self_url = $query->url(-absolute => 1);
412         $template->param(url => $self_url);
413         $template->param(\%info);
414         $cookie=$query->cookie(-name => 'sessionID',
415                                         -value => $sessionID,
416                                         -expires => '');
417         print $query->header(
418                 -type => guesstype($template->output),
419                 -cookie => $cookie
420                 ), $template->output;
421         exit;
422 }
423
424
425
426 # this checkpw is a LDAP based one
427 # it connects to LDAP (anonymous)
428 # it retrieve $userid a-login
429 # then compare $password with a-weak
430 # then get the LDAP entry
431 # and calls the memberadd if necessary
432
433 sub checkpw {
434         my ($dbh, $userid, $password) = @_;
435         if ($userid eq C4::Context->config('user') && $password eq C4::Context->config('pass')) {
436                 # Koha superuser account
437                 return 2;
438         }
439         ##################################################
440         ### LOCAL
441         ### Change the code below to match your own LDAP server.
442         ##################################################
443         # LDAP connexion parameters
444         my $ldapserver = 'your.ldap.server.com';
445         # Infos to do an anonymous bind
446         my $ldapinfos = 'a-section=people,dc=emn,dc=fr ';
447         my $name  = "a-section=people,dc=emn,dc=fr";
448         my $db = Net::LDAP->new( $ldapserver );
449         
450         # do an anonymous bind
451         my $res =$db->bind();
452         # check connexion
453         if($res->code) {
454                 # auth refused
455                 warn "LDAP Auth impossible : server not responding";
456                 return 0;
457         # search user
458         } else {
459                 my $userdnsearch = $db->search(base => $name,
460                                 filter =>"(a-login=$userid)",
461                                 );
462                 if($userdnsearch->code || ! ( $userdnsearch-> count eq 1 ) ) {
463                         warn "LDAP Auth impossible : user unknown in LDAP";
464                         return 0;
465                 };
466                 # compare a-weak with $password.
467                 # The a-weak LDAP field contains the password
468                 my $userldapentry=$userdnsearch -> shift_entry;
469                 my $cmpmesg = $db -> compare ( $userldapentry, attr => 'a-weak', value => $password );
470                 if( $cmpmesg -> code != 6 ) {
471                         warn "LDAP Auth impossible : wrong password";
472                         return 0;
473                 };
474                 # build LDAP hash
475                 my %memberhash;
476                 my $x =$userldapentry->{asn}{attributes};
477                 my $key;
478                 foreach my $k ( @$x) {
479                         foreach my $k2 (keys %$k) {
480                                 if ($k2 eq 'type') {
481                                         $key = $$k{$k2};
482                                 } else {
483                                         my $a = @$k{$k2};
484                                         foreach my $k3 (@$a) {
485                                                 $memberhash{$key} .= $k3." ";
486                                         }
487                                 }
488                         }
489                 }
490                 #
491                 # BUILD %borrower to CREATE or MODIFY BORROWER
492                 # change $memberhash{'xxx'} to fit your ldap structure.
493                 # check twice that mandatory fields are correctly filled
494                 #
495                 my %borrower;
496                 $borrower{cardnumber} = $userid;
497                 $borrower{firstname} = $memberhash{givenName}; # MANDATORY FIELD
498                 $borrower{surname} = $memberhash{sn}; # MANDATORY FIELD
499                 $borrower{initials} = substr($borrower{firstname},0,1).substr($borrower{surname},0,1)."  "; # MANDATORY FIELD
500                 $borrower{streetaddress} = $memberhash{l}." "; # MANDATORY FIELD
501                 $borrower{city} = " "; # MANDATORY FIELD
502                 $borrower{phone} = " "; # MANDATORY FIELD
503                 $borrower{branchcode} = $memberhash{branch}; # MANDATORY FIELD
504                 $borrower{emailaddress} = $memberhash{mail};
505                 $borrower{categorycode} = $memberhash{employeeType};
506         ##################################################
507         ### /LOCAL
508         ### No change needed after this line (unless there's a bug ;-) )
509         ##################################################
510                 # check if borrower exists
511                 my $sth=$dbh->prepare("select password from borrowers where cardnumber=?");
512                 $sth->execute($userid);
513                 if ($sth->rows) {
514                         # it exists, MODIFY
515                         my $sth2 = $dbh->prepare("update borrowers set firstname=?,surname=?,initials=?,streetaddress=?,city=?,phone=?, categorycode=?,branchcode=?,emailaddress=?,sort1=? where cardnumber=?");
516                         $sth2->execute($borrower{firstname},$borrower{surname},$borrower{initials},
517                                                         $borrower{streetaddress},$borrower{city},$borrower{phone},
518                                                         $borrower{categorycode},$borrower{branchcode},$borrower{emailaddress},
519                                                         $borrower{sort1} ,$userid);
520                 } else {
521                         # it does not exists, ADD borrower
522                         my $borrowerid = newmember(%borrower);
523                 }
524                 #
525                 # CREATE or MODIFY PASSWORD/LOGIN
526                 #
527                 # search borrowerid
528                 $sth = $dbh->prepare("select borrowernumber from borrowers where cardnumber=?");
529                 $sth->execute($userid);
530                 my ($borrowerid)=$sth->fetchrow;
531                 my $digest=md5_base64($password);
532                 changepassword($userid,$borrowerid,$digest);
533         }
534
535 # INTERNAL AUTH. The borrower entry has been created by LDAP if needed, The auth is probably useless
536 # but it's the standard Auth.pm here.
537         my $sth=$dbh->prepare("select password,cardnumber from borrowers where userid=?");
538         $sth->execute($userid);
539         if ($sth->rows) {
540                 my ($md5password,$cardnumber) = $sth->fetchrow;
541                 if (md5_base64($password) eq $md5password) {
542                         return 1,$cardnumber;
543                 }
544         }
545         my $sth=$dbh->prepare("select password from borrowers where cardnumber=?");
546         $sth->execute($userid);
547         if ($sth->rows) {
548                 my ($md5password) = $sth->fetchrow;
549                 if (md5_base64($password) eq $md5password) {
550                         return 1,$userid;
551                 }
552         }
553         return 0;
554 }
555
556 sub getuserflags {
557     my $cardnumber=shift;
558     my $dbh=shift;
559     my $userflags;
560     my $sth=$dbh->prepare("SELECT flags FROM borrowers WHERE cardnumber=?");
561     $sth->execute($cardnumber);
562     my ($flags) = $sth->fetchrow;
563     $sth=$dbh->prepare("SELECT bit, flag, defaulton FROM userflags");
564     $sth->execute;
565     while (my ($bit, $flag, $defaulton) = $sth->fetchrow) {
566         if (($flags & (2**$bit)) || $defaulton) {
567             $userflags->{$flag}=1;
568         }
569     }
570     return $userflags;
571 }
572
573 sub haspermission {
574     my ($dbh, $userid, $flagsrequired) = @_;
575     my $sth=$dbh->prepare("SELECT cardnumber FROM borrowers WHERE userid=?");
576     $sth->execute($userid);
577     my ($cardnumber) = $sth->fetchrow;
578     ($cardnumber) || ($cardnumber=$userid);
579     my $flags=getuserflags($cardnumber,$dbh);
580     my $configfile;
581     if ($userid eq C4::Context->config('user')) {
582         # Super User Account from /etc/koha.conf
583         $flags->{'superlibrarian'}=1;
584      }
585      if ($userid eq 'demo' && C4::Context->config('demo')) {
586         # Demo user that can do "anything" (demo=1 in /etc/koha.conf)
587         $flags->{'superlibrarian'}=1;
588     }
589     return $flags if $flags->{superlibrarian};
590     foreach (keys %$flagsrequired) {
591         return $flags if $flags->{$_};
592     }
593     return 0;
594 }
595
596 sub getborrowernumber {
597     my ($userid) = @_;
598     my $dbh = C4::Context->dbh;
599     for my $field ('userid', 'cardnumber') {
600       my $sth=$dbh->prepare
601           ("select borrowernumber from borrowers where $field=?");
602       $sth->execute($userid);
603       if ($sth->rows) {
604         my ($bnumber) = $sth->fetchrow;
605         return $bnumber;
606       }
607     }
608     return 0;
609 }
610
611 END { }       # module clean-up code here (global destructor)
612 1;
613 __END__
614
615 =back
616
617 =head1 SEE ALSO
618
619 CGI(3)
620
621 C4::Output(3)
622
623 Digest::MD5(3)
624
625 =cut