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 );
27 use Net::LDAP::Filter;
28 # use Net::LDAP qw(:all);
30 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
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 );
42 C4::Auth - Authenticates Koha users
46 use C4::Auth_with_ldap;
50 This module is specific to LDAP authentification. It requires Net::LDAP package and one or more
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.
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.
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:
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)
120 Then %mappings establishes the relationship between mysql field and LDAP attribute.
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
131 use vars qw(%mapping @ldaphosts $base $ldapname $ldappassword);
134 firstname => 'givenName',
136 address => 'postalAddress',
138 zipcode => 'postalCode',
139 branchcode => 'branch',
140 emailaddress => 'mail',
141 categorycode => 'employeeType',
142 phone => 'telephoneNumber',
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
151 (@ldaphosts) = (qw(localhost)); # Potentially multiple LDAP hosts!
152 $base = "dc=metavore,dc=com"; # But only 1 base.
155 $ldapname = "cn=Manager,$base"; # Your LDAP user. EDIT THIS LINE.
156 $ldappassword = 'metavore'; # Your LDAP user's password. EDIT THIS LINE.
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
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";
172 my ($dbh, $userid, $password) = @_;
173 if ( $userid eq C4::Context->config('user')
174 && $password eq C4::Context->config('pass') )
176 return 2; # Koha superuser account
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);
186 my $search = $db->search(
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);
197 warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count);
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);
207 unless($config{update} or $config{replicate}) {
210 my %borrower = ldap_entry_2_hash($userldapentry,$userid);
211 if (exists_local($userid)) {
212 ($config{update} ) and &update_local($userid,$password,%borrower);
214 ($config{replicate}) and AddMember(%borrower);
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!
224 sub ldap_entry_2_hash ($$) {
225 my $userldapentry = shift;
226 my %borrower = ( cardnumber => shift );
228 my $x = $userldapentry->{asn}{attributes} or return undef;
230 foreach my $k (@$x) {
231 foreach my $k2 ( keys %$k ) {
235 $memberhash{$key} .= map {$_ . " "} @$k{$k2};
239 foreach my $key (%mapping) {
240 my $data = $memberhash{$mapping{$key}};
241 defined $data or $data = ' ';
242 $borrower{$key} = ($data ne '') ? $data : ' ' ;
244 $borrower{initials} = $memberhash{initials} ||
245 ( substr($borrower{'firstname'},0,1)
246 . substr($borrower{ 'surname' },0,1)
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 ;
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("
265 SET firstname=?,surname=?,initials=?,streetaddress=?,city=?,phone=?, categorycode=?,branchcode=?,emailaddress=?,sort1=?
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},
277 # MODIFY PASSWORD/LOGIN
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);
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.";
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);
300 my ($md5password, $othernum) = $sth->fetchrow;
301 ($digest eq $md5password) and return $othernum;
302 warn "Password mismatch after update to userid=$userid";
305 warn "Could not recover record after updating password for userid=$userid";