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