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
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
21 use Digest
::MD5
qw(md5_base64);
24 use C4
::Members
qw(AddMember changepassword);
25 use C4
::Utils
qw( :all );
27 use Net
::LDAP
::Filter
;
29 use vars
qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $debug);
33 $VERSION = 3.02; # set the version for version checking
34 $debug = $ENV{DEBUG} || 0;
36 @EXPORT = qw( checkpw_ldap );
39 # Redefine checkpw_ldap:
40 # connect to LDAP (named or anonymous)
41 # ~ retrieves $userid from KOHA_CONF mapping
42 # ~ then compares $password with userPassword
43 # ~ then gets the LDAP entry
44 # ~ and calls the memberadd if necessary
46 sub ldapserver_error
($) {
47 return sprintf('No ldapserver "%s" defined in KOHA_CONF: ' . $ENV{KOHA_CONF
}, shift);
50 use vars
qw($mapping @ldaphosts $base $ldapname $ldappassword);
51 my $context = C4::Context->new() or die 'C4::Context->new failed';
52 my $ldap = $context->{server}->{ldapserver} or die 'No "ldapserver" in server hash from KOHA_CONF: ' . $ENV{KOHA_CONF};
53 my $prefhost = $ldap->{hostname} or die ldapserver_error('hostname');
54 my $base = $ldap->{base} or die ldapserver_error('base');
55 $ldapname = $ldap->{user} or die ldapserver_error('user');
56 $ldappassword = $ldap->{pass} or die ldapserver_error('pass');
57 our %mapping = %{$ldap->{mapping}} or die ldapserver_error('mapping');
58 my @mapkeys = keys %mapping;
59 $debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys ( total ): ", join ' ', @mapkeys, "\n";
60 @mapkeys = grep {defined $mapping{$_}->{is}} @mapkeys;
61 $debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys (populated): ", join ' ', @mapkeys, "\n";
64 anonymous => ($ldapname and $ldappassword) ? 0 : 1,
65 replicate => $ldap->{replicate} || 1, # add from LDAP to Koha database for new user
66 update => $ldap->{update} || 1, # update from LDAP to Koha database for existing user
70 my $result = shift or return undef;
71 return "LDAP error #" . $result->code
72 . ": " . $result->error_name . "\n"
73 . "# " . $result->error_text . "\n";
77 my ($dbh, $userid, $password) = @_;
78 my $db = Net::LDAP->new([$prefhost]);
79 #$debug and $db->debug(5);
80 my $uid_field = $mapping{userid}->{is} or die ldapserver_error("mapping for 'userid'");
81 my $filter = Net::LDAP::Filter->new("$uid_field=$userid") or die "Failed to create new Net::LDAP::Filter";
82 my $res = ($config{anonymous}) ? $db->bind : $db->bind($ldapname, password=>$ldappassword);
83 if ($res->code) { # connection refused
84 warn "LDAP bind failed as $ldapname: " . description($res);
87 my $search = $db->search(
91 ) or die "LDAP search failed to return object.";
92 my $count = $search->count;
93 if ($search->code > 0) {
94 warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count) . description($search);
98 warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count);
102 my $userldapentry = $search->shift_entry;
103 my $cmpmesg = $db->compare( $userldapentry, attr=>'userpassword', value => $password );
104 if ($cmpmesg->code != 6) {
105 warn "LDAP Auth rejected : invalid password for user '$userid'. " . description($cmpmesg);
108 unless ($config{update} or $config{replicate}) {
111 my %borrower = ldap_entry_2_hash($userldapentry,$userid);
112 $debug and print "checkpw_ldap received \%borrower w/ " . keys(%borrower), " keys: ", join(' ', keys %borrower), "\n";
113 my ($borrowernumber,$cardnumber,$savedpw);
114 ($borrowernumber,$cardnumber,$userid,$savedpw) = exists_local($userid);
115 if ($borrowernumber) {
116 ($config{update} ) and my $c2 = &update_local($userid,$password,$borrowernumber,\%borrower) || '';
117 ($cardnumber eq $c2) or warn "update_local returned cardnumber '$c2' instead of '$cardnumber'";
119 ($config{replicate}) and $borrowernumber = AddMember(%borrower);
121 return(1, $cardnumber);
124 # Pass LDAP entry object and local cardnumber (userid).
125 # Returns borrower hash.
126 # Edit KOHA_CONF so $memberhash{'xxx'} fits your ldap structure.
127 # Ensure that mandatory fields are correctly filled!
129 sub ldap_entry_2_hash ($$) {
130 my $userldapentry = shift;
131 my %borrower = ( cardnumber => shift );
133 $userldapentry->exists('uid'); # This is bad, but required! By side-effect, this initializes the attrs hash.
135 print "\nkeys(\%\$userldapentry) = " . join(', ', keys %$userldapentry), "\n", $userldapentry->dump();
136 foreach (keys %$userldapentry) {
137 print "\n\nLDAP key: $_\t", sprintf('(%s)', ref $userldapentry->{$_}), "\n";
138 hashdump("LDAP key: ",$userldapentry->{$_});
141 my $x = $userldapentry->{attrs} or return undef;
144 $memberhash{$_} = join ' ', @{$x->{$_}};
145 $debug and print sprintf("building \$memberhash{%s} = ", $_, join(' ', @{$x->{$_}})), "\n";
147 $debug and print "Finsihed \%memberhash has ", scalar(keys %memberhash), " keys\n",
148 "Referencing \%mapping with ", scalar(keys %mapping), " keys\n";
149 foreach my $key (keys %mapping) {
150 my $data = $memberhash{$mapping{$key}->{is}};
151 $debug and printf "mapping %20s ==> %-20s (%s)\n", $key, $mapping{$key}->{is}, $data;
152 unless (defined $data) {
153 $data = $mapping{$key}->{content} || ''; # default or failsafe ''
155 $borrower{$key} = ($data ne '') ? $data : ' ' ;
157 $borrower{initials} = $memberhash{initials} ||
158 ( substr($borrower{'firstname'},0,1)
159 . substr($borrower{ 'surname' },0,1)
164 sub exists_local($) {
166 my $dbh = C4::Context->dbh;
167 my $select = "SELECT borrowernumber,cardnumber,userid,password FROM borrowers ";
169 my $sth = $dbh->prepare("$select WHERE userid=?"); # was cardnumber=?
171 $debug and printf "Userid '$arg' exists_local? %s\n", $sth->rows;
172 ($sth->rows == 1) and return $sth->fetchrow;
174 $sth = $dbh->prepare("$select WHERE cardnumber=?");
176 $debug and printf "Cardnumber '$arg' exists_local? %s\n", $sth->rows;
177 ($sth->rows == 1) and return $sth->fetchrow;
181 sub update_local($$$$) {
182 my $userid = shift or return undef;
183 my $digest = md5_base64(shift) or return undef;
184 my $borrowerid = shift or return undef;
185 my $borrower = shift or return undef;
186 my @keys = keys %$borrower;
187 my $dbh = C4::Context->dbh;
188 my $query = "UPDATE borrowers\nSET " .
189 join(',', map {"$_=?"} @keys) .
190 "\nWHERE borrowernumber=? ";
191 my $sth = $dbh->prepare($query);
193 print STDERR $query, "\n",
194 join "\n", map {"$_ = '" . $borrower->{$_} . "'"} @keys;
195 print STDERR "\nuserid = $userid\n";
198 ((map {$borrower->{$_}} @keys), $borrowerid)
201 # MODIFY PASSWORD/LOGIN
203 $debug and print "changing local password for borrowernumber=$borrowerid to '$digest'\n";
204 changepassword($userid, $borrowerid, $digest);
207 $sth = $dbh->prepare("SELECT password,cardnumber FROM borrowers WHERE borrowernumber=? ");
208 $sth->execute($borrowerid);
210 my ($md5password, $cardnum) = $sth->fetchrow;
211 ($digest eq $md5password) and return $cardnum;
212 warn "Password mismatch after update to cardnumber=$cardnum (borrowernumber=$borrowerid)";
215 die "Unexpected error after password update to userid/borrowernumber: $userid / $borrowerid.";
223 C4::Auth - Authenticates Koha users
227 use C4::Auth_with_ldap;
229 =head1 LDAP Configuration
231 This module is specific to LDAP authentification. It requires Net::LDAP package and one or more
232 working LDAP servers.
234 * Modify ldapserver element in KOHA_CONF
235 * Establish field mapping in <mapping> element.
237 For example, if your user records are stored according to the inetOrgPerson schema, RFC#2798,
238 the username would match the "uid" field, and the password should match the "userpassword" field.
240 Make sure that ALL required fields are populated by your LDAP database (and mapped in KOHA_CONF).
241 What are the required fields? Well, in mysql you can check the database table "borrowers" like this:
243 mysql> show COLUMNS from borrowers;
244 +------------------+--------------+------+-----+---------+----------------+
245 | Field | Type | Null | Key | Default | Extra |
246 +------------------+--------------+------+-----+---------+----------------+
247 | borrowernumber | int(11) | NO | PRI | NULL | auto_increment |
248 | cardnumber | varchar(16) | YES | UNI | NULL | |
249 | surname | mediumtext | NO | | | |
250 | firstname | text | YES | | NULL | |
251 | title | mediumtext | YES | | NULL | |
252 | othernames | mediumtext | YES | | NULL | |
253 | initials | text | YES | | NULL | |
254 | streetnumber | varchar(10) | YES | | NULL | |
255 | streettype | varchar(50) | YES | | NULL | |
256 | address | mediumtext | NO | | | |
257 | address2 | text | YES | | NULL | |
258 | city | mediumtext | NO | | | |
259 | zipcode | varchar(25) | YES | | NULL | |
260 | email | mediumtext | YES | | NULL | |
261 | phone | text | YES | | NULL | |
262 | mobile | varchar(50) | YES | | NULL | |
263 | fax | mediumtext | YES | | NULL | |
264 | emailpro | text | YES | | NULL | |
265 | phonepro | text | YES | | NULL | |
266 | B_streetnumber | varchar(10) | YES | | NULL | |
267 | B_streettype | varchar(50) | YES | | NULL | |
268 | B_address | varchar(100) | YES | | NULL | |
269 | B_city | mediumtext | YES | | NULL | |
270 | B_zipcode | varchar(25) | YES | | NULL | |
271 | B_email | text | YES | | NULL | |
272 | B_phone | mediumtext | YES | | NULL | |
273 | dateofbirth | date | YES | | NULL | |
274 | branchcode | varchar(10) | NO | MUL | | |
275 | categorycode | varchar(10) | NO | MUL | | |
276 | dateenrolled | date | YES | | NULL | |
277 | dateexpiry | date | YES | | NULL | |
278 | gonenoaddress | tinyint(1) | YES | | NULL | |
279 | lost | tinyint(1) | YES | | NULL | |
280 | debarred | tinyint(1) | YES | | NULL | |
281 | contactname | mediumtext | YES | | NULL | |
282 | contactfirstname | text | YES | | NULL | |
283 | contacttitle | text | YES | | NULL | |
284 | guarantorid | int(11) | YES | | NULL | |
285 | borrowernotes | mediumtext | YES | | NULL | |
286 | relationship | varchar(100) | YES | | NULL | |
287 | ethnicity | varchar(50) | YES | | NULL | |
288 | ethnotes | varchar(255) | YES | | NULL | |
289 | sex | varchar(1) | YES | | NULL | |
290 | password | varchar(30) | YES | | NULL | |
291 | flags | int(11) | YES | | NULL | |
292 | userid | varchar(30) | YES | MUL | NULL | | # UNIQUE in next release.
293 | opacnote | mediumtext | YES | | NULL | |
294 | contactnote | varchar(255) | YES | | NULL | |
295 | sort1 | varchar(80) | YES | | NULL | |
296 | sort2 | varchar(80) | YES | | NULL | |
297 +------------------+--------------+------+-----+---------+----------------+
298 50 rows in set (0.01 sec)
300 Where Null="NO", the field is required.
304 =head1 KOHA_CONF and field mapping
306 Example XML stanza for LDAP configuration in KOHA_CONF:
308 <!-- LDAP SERVER (optional) -->
309 <server id="ldapserver" listenref="ldapserver">
310 <hostname>localhost</hostname>
311 <base>dc=metavore,dc=com</base>
312 <user>cn=Manager,dc=metavore,dc=com</user> <!-- DN, if not anonymous -->
313 <pass>metavore</pass> <!-- password, if not anonymous -->
314 <replicate>1</replicate> <!-- add new users from LDAP to Koha database -->
315 <update>1</update> <!-- update existing users in Koha database -->
316 <mapping> <!-- match koha SQL field names to your LDAP record field names -->
317 <firstname is="givenname" ></firstname>
318 <surname is="sn" ></surname>
319 <address is="postaladdress" ></address>
320 <city is="l" >Athens, OH</city>
321 <zipcode is="postalcode" ></zipcode>
322 <branchcode is="branch" >MAIN</branchcode>
323 <userid is="uid" ></userid>
324 <password is="userpassword" ></password>
325 <email is="mail" ></email>
326 <categorycode is="employeetype" >PT</categorycode>
327 <phone is="telephonenumber"></phone>
331 The <mapping> subelements establish the relationship between mysql fields and LDAP attributes. The element name
332 is the column in mysql, with the "is" characteristic set to the LDAP attribute name. Optionally, any content
333 between the element tags is taken as the default value. In this example, the default categorycode is "PT" (for
338 # ========================================
339 # Using attrs instead of {asn}->attributes
340 # ========================================
342 # LDAP key: ->{ cn} = ARRAY w/ 3 members.
343 # LDAP key: ->{ cn}->{ sss} = sss
344 # LDAP key: ->{ cn}->{ Steve Smith} = Steve Smith
345 # LDAP key: ->{ cn}->{Steve S. Smith} = Steve S. Smith
347 # LDAP key: ->{ givenname} = ARRAY w/ 1 members.
348 # LDAP key: ->{ givenname}->{Steve} = Steve