Auth_with_ldap.pm - Expanded comments and field mappings.
[koha.git] / C4 / Auth_with_ldap.pm
1 package C4::Auth_with_ldap;
2
3 # Copyright 2000-2002 Katipo Communications
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 2 of the License, or (at your option) any later
10 # version.
11 #
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License along with
17 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
18 # Suite 330, Boston, MA  02111-1307 USA
19
20 use strict;
21 use Digest::MD5 qw(md5_base64);
22
23 use C4::Context;
24 use C4::Members qw(AddMember );
25
26 use Net::LDAP;
27 use Net::LDAP::Filter;
28 # use Net::LDAP qw(:all);
29
30 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
31
32 BEGIN {
33         require Exporter;
34         $VERSION = 3.01;        # set the version for version checking
35         our $debug = $ENV{DEBUG} || 0;
36         @ISA    = qw(Exporter C4::Auth);
37         @EXPORT = qw( checkauth );
38 }
39
40 =head1 NAME
41
42 C4::Auth - Authenticates Koha users
43
44 =head1 SYNOPSIS
45
46   use C4::Auth_with_ldap;
47
48 =head1 LDAP specific
49
50     This module is specific to LDAP authentification. It requires Net::LDAP package and one or more
51         working LDAP servers.
52         To use it :
53            * Modify ldapserver and ldapinfos via web "Preferences".
54            * Modify the values (right side) of %mapping pairs, to match your LDAP fields.
55            * Modify $ldapname and $ldappassword, if required.
56
57         It is assumed your user records are stored according to the inetOrgPerson schema, RFC#2798.
58         Thus the username must match the "uid" field, and the password must match the "userPassword" field.
59
60         Make sure that the required fields are populated in your LDAP database.  What are they?  Well, in
61         mysql you can check the database table "borrowers" like this:
62
63         mysql> show COLUMNS from borrowers;
64                 +------------------+--------------+------+-----+---------+----------------+
65                 | Field            | Type         | Null | Key | Default | Extra          |
66                 +------------------+--------------+------+-----+---------+----------------+
67                 | borrowernumber   | int(11)      | NO   | PRI | NULL    | auto_increment | 
68                 | cardnumber       | varchar(16)  | YES  | UNI | NULL    |                | 
69                 | surname          | mediumtext   | NO   |     |         |                | 
70                 | firstname        | text         | YES  |     | NULL    |                | 
71                 | title            | mediumtext   | YES  |     | NULL    |                | 
72                 | othernames       | mediumtext   | YES  |     | NULL    |                | 
73                 | initials         | text         | YES  |     | NULL    |                | 
74                 | streetnumber     | varchar(10)  | YES  |     | NULL    |                | 
75                 | streettype       | varchar(50)  | YES  |     | NULL    |                | 
76                 | address          | mediumtext   | NO   |     |         |                | 
77                 | address2         | text         | YES  |     | NULL    |                | 
78                 | city             | mediumtext   | NO   |     |         |                | 
79                 | zipcode          | varchar(25)  | YES  |     | NULL    |                | 
80                 | email            | mediumtext   | YES  |     | NULL    |                | 
81                 | phone            | text         | YES  |     | NULL    |                | 
82                 | mobile           | varchar(50)  | YES  |     | NULL    |                | 
83                 | fax              | mediumtext   | YES  |     | NULL    |                | 
84                 | emailpro         | text         | YES  |     | NULL    |                | 
85                 | phonepro         | text         | YES  |     | NULL    |                | 
86                 | B_streetnumber   | varchar(10)  | YES  |     | NULL    |                | 
87                 | B_streettype     | varchar(50)  | YES  |     | NULL    |                | 
88                 | B_address        | varchar(100) | YES  |     | NULL    |                | 
89                 | B_city           | mediumtext   | YES  |     | NULL    |                | 
90                 | B_zipcode        | varchar(25)  | YES  |     | NULL    |                | 
91                 | B_email          | text         | YES  |     | NULL    |                | 
92                 | B_phone          | mediumtext   | YES  |     | NULL    |                | 
93                 | dateofbirth      | date         | YES  |     | NULL    |                | 
94                 | branchcode       | varchar(10)  | NO   | MUL |         |                | 
95                 | categorycode     | varchar(10)  | NO   | MUL |         |                | 
96                 | dateenrolled     | date         | YES  |     | NULL    |                | 
97                 | dateexpiry       | date         | YES  |     | NULL    |                | 
98                 | gonenoaddress    | tinyint(1)   | YES  |     | NULL    |                | 
99                 | lost             | tinyint(1)   | YES  |     | NULL    |                | 
100                 | debarred         | tinyint(1)   | YES  |     | NULL    |                | 
101                 | contactname      | mediumtext   | YES  |     | NULL    |                | 
102                 | contactfirstname | text         | YES  |     | NULL    |                | 
103                 | contacttitle     | text         | YES  |     | NULL    |                | 
104                 | guarantorid      | int(11)      | YES  |     | NULL    |                | 
105                 | borrowernotes    | mediumtext   | YES  |     | NULL    |                | 
106                 | relationship     | varchar(100) | YES  |     | NULL    |                | 
107                 | ethnicity        | varchar(50)  | YES  |     | NULL    |                | 
108                 | ethnotes         | varchar(255) | YES  |     | NULL    |                | 
109                 | sex              | varchar(1)   | YES  |     | NULL    |                | 
110                 | password         | varchar(30)  | YES  |     | NULL    |                | 
111                 | flags            | int(11)      | YES  |     | NULL    |                | 
112                 | userid           | varchar(30)  | YES  | MUL | NULL    |                | 
113                 | opacnote         | mediumtext   | YES  |     | NULL    |                | 
114                 | contactnote      | varchar(255) | YES  |     | NULL    |                | 
115                 | sort1            | varchar(80)  | YES  |     | NULL    |                | 
116                 | sort2            | varchar(80)  | YES  |     | NULL    |                | 
117                 +------------------+--------------+------+-----+---------+----------------+
118                 50 rows in set (0.01 sec)
119         
120                 Then %mappings establishes the relationship between mysql field and LDAP attribute.
121
122 =cut
123
124 # Redefine checkauth:
125 # connect to LDAP (named or anonymous)
126 # ~ retrieves $userid from "uid"
127 # ~ then compares $password with userPassword 
128 # ~ then gets the LDAP entry
129 # ~ and calls the memberadd if necessary
130
131 use vars qw(%mapping @ldaphosts $base $ldapname $ldappassword);
132
133 %mapping = (
134         firstname     => 'givenName',
135         surname       => 'sn',
136         address       => 'postalAddress',
137         city              => 'l',
138         zipcode       => 'postalCode',
139         branchcode    => 'branch',
140         emailaddress  => 'mail',
141         categorycode  => 'employeeType',
142         phone         => 'telephoneNumber',
143 );
144
145 my $prefhost;
146 if ($prefhost = C4::Context->preference('ldapserver')) {        # assignment, not comparison
147         warn "Using preference from ldapserver: $prefhost";
148         (@ldaphosts) = split /\|/,$prefhost;    # Potentially multiple LDAP hosts!
149         $base = C4::Context->preference('ldapinfos') || '';             # probably will fail w/o base
150 } else {
151         (@ldaphosts) = (qw(localhost));                 # Potentially multiple LDAP hosts!
152         $base = "dc=metavore,dc=com";                   # But only 1 base.
153 }
154
155 $ldapname     = "cn=Manager,$base";             # Your LDAP user.                               EDIT THIS LINE.
156 $ldappassword = 'metavore';                             # Your LDAP user's password.    EDIT THIS LINE.
157
158 my %config = (
159         anonymous => ($ldapname and $ldappassword) ? 0 : 1,
160         replicate => 1,         #    add from LDAP to Koha database for new user
161            update => 1,         # update from LDAP to Koha database for existing user
162 );
163
164 sub description ($) {
165         my $result = shift or return undef;
166         return "LDAP error #" . $result->code
167                         . ": " . $result->error_name . "\n"
168                         . "# " . $result->error_text . "\n";
169 }
170
171 sub checkauth {
172     my ($dbh, $userid, $password) = @_;
173     if (   $userid   eq C4::Context->config('user')
174         && $password eq C4::Context->config('pass') )
175     {
176         return 2;       # Koha superuser account
177     }
178     my $db = Net::LDAP->new(\@ldaphosts);
179         #$debug and $db->debug(5);
180         my $filter = Net::LDAP::Filter->new("uid=$userid") or die "Failed to create new Net::LDAP::Filter";
181     my $res = ($config{anonymous}) ? $db->bind : $db->bind($ldapname, password=>$ldappassword);
182     if ($res->code) {           # connection refused
183         warn "LDAP bind failed as $ldapname: " . description($res);
184         return 0;
185     }
186         my $search = $db->search(
187                   base => $base,
188                 filter => $filter,
189                 # attrs => ['*'],
190         ) or die "LDAP search failed to return object.";
191         my $count = $search->count;
192         if ($search->code > 0) {
193                 warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count) . description($search);
194                 return 0;
195         }
196         if ($count != 1) {
197                 warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count);
198                 return 0;
199         }
200
201         my $userldapentry = $search->shift_entry;
202         my $cmpmesg = $db->compare( $userldapentry, attr=>'userPassword', value => $password );
203         if($cmpmesg->code != 6) {
204                 warn "LDAP Auth rejected : invalid password for user '$userid'. " . description($cmpmesg);
205                 return 0;
206         }
207         unless($config{update} or $config{replicate}) {
208                 return 1;
209         }
210         my %borrower = ldap_entry_2_hash($userldapentry,$userid);
211         if (exists_local($userid)) {
212                 ($config{update}   ) and &update_local($userid,$password,%borrower);
213         } else {
214                 ($config{replicate}) and AddMember(%borrower);
215         }
216         return 1;
217 }
218
219 # Pass LDAP entry object and local cardnumber (userid).
220 # Returns borrower hash.
221 # Edit %mapping so $memberhash{'xxx'} fits your ldap structure.
222 # Ensure that mandatory fields are correctly filled!
223 #
224 sub ldap_entry_2_hash ($$) {
225         my $userldapentry = shift;
226         my %borrower = ( cardnumber => shift );
227         my %memberhash;
228         my $x = $userldapentry->{asn}{attributes} or return undef;
229         my $key;
230         foreach my $k (@$x) {
231                 foreach my $k2 ( keys %$k ) {
232                         if ($k2 eq 'type') {
233                                 $key = $$k{$k2};
234                         } else {
235                                 $memberhash{$key} .= map {$_ . " "} @$k{$k2};
236                         }
237                 }
238         }
239         foreach my $key (%mapping) {
240                 my $data = $memberhash{$mapping{$key}}; 
241                 defined $data or $data = ' ';
242                 $borrower{$key} = ($data ne '') ? $data : ' ' ;
243         }
244         $borrower{initials} = $memberhash{initials} || 
245                 ( substr($borrower{'firstname'},0,1)
246                 . substr($borrower{ 'surname' },0,1)
247                 . "  ");
248         return %borrower;
249 }
250
251 sub exists_local($) {
252         my $sth = C4::Context->dbh->prepare("SELECT password from borrowers WHERE cardnumber=?");
253         $sth->execute(shift);
254         return ($sth->rows) ? 1 : 0 ;
255 }
256
257 sub update_local($$%) {
258         # warn "MODIFY borrower";
259         my   $userid = shift or return undef;
260         my   $digest = md5_base64(shift) or return undef;
261         my %borrower = shift or return undef;
262         my $dbh = C4::Context->dbh;
263         my $sth = $dbh->prepare("
264 UPDATE  borrowers 
265 SET     firstname=?,surname=?,initials=?,streetaddress=?,city=?,phone=?, categorycode=?,branchcode=?,emailaddress=?,sort1=?
266 WHERE   cardnumber=?
267         ");
268         $sth->execute(
269                 $borrower{firstname},    $borrower{surname},
270                 $borrower{initials},     $borrower{streetaddress},
271                 $borrower{city},         $borrower{phone},
272                 $borrower{categorycode}, $borrower{branchcode},
273                 $borrower{emailaddress}, $borrower{sort1},
274                 $userid
275         );
276
277         # MODIFY PASSWORD/LOGIN
278         # search borrowerid
279         $sth = $dbh->prepare("SELECT borrowernumber from borrowers WHERE cardnumber=? ");
280         $sth->execute($userid);
281         my ($borrowerid) = $sth->fetchrow;
282         # warn "change local password for $borrowerid setting $password";
283         changepassword($userid, $borrowerid, $digest);
284
285         # Confirm changes
286         my $cardnumber;
287         $sth = $dbh->prepare("SELECT password,cardnumber from borrowers WHERE userid=? ");
288         $cardnumber = confirmer($sth,$userid,$digest) and return $cardnumber;
289     $sth = $dbh->prepare("SELECT password,cardnumber from borrowers WHERE cardnumber=? ");
290         $cardnumber = confirmer($sth,$userid,$digest) and return $cardnumber;
291         die "Unexpected error after password update to $userid / $cardnumber.";
292 }
293
294 sub confirmer($$$) {
295         my    $sth = shift or return undef;
296         my $userid = shift or return undef;
297         my $digest = shift or return undef;
298         $sth->execute($userid);
299         if ($sth->rows) {
300                 my ($md5password, $othernum) = $sth->fetchrow;
301         ($digest eq $md5password) and return $othernum;
302                 warn "Password mismatch after update to userid=$userid";
303                 return undef;
304     }
305         warn "Could not recover record after updating password for userid=$userid";
306         return 0;
307 }
308 1;
309 __END__
310
311 =back
312
313 =head1 SEE ALSO
314
315 CGI(3)
316
317 Net::LDAP()
318
319 Digest::MD5(3)
320
321 =cut