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                 
141                 # We are going to use the $flags returned by checkauth
142                 # to create the template's parameters that will indicate
143                 # which menus the user can access.
144                 if ($flags->{superlibrarian} == 1)
145                 {
146                         $template->param(CAN_user_circulate => 1);
147                         $template->param(CAN_user_catalogue => 1);
148                         $template->param(CAN_user_parameters => 1);
149                         $template->param(CAN_user_borrowers => 1);
150                         $template->param(CAN_user_permission => 1);
151                         $template->param(CAN_user_reserveforothers => 1);
152                         $template->param(CAN_user_borrow => 1);
153                         $template->param(CAN_user_reserveforself => 1);
154                         $template->param(CAN_user_editcatalogue => 1);
155                         $template->param(CAN_user_updatecharge => 1);
156                         $template->param(CAN_user_acquisition => 1);
157                         $template->param(CAN_user_management => 1);
158                         $template->param(CAN_user_tools => 1); }
159                 
160                 if ($flags->{circulate} == 1) {
161                         $template->param(CAN_user_circulate => 1); }
162
163                 if ($flags->{catalogue} == 1) {
164                         $template->param(CAN_user_catalogue => 1); }
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                 if ($flags->{borrowers} == 1) {
172                         $template->param(CAN_user_borrowers => 1); }
173                 
174                 if ($flags->{permissions} == 1) {
175                         $template->param(CAN_user_permission => 1); }
176                 
177                 if ($flags->{reserveforothers} == 1) {
178                         $template->param(CAN_user_reserveforothers => 1); }
179                 
180                 if ($flags->{borrow} == 1) {
181                         $template->param(CAN_user_borrow => 1); }
182                 
183                 if ($flags->{reserveforself} == 1) {
184                         $template->param(CAN_user_reserveforself => 1); }
185                 
186                 if ($flags->{editcatalogue} == 1) {
187                         $template->param(CAN_user_editcatalogue => 1); }
188                 
189                 if ($flags->{updatecharges} == 1) {
190                         $template->param(CAN_user_updatecharge => 1); }
191                 
192                 if ($flags->{acquisition} == 1) {
193                         $template->param(CAN_user_acquisition => 1); }
194                 
195                 if ($flags->{management} == 1) {
196                         $template->param(CAN_user_management => 1);
197                         $template->param(CAN_user_tools => 1); }
198                 
199                 if ($flags->{tools} == 1) {
200                         $template->param(CAN_user_tools => 1); }
201                 
202         }
203         $template->param(
204                              LibraryName => C4::Context->preference("LibraryName"),
205                 );
206         return ($template, $borrowernumber, $cookie);
207 }
208
209
210 =item checkauth
211
212   ($userid, $cookie, $sessionID) = &checkauth($query, $noauth, $flagsrequired, $type);
213
214 Verifies that the user is authorized to run this script.  If
215 the user is authorized, a (userid, cookie, session-id, flags)
216 quadruple is returned.  If the user is not authorized but does
217 not have the required privilege (see $flagsrequired below), it
218 displays an error page and exits.  Otherwise, it displays the
219 login page and exits.
220
221 Note that C<&checkauth> will return if and only if the user
222 is authorized, so it should be called early on, before any
223 unfinished operations (e.g., if you've opened a file, then
224 C<&checkauth> won't close it for you).
225
226 C<$query> is the CGI object for the script calling C<&checkauth>.
227
228 The C<$noauth> argument is optional. If it is set, then no
229 authorization is required for the script.
230
231 C<&checkauth> fetches user and session information from C<$query> and
232 ensures that the user is authorized to run scripts that require
233 authorization.
234
235 The C<$flagsrequired> argument specifies the required privileges
236 the user must have if the username and password are correct.
237 It should be specified as a reference-to-hash; keys in the hash
238 should be the "flags" for the user, as specified in the Members
239 intranet module. Any key specified must correspond to a "flag"
240 in the userflags table. E.g., { circulate => 1 } would specify
241 that the user must have the "circulate" privilege in order to
242 proceed. To make sure that access control is correct, the
243 C<$flagsrequired> parameter must be specified correctly.
244
245 The C<$type> argument specifies whether the template should be
246 retrieved from the opac or intranet directory tree.  "opac" is
247 assumed if it is not specified; however, if C<$type> is specified,
248 "intranet" is assumed if it is not "opac".
249
250 If C<$query> does not have a valid session ID associated with it
251 (i.e., the user has not logged in) or if the session has expired,
252 C<&checkauth> presents the user with a login page (from the point of
253 view of the original script, C<&checkauth> does not return). Once the
254 user has authenticated, C<&checkauth> restarts the original script
255 (this time, C<&checkauth> returns).
256
257 The login page is provided using a HTML::Template, which is set in the
258 systempreferences table or at the top of this file. The variable C<$type>
259 selects which template to use, either the opac or the intranet 
260 authentification template.
261
262 C<&checkauth> returns a user ID, a cookie, and a session ID. The
263 cookie should be sent back to the browser; it verifies that the user
264 has authenticated.
265
266 =cut
267
268
269
270 sub checkauth {
271         my $query=shift;
272         # $authnotrequired will be set for scripts which will run without authentication
273         my $authnotrequired = shift;
274         my $flagsrequired = shift;
275         my $type = shift;
276         $type = 'opac' unless $type;
277
278         my $dbh = C4::Context->dbh;
279         my $timeout = C4::Context->preference('timeout');
280         $timeout = 600 unless $timeout;
281
282         my $template_name;
283         if ($type eq 'opac') {
284                 $template_name = "opac-auth.tmpl";
285         } else {
286                 $template_name = "auth.tmpl";
287         }
288
289         # state variables
290         my $loggedin = 0;
291         my %info;
292         my ($userid, $cookie, $sessionID, $flags);
293         my $logout = $query->param('logout.x');
294         if ($userid = $ENV{'REMOTE_USER'}) {
295                 # Using Basic Authentication, no cookies required
296                 $cookie=$query->cookie(-name => 'sessionID',
297                                 -value => '',
298                                 -expires => '');
299                 $loggedin = 1;
300         } elsif ($sessionID=$query->cookie('sessionID')) {
301                 my ($ip , $lasttime);
302                 ($userid, $ip, $lasttime) = $dbh->selectrow_array(
303                                 "SELECT userid,ip,lasttime FROM sessions WHERE sessionid=?",
304                                                                 undef, $sessionID);
305                 if ($logout) {
306                 # voluntary logout the user
307                 $dbh->do("DELETE FROM sessions WHERE sessionID=?", undef, $sessionID);
308                 $sessionID = undef;
309                 $userid = undef;
310                 open L, ">>/tmp/sessionlog";
311                 my $time=localtime(time());
312                 printf L "%20s from %16s logged out at %30s (manually).\n", $userid, $ip, $time;
313                 close L;
314                 }
315                 if ($userid) {
316                 if ($lasttime<time()-$timeout) {
317                         # timed logout
318                         $info{'timed_out'} = 1;
319                         $dbh->do("DELETE FROM sessions WHERE sessionID=?", undef, $sessionID);
320                         $userid = undef;
321                         $sessionID = undef;
322                         open L, ">>/tmp/sessionlog";
323                         my $time=localtime(time());
324                         printf L "%20s from %16s logged out at %30s (inactivity).\n", $userid, $ip, $time;
325                         close L;
326                 } elsif ($ip ne $ENV{'REMOTE_ADDR'}) {
327                         # Different ip than originally logged in from
328                         $info{'oldip'} = $ip;
329                         $info{'newip'} = $ENV{'REMOTE_ADDR'};
330                         $info{'different_ip'} = 1;
331                         $dbh->do("DELETE FROM sessions WHERE sessionID=?", undef, $sessionID);
332                         $sessionID = undef;
333                         $userid = undef;
334                         open L, ">>/tmp/sessionlog";
335                         my $time=localtime(time());
336                         printf L "%20s from logged out at %30s (ip changed from %16s to %16s).\n", $userid, $time, $ip, $info{'newip'};
337                         close L;
338                 } else {
339                         $cookie=$query->cookie(-name => 'sessionID',
340                                         -value => $sessionID,
341                                         -expires => '');
342                         $dbh->do("UPDATE sessions SET lasttime=? WHERE sessionID=?",
343                                 undef, (time(), $sessionID));
344                         $flags = haspermission($dbh, $userid, $flagsrequired);
345                         if ($flags) {
346                         $loggedin = 1;
347                         } else {
348                         $info{'nopermission'} = 1;
349                         }
350                 }
351                 }
352         }
353         unless ($userid) {
354                 $sessionID=int(rand()*100000).'-'.time();
355                 $userid=$query->param('userid');
356                 my $password=$query->param('password');
357                 my ($return, $cardnumber) = checkpw($dbh,$userid,$password);
358                 if ($return) {
359                 $dbh->do("DELETE FROM sessions WHERE sessionID=? AND userid=?",
360                         undef, ($sessionID, $userid));
361                 $dbh->do("INSERT INTO sessions (sessionID, userid, ip,lasttime) VALUES (?, ?, ?, ?)",
362                         undef, ($sessionID, $userid, $ENV{'REMOTE_ADDR'}, time()));
363                 open L, ">>/tmp/sessionlog";
364                 my $time=localtime(time());
365                 printf L "%20s from %16s logged in  at %30s.\n", $userid, $ENV{'REMOTE_ADDR'}, $time;
366                 close L;
367                 $cookie=$query->cookie(-name => 'sessionID',
368                                         -value => $sessionID,
369                                         -expires => '');
370                 if ($flags = haspermission($dbh, $userid, $flagsrequired)) {
371                         $loggedin = 1;
372                 } else {
373                         $info{'nopermission'} = 1;
374                 }
375                 } else {
376                 if ($userid) {
377                         $info{'invalid_username_or_password'} = 1;
378                 }
379                 }
380         }
381         my $insecure = C4::Context->boolean_preference('insecure');
382         # finished authentification, now respond
383         if ($loggedin || $authnotrequired || (defined($insecure) && $insecure)) {
384                 # successful login
385                 unless ($cookie) {
386                 $cookie=$query->cookie(-name => 'sessionID',
387                                         -value => '',
388                                         -expires => '');
389                 }
390                 return ($userid, $cookie, $sessionID, $flags);
391         }
392         # else we have a problem...
393         # get the inputs from the incoming query
394         my @inputs =();
395         foreach my $name (param $query) {
396                 (next) if ($name eq 'userid' || $name eq 'password');
397                 my $value = $query->param($name);
398                 push @inputs, {name => $name , value => $value};
399         }
400
401         my $template = gettemplate($template_name, $type,$query);
402         $template->param(INPUTS => \@inputs);
403         $template->param(loginprompt => 1) unless $info{'nopermission'};
404
405         my $self_url = $query->url(-absolute => 1);
406         $template->param(url => $self_url);
407         $template->param(\%info);
408         $cookie=$query->cookie(-name => 'sessionID',
409                                         -value => $sessionID,
410                                         -expires => '');
411         print $query->header(
412                 -type => guesstype($template->output),
413                 -cookie => $cookie
414                 ), $template->output;
415         exit;
416 }
417
418
419
420 # this checkpw is a LDAP based one
421 # it connects to LDAP (anonymous)
422 # it retrieve $userid a-login
423 # then compare $password with a-weak
424 # then get the LDAP entry
425 # and calls the memberadd if necessary
426
427 sub checkpw {
428         my ($dbh, $userid, $password) = @_;
429         if ($userid eq C4::Context->config('user') && $password eq C4::Context->config('pass')) {
430                 # Koha superuser account
431                 return 2;
432         }
433         ##################################################
434         ### LOCAL
435         ### Change the code below to match your own LDAP server.
436         ##################################################
437         # LDAP connexion parameters
438         my $ldapserver = 'your.ldap.server.com';
439         # Infos to do an anonymous bind
440         my $ldapinfos = 'a-section=people,dc=emn,dc=fr ';
441         my $name  = "a-section=people,dc=emn,dc=fr";
442         my $db = Net::LDAP->new( $ldapserver );
443         
444         # do an anonymous bind
445         my $res =$db->bind();
446         # check connexion
447         if($res->code) {
448                 # auth refused
449                 warn "LDAP Auth impossible : server not responding";
450                 return 0;
451         # search user
452         } else {
453                 my $userdnsearch = $db->search(base => $name,
454                                 filter =>"(a-login=$userid)",
455                                 );
456                 if($userdnsearch->code || ! ( $userdnsearch-> count eq 1 ) ) {
457                         warn "LDAP Auth impossible : user unknown in LDAP";
458                         return 0;
459                 };
460                 # compare a-weak with $password.
461                 # The a-weak LDAP field contains the password
462                 my $userldapentry=$userdnsearch -> shift_entry;
463                 my $cmpmesg = $db -> compare ( $userldapentry, attr => 'a-weak', value => $password );
464                 if( $cmpmesg -> code != 6 ) {
465                         warn "LDAP Auth impossible : wrong password";
466                         return 0;
467                 };
468                 # build LDAP hash
469                 my %memberhash;
470                 my $x =$userldapentry->{asn}{attributes};
471                 my $key;
472                 foreach my $k ( @$x) {
473                         foreach my $k2 (keys %$k) {
474                                 if ($k2 eq 'type') {
475                                         $key = $$k{$k2};
476                                 } else {
477                                         my $a = @$k{$k2};
478                                         foreach my $k3 (@$a) {
479                                                 $memberhash{$key} .= $k3." ";
480                                         }
481                                 }
482                         }
483                 }
484                 #
485                 # BUILD %borrower to CREATE or MODIFY BORROWER
486                 # change $memberhash{'xxx'} to fit your ldap structure.
487                 # check twice that mandatory fields are correctly filled
488                 #
489                 my %borrower;
490                 $borrower{cardnumber} = $userid;
491                 $borrower{firstname} = $memberhash{givenName}; # MANDATORY FIELD
492                 $borrower{surname} = $memberhash{sn}; # MANDATORY FIELD
493                 $borrower{initials} = substr($borrower{firstname},0,1).substr($borrower{surname},0,1)."  "; # MANDATORY FIELD
494                 $borrower{streetaddress} = $memberhash{l}." "; # MANDATORY FIELD
495                 $borrower{city} = " "; # MANDATORY FIELD
496                 $borrower{phone} = " "; # MANDATORY FIELD
497                 $borrower{branchcode} = $memberhash{branch}; # MANDATORY FIELD
498                 $borrower{emailaddress} = $memberhash{mail};
499                 $borrower{categorycode} = $memberhash{employeeType};
500         ##################################################
501         ### /LOCAL
502         ### No change needed after this line (unless there's a bug ;-) )
503         ##################################################
504                 # check if borrower exists
505                 my $sth=$dbh->prepare("select password from borrowers where cardnumber=?");
506                 $sth->execute($userid);
507                 if ($sth->rows) {
508                         # it exists, MODIFY
509                         my $sth2 = $dbh->prepare("update borrowers set firstname=?,surname=?,initials=?,streetaddress=?,city=?,phone=?, categorycode=?,branchcode=?,emailaddress=?,sort1=? where cardnumber=?");
510                         $sth2->execute($borrower{firstname},$borrower{surname},$borrower{initials},
511                                                         $borrower{streetaddress},$borrower{city},$borrower{phone},
512                                                         $borrower{categorycode},$borrower{branchcode},$borrower{emailaddress},
513                                                         $borrower{sort1} ,$userid);
514                 } else {
515                         # it does not exists, ADD borrower
516                         my $borrowerid = newmember(%borrower);
517                 }
518                 #
519                 # CREATE or MODIFY PASSWORD/LOGIN
520                 #
521                 # search borrowerid
522                 $sth = $dbh->prepare("select borrowernumber from borrowers where cardnumber=?");
523                 $sth->execute($userid);
524                 my ($borrowerid)=$sth->fetchrow;
525                 my $digest=md5_base64($password);
526                 changepassword($userid,$borrowerid,$digest);
527         }
528
529 # INTERNAL AUTH. The borrower entry has been created by LDAP if needed, The auth is probably useless
530 # but it's the standard Auth.pm here.
531         my $sth=$dbh->prepare("select password,cardnumber from borrowers where userid=?");
532         $sth->execute($userid);
533         if ($sth->rows) {
534                 my ($md5password,$cardnumber) = $sth->fetchrow;
535                 if (md5_base64($password) eq $md5password) {
536                         return 1,$cardnumber;
537                 }
538         }
539         my $sth=$dbh->prepare("select password from borrowers where cardnumber=?");
540         $sth->execute($userid);
541         if ($sth->rows) {
542                 my ($md5password) = $sth->fetchrow;
543                 if (md5_base64($password) eq $md5password) {
544                         return 1,$userid;
545                 }
546         }
547         return 0;
548 }
549
550 sub getuserflags {
551     my $cardnumber=shift;
552     my $dbh=shift;
553     my $userflags;
554     my $sth=$dbh->prepare("SELECT flags FROM borrowers WHERE cardnumber=?");
555     $sth->execute($cardnumber);
556     my ($flags) = $sth->fetchrow;
557     $sth=$dbh->prepare("SELECT bit, flag, defaulton FROM userflags");
558     $sth->execute;
559     while (my ($bit, $flag, $defaulton) = $sth->fetchrow) {
560         if (($flags & (2**$bit)) || $defaulton) {
561             $userflags->{$flag}=1;
562         }
563     }
564     return $userflags;
565 }
566
567 sub haspermission {
568     my ($dbh, $userid, $flagsrequired) = @_;
569     my $sth=$dbh->prepare("SELECT cardnumber FROM borrowers WHERE userid=?");
570     $sth->execute($userid);
571     my ($cardnumber) = $sth->fetchrow;
572     ($cardnumber) || ($cardnumber=$userid);
573     my $flags=getuserflags($cardnumber,$dbh);
574     my $configfile;
575     if ($userid eq C4::Context->config('user')) {
576         # Super User Account from /etc/koha.conf
577         $flags->{'superlibrarian'}=1;
578      }
579      if ($userid eq 'demo' && C4::Context->config('demo')) {
580         # Demo user that can do "anything" (demo=1 in /etc/koha.conf)
581         $flags->{'superlibrarian'}=1;
582     }
583     return $flags if $flags->{superlibrarian};
584     foreach (keys %$flagsrequired) {
585         return $flags if $flags->{$_};
586     }
587     return 0;
588 }
589
590 sub getborrowernumber {
591     my ($userid) = @_;
592     my $dbh = C4::Context->dbh;
593     for my $field ('userid', 'cardnumber') {
594       my $sth=$dbh->prepare
595           ("select borrowernumber from borrowers where $field=?");
596       $sth->execute($userid);
597       if ($sth->rows) {
598         my ($bnumber) = $sth->fetchrow;
599         return $bnumber;
600       }
601     }
602     return 0;
603 }
604
605 END { }       # module clean-up code here (global destructor)
606 1;
607 __END__
608
609 =back
610
611 =head1 SEE ALSO
612
613 CGI(3)
614
615 C4::Output(3)
616
617 Digest::MD5(3)
618
619 =cut