Big LDAP changes, module test for Context.pm, still more yet to come.
[koha.git] / C4 / Auth_with_ldap.pm
blob42f1c71d80935b1e456a0238c055b25eb7d125bc
1 package C4::Auth_with_ldap;
3 # Copyright 2000-2002 Katipo Communications
5 # This file is part of Koha.
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.
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.
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
20 use strict;
21 use Digest::MD5 qw(md5_base64);
23 use C4::Context;
24 use C4::Members qw(AddMember changepassword);
25 use C4::Utils qw( :all );
26 use Net::LDAP;
27 use Net::LDAP::Filter;
29 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $debug);
31 BEGIN {
32 require Exporter;
33 $VERSION = 3.01; # set the version for version checking
34 $debug = $ENV{DEBUG} || 0;
35 @ISA = qw(Exporter C4::Auth);
36 @EXPORT = qw( checkauth );
39 =head1 NAME
41 C4::Auth - Authenticates Koha users
43 =head1 SYNOPSIS
45 use C4::Auth_with_ldap;
47 =head1 LDAP specific
49 This module is specific to LDAP authentification. It requires Net::LDAP package and one or more
50 working LDAP servers.
51 To use it :
52 * Modify ldapserver and ldapinfos via web "Preferences".
53 * Modify the values (right side) of %mapping pairs, to match your LDAP fields.
54 * Modify $ldapname and $ldappassword, if required.
56 It is assumed your user records are stored according to the inetOrgPerson schema, RFC#2798.
57 Thus the username must match the "uid" field, and the password must match the "userPassword" field.
59 Make sure that the required fields are populated in your LDAP database. What are they? Well, in
60 mysql you can check the database table "borrowers" like this:
62 mysql> show COLUMNS from borrowers;
63 +------------------+--------------+------+-----+---------+----------------+
64 | Field | Type | Null | Key | Default | Extra |
65 +------------------+--------------+------+-----+---------+----------------+
66 | borrowernumber | int(11) | NO | PRI | NULL | auto_increment |
67 | cardnumber | varchar(16) | YES | UNI | NULL | |
68 | surname | mediumtext | NO | | | |
69 | firstname | text | YES | | NULL | |
70 | title | mediumtext | YES | | NULL | |
71 | othernames | mediumtext | YES | | NULL | |
72 | initials | text | YES | | NULL | |
73 | streetnumber | varchar(10) | YES | | NULL | |
74 | streettype | varchar(50) | YES | | NULL | |
75 | address | mediumtext | NO | | | |
76 | address2 | text | YES | | NULL | |
77 | city | mediumtext | NO | | | |
78 | zipcode | varchar(25) | YES | | NULL | |
79 | email | mediumtext | YES | | NULL | |
80 | phone | text | YES | | NULL | |
81 | mobile | varchar(50) | YES | | NULL | |
82 | fax | mediumtext | YES | | NULL | |
83 | emailpro | text | YES | | NULL | |
84 | phonepro | text | YES | | NULL | |
85 | B_streetnumber | varchar(10) | YES | | NULL | |
86 | B_streettype | varchar(50) | YES | | NULL | |
87 | B_address | varchar(100) | YES | | NULL | |
88 | B_city | mediumtext | YES | | NULL | |
89 | B_zipcode | varchar(25) | YES | | NULL | |
90 | B_email | text | YES | | NULL | |
91 | B_phone | mediumtext | YES | | NULL | |
92 | dateofbirth | date | YES | | NULL | |
93 | branchcode | varchar(10) | NO | MUL | | |
94 | categorycode | varchar(10) | NO | MUL | | |
95 | dateenrolled | date | YES | | NULL | |
96 | dateexpiry | date | YES | | NULL | |
97 | gonenoaddress | tinyint(1) | YES | | NULL | |
98 | lost | tinyint(1) | YES | | NULL | |
99 | debarred | tinyint(1) | YES | | NULL | |
100 | contactname | mediumtext | YES | | NULL | |
101 | contactfirstname | text | YES | | NULL | |
102 | contacttitle | text | YES | | NULL | |
103 | guarantorid | int(11) | YES | | NULL | |
104 | borrowernotes | mediumtext | YES | | NULL | |
105 | relationship | varchar(100) | YES | | NULL | |
106 | ethnicity | varchar(50) | YES | | NULL | |
107 | ethnotes | varchar(255) | YES | | NULL | |
108 | sex | varchar(1) | YES | | NULL | |
109 | password | varchar(30) | YES | | NULL | |
110 | flags | int(11) | YES | | NULL | |
111 | userid | varchar(30) | YES | MUL | NULL | |
112 | opacnote | mediumtext | YES | | NULL | |
113 | contactnote | varchar(255) | YES | | NULL | |
114 | sort1 | varchar(80) | YES | | NULL | |
115 | sort2 | varchar(80) | YES | | NULL | |
116 +------------------+--------------+------+-----+---------+----------------+
117 50 rows in set (0.01 sec)
119 Then %mappings establishes the relationship between mysql field and LDAP attribute.
121 =cut
123 # Redefine checkauth:
124 # connect to LDAP (named or anonymous)
125 # ~ retrieves $userid from "uid"
126 # ~ then compares $password with userPassword
127 # ~ then gets the LDAP entry
128 # ~ and calls the memberadd if necessary
130 sub ldapserver_error ($) {
131 return sprintf('No ldapserver "%s" defined in KOHA_CONF: ' . $ENV{KOHA_CONF}, shift);
134 use vars qw($mapping @ldaphosts $base $ldapname $ldappassword);
135 my $context = C4::Context->new() or die 'C4::Context->new failed';
136 my $ldap = $context->{server}->{ldapserver} or die 'No "ldapserver" in server hash from KOHA_CONF: ' . $ENV{KOHA_CONF};
137 my $prefhost = $ldap->{hostname} or die ldapserver_error('hostname');
138 my $base = $ldap->{base} or die ldapserver_error('base');
139 $ldapname = $ldap->{user} or die ldapserver_error('user');
140 $ldappassword = $ldap->{pass} or die ldapserver_error('pass');
141 our %mapping = %{$ldap->{mapping}} or die ldapserver_error('mapping');
142 my @mapkeys = keys %mapping;
143 print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys ( total ): ", join ' ', @mapkeys, "\n";
144 @mapkeys = grep {defined $mapping{$_}->{is}} @mapkeys;
145 print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys (populated): ", join ' ', @mapkeys, "\n";
147 my %config = (
148 anonymous => ($ldapname and $ldappassword) ? 0 : 1,
149 replicate => $ldap->{replicate} || 1, # add from LDAP to Koha database for new user
150 update => $ldap->{update} || 1, # update from LDAP to Koha database for existing user
153 sub description ($) {
154 my $result = shift or return undef;
155 return "LDAP error #" . $result->code
156 . ": " . $result->error_name . "\n"
157 . "# " . $result->error_text . "\n";
160 sub checkauth {
161 my ($dbh, $userid, $password) = @_;
162 if ( $userid eq C4::Context->config('user')
163 && $password eq C4::Context->config('pass') )
165 return 2; # Koha superuser account
167 my $db = Net::LDAP->new([$prefhost]);
168 #$debug and $db->debug(5);
169 my $filter = Net::LDAP::Filter->new("uid=$userid") or die "Failed to create new Net::LDAP::Filter";
170 my $res = ($config{anonymous}) ? $db->bind : $db->bind($ldapname, password=>$ldappassword);
171 if ($res->code) { # connection refused
172 warn "LDAP bind failed as $ldapname: " . description($res);
173 return 0;
175 my $search = $db->search(
176 base => $base,
177 filter => $filter,
178 # attrs => ['*'],
179 ) or die "LDAP search failed to return object.";
180 my $count = $search->count;
181 if ($search->code > 0) {
182 warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count) . description($search);
183 return 0;
185 if ($count != 1) {
186 warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count);
187 return 0;
190 my $userldapentry = $search->shift_entry;
191 my $cmpmesg = $db->compare( $userldapentry, attr=>'userPassword', value => $password );
192 if($cmpmesg->code != 6) {
193 warn "LDAP Auth rejected : invalid password for user '$userid'. " . description($cmpmesg);
194 return 0;
196 unless($config{update} or $config{replicate}) {
197 return 1;
199 my %borrower = ldap_entry_2_hash($userldapentry,$userid);
200 if (exists_local($userid)) {
201 ($config{update} ) and &update_local($userid,$password,%borrower);
202 } else {
203 ($config{replicate}) and warn "Replicating!!" and AddMember(%borrower);
205 return 1;
208 # Pass LDAP entry object and local cardnumber (userid).
209 # Returns borrower hash.
210 # Edit KOHA_CONF so $memberhash{'xxx'} fits your ldap structure.
211 # Ensure that mandatory fields are correctly filled!
213 sub ldap_entry_2_hash ($$) {
214 my $userldapentry = shift;
215 my %borrower = ( cardnumber => shift );
216 my %memberhash;
217 print "keys(\%\$userldapentry) = " . join(', ', keys %$userldapentry), "\n";
218 print $userldapentry->dump();
219 foreach (keys %$userldapentry) {
220 print "\n\nLDAP key: $_\t", sprintf('(%s)', ref $userldapentry->{$_}), "\n";
221 hashdump("LDAP key: ",$userldapentry->{$_});
223 warn "->{asn}->{attributes} : " . $userldapentry->{asn}->{attributes} ;
224 my $x = $userldapentry->{asn}->{attributes} or return undef;
225 my $key;
227 # asn (HASH)
228 # LDAP key: ->{attributes} = ARRAY w/ 17 members.
229 # LDAP key: ->{attributes}->{HASH(0x9234290)} = HASH w/ 2 keys.
230 # LDAP key: ->{attributes}->{HASH(0x9234290)}->{type} = cn
231 # LDAP key: ->{attributes}->{HASH(0x9234290)}->{vals} = ARRAY w/ 3 members.
232 # LDAP key: ->{attributes}->{HASH(0x9234290)}->{vals}->{ sss} = sss
233 # LDAP key: ->{attributes}->{HASH(0x9234290)}->{vals}->{ Steve Smith} = Steve Smith
234 # LDAP key: ->{attributes}->{HASH(0x9234290)}->{vals}->{Steve S. Smith} = Steve S. Smith
235 # $x $anon
236 # LDAP key: ->{attributes}->{HASH(0x9234490)} = HASH w/ 2 keys.
237 # LDAP key: ->{attributes}->{HASH(0x9234490)}->{type} = o
238 # LDAP key: ->{attributes}->{HASH(0x9234490)}->{vals} = ARRAY w/ 1 members.
239 # LDAP key: ->{attributes}->{HASH(0x9234490)}->{vals}->{metavore} = metavore
240 # $x=([ cn=>['sss','Steve Smith','Steve S. Smith'], sss, o=>['metavore'], ])
241 # . . . . .
243 foreach my $anon (@$x) {
244 $key = $anon->{type} or next;
245 $memberhash{$key} = join " ", @{$anon->{vals}};
247 foreach my $key (keys %mapping) {
248 my $data = $memberhash{$mapping{$key}->{is}};
249 unless (defined $data) {
250 $data = $mapping{$key}->{content} || ''; # default or failsafe ''
252 $borrower{$key} = ($data ne '') ? $data : ' ' ;
254 $borrower{initials} = $memberhash{initials} ||
255 ( substr($borrower{'firstname'},0,1)
256 . substr($borrower{ 'surname' },0,1)
257 . " ");
258 return %borrower;
261 sub exists_local($) {
262 my $sth = C4::Context->dbh->prepare("SELECT password from borrowers WHERE cardnumber=?");
263 $sth->execute(shift);
264 return ($sth->rows) ? 1 : 0 ;
267 sub update_local($$%) {
268 # warn "MODIFY borrower";
269 my $userid = shift or return undef;
270 my $digest = md5_base64(shift) or return undef;
271 my %borrower = shift or return undef;
272 my $dbh = C4::Context->dbh;
273 my $sth = $dbh->prepare("
274 UPDATE borrowers
275 SET firstname=?,surname=?,initials=?,address=?,city=?,phone=?, categorycode=?,branchcode=?,email=?,sort1=?
276 WHERE cardnumber=?
278 $sth->execute(
279 $borrower{firstname}, $borrower{surname},
280 $borrower{initials}, $borrower{address},
281 $borrower{city}, $borrower{phone},
282 $borrower{categorycode}, $borrower{branchcode},
283 $borrower{email}, $borrower{sort1},
284 $userid
287 # MODIFY PASSWORD/LOGIN
288 # search borrowerid
289 $sth = $dbh->prepare("SELECT borrowernumber from borrowers WHERE cardnumber=? ");
290 $sth->execute($userid);
291 my ($borrowerid) = $sth->fetchrow;
292 # warn "change local password for $borrowerid setting $password";
293 changepassword($userid, $borrowerid, $digest);
295 # Confirm changes
296 my $cardnumber;
297 $sth = $dbh->prepare("SELECT password,cardnumber from borrowers WHERE userid=? ");
298 $cardnumber = confirmer($sth,$userid,$digest) and return $cardnumber;
299 $sth = $dbh->prepare("SELECT password,cardnumber from borrowers WHERE cardnumber=? ");
300 $cardnumber = confirmer($sth,$userid,$digest) and return $cardnumber;
301 die "Unexpected error after password update to $userid / $cardnumber.";
304 sub confirmer($$$) {
305 my $sth = shift or return undef;
306 my $userid = shift or return undef;
307 my $digest = shift or return undef;
308 $sth->execute($userid);
309 if ($sth->rows) {
310 my ($md5password, $othernum) = $sth->fetchrow;
311 ($digest eq $md5password) and return $othernum;
312 warn "Password mismatch after update to userid=$userid";
313 return undef;
315 warn "Could not recover record after updating password for userid=$userid";
316 return 0;
319 __END__
321 =back
323 =head1 SEE ALSO
325 CGI(3)
327 Net::LDAP()
329 Digest::MD5(3)
331 =cut