X-Git-Url: http://git.rot13.org/?a=blobdiff_plain;f=C4%2FAuth_with_ldap.pm;h=03e81e1c67709bc0b47a661e3b5d317e7300c396;hb=e1775fe1d5ffae3411d64c4d69fca75ad5641293;hp=6ad17f756f26e6ab3eb8e3dd7128cb3b1b1cf956;hpb=c78608483e113c9a66fc8ae90459628b9dae5f44;p=koha.git diff --git a/C4/Auth_with_ldap.pm b/C4/Auth_with_ldap.pm index 6ad17f756f..03e81e1c67 100644 --- a/C4/Auth_with_ldap.pm +++ b/C4/Auth_with_ldap.pm @@ -4,38 +4,40 @@ package C4::Auth_with_ldap; # # This file is part of Koha. # -# Koha is free software; you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software -# Foundation; either version 2 of the License, or (at your option) any later -# version. +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. # -# Koha is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. # -# You should have received a copy of the GNU General Public License along -# with Koha; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . use strict; #use warnings; FIXME - Bug 2505 -use Digest::MD5 qw(md5_base64); +use Carp; use C4::Debug; use C4::Context; -use C4::Members qw(AddMember changepassword); +use C4::Members qw(AddMember); use C4::Members::Attributes; use C4::Members::AttributeTypes; -use C4::Utils qw( :all ); +use C4::Members::Messaging; +use C4::Auth qw(checkpw_internal); +use Koha::Patrons; +use Koha::AuthUtils qw(hash_password); use List::MoreUtils qw( any ); use Net::LDAP; use Net::LDAP::Filter; -use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $debug); +use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $debug); BEGIN { require Exporter; - $VERSION = 3.10; # set the version for version checking @ISA = qw(Exporter); @EXPORT = qw( checkpw_ldap ); } @@ -47,7 +49,7 @@ BEGIN { # ~ then gets the LDAP entry # ~ and calls the memberadd if necessary -sub ldapserver_error ($) { +sub ldapserver_error { return sprintf('No ldapserver "%s" defined in KOHA_CONF: ' . $ENV{KOHA_CONF}, shift); } @@ -64,14 +66,23 @@ $debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys ( total ): ", @mapkeys = grep {defined $mapping{$_}->{is}} @mapkeys; $debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys (populated): ", join ' ', @mapkeys, "\n"; +my %categorycode_conversions; +my $default_categorycode; +if(defined $ldap->{categorycode_mapping}) { + $default_categorycode = $ldap->{categorycode_mapping}->{default}; + foreach my $cat (@{$ldap->{categorycode_mapping}->{categorycode}}) { + $categorycode_conversions{$cat->{value}} = $cat->{content}; + } +} + my %config = ( anonymous => ($ldapname and $ldappassword) ? 0 : 1, replicate => defined($ldap->{replicate}) ? $ldap->{replicate} : 1, # add from LDAP to Koha database for new user update => defined($ldap->{update} ) ? $ldap->{update} : 1, # update from LDAP to Koha database for existing user ); -sub description ($) { - my $result = shift or return undef; +sub description { + my $result = shift or return; return "LDAP error #" . $result->code . ": " . $result->error_name . "\n" . "# " . $result->error_text . "\n"; @@ -86,7 +97,9 @@ sub search_method { base => $base, filter => $filter, # attrs => ['*'], - ) or die "LDAP search failed to return object."; + ); + die "LDAP search failed to return object : " . $search->error if $search->code; + my $count = $search->count; if ($search->code > 0) { warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count) . description($search); @@ -103,32 +116,65 @@ sub checkpw_ldap { my ($dbh, $userid, $password) = @_; my @hosts = split(',', $prefhost); my $db = Net::LDAP->new(\@hosts); + unless ( $db ) { + warn "LDAP connexion failed"; + return 0; + } + #$debug and $db->debug(5); my $userldapentry; - if ( $ldap->{auth_by_bind} ) { - my $principal_name = $ldap->{principal_name}; - if ($principal_name and $principal_name =~ /\%/) { - $principal_name = sprintf($principal_name,$userid); - } else { - $principal_name = $userid; + + if ( $ldap->{auth_by_bind} ) { + my $principal_name; + if ( $ldap->{anonymous_bind} ) { + + # Perform an anonymous bind + my $res = $db->bind; + if ( $res->code ) { + warn "Anonymous LDAP bind failed: " . description($res); + return 0; + } + + # Perform a LDAP search for the given username + my $search = search_method( $db, $userid ) + or return 0; # warnings are in the sub + $userldapentry = $search->shift_entry; + $principal_name = $userldapentry->dn; } - my $res = $db->bind( $principal_name, password => $password ); - if ( $res->code ) { - $debug and warn "LDAP bind failed as kohauser $principal_name: ". description($res); - return 0; + else { + $principal_name = $ldap->{principal_name}; + if ( $principal_name and $principal_name =~ /\%/ ) { + $principal_name = sprintf( $principal_name, $userid ); + } + else { + $principal_name = $userid; + } } - # FIXME dpavlin -- we really need $userldapentry leater on even if using auth_by_bind! - - # BUG #5094 - # 2010-08-04 JeremyC - # a $userldapentry is only needed if either updating or replicating are enabled - if($config{update} or $config{replicate}) { - my $search = search_method($db, $userid) or return 0; # warnings are in the sub - $userldapentry = $search->shift_entry; - } - - } else { + # Perform a LDAP bind for the given username using the matched DN + my $res = $db->bind( $principal_name, password => $password ); + if ( $res->code ) { + if ( $ldap->{anonymous_bind} ) { + # With anonymous_bind approach we can be sure we have found the correct user + # and that any 'code' response indicates a 'bad' user (be that blocked, banned + # or password changed). We should not fall back to local accounts in this case. + warn "LDAP bind failed as kohauser $userid: " . description($res); + return -1; + } else { + # Without a anonymous_bind, we cannot be sure we are looking at a valid ldap user + # at all, and thus we should fall back to local logins to restore previous behaviour + # see bug 12831 + warn "LDAP bind failed as kohauser $userid: " . description($res); + return 0; + } + } + if ( !defined($userldapentry) + && ( $config{update} or $config{replicate} ) ) + { + my $search = search_method( $db, $userid ) or return 0; + $userldapentry = $search->shift_entry; + } + } else { my $res = ($config{anonymous}) ? $db->bind : $db->bind($ldapname, password=>$ldappassword); if ($res->code) { # connection refused warn "LDAP bind failed as ldapuser " . ($ldapname || '[ANONYMOUS]') . ": " . description($res); @@ -139,7 +185,7 @@ sub checkpw_ldap { my $cmpmesg = $db->compare( $userldapentry, attr=>'userpassword', value => $password ); if ($cmpmesg->code != 6) { warn "LDAP Auth rejected : invalid password for user '$userid'. " . description($cmpmesg); - return 0; + return -1; } } @@ -161,34 +207,28 @@ sub checkpw_ldap { ($cardnumber eq $c2) or warn "update_local returned cardnumber '$c2' instead of '$cardnumber'"; } else { # C1, D1 # maybe update just the password? - return(1, $cardnumber); # FIXME dpavlin -- don't destroy ExtendedPatronAttributes + return(1, $cardnumber, $local_userid); } } elsif ($config{replicate}) { # A2, C2 $borrowernumber = AddMember(%borrower) or die "AddMember failed"; + C4::Members::Messaging::SetMessagingPreferencesFromDefaults( { borrowernumber => $borrowernumber, categorycode => $borrower{'categorycode'} } ); } else { return 0; # B2, D2 } - if (C4::Context->preference('ExtendedPatronAttributes') && $borrowernumber && ($config{update} ||$config{replicate})) { - my @types = C4::Members::AttributeTypes::GetAttributeTypes(); - my @attributes = grep{my $key=$_; any{$_ eq $key}@types;} keys %borrower; - my $extended_patron_attributes = map{{code=>$_,value=>$borrower{$_}}}@attributes; - my $extended_patron_attributes = [] unless $extended_patron_attributes; - my @errors; - #Check before add - for (my $i; $i< scalar(@$extended_patron_attributes)-1;$i++) { - my $attr=$extended_patron_attributes->[$i]; - unless (C4::Members::Attributes::CheckUniqueness($attr->{code}, $attr->{value}, $borrowernumber)) { - unshift @errors, $i; - warn "ERROR_extended_unique_id_failed $attr->{code} $attr->{value}"; - } - } - #Removing erroneous attributes - foreach my $index (@errors){ - @$extended_patron_attributes=splice(@$extended_patron_attributes,$index,1); - } - C4::Members::Attributes::SetBorrowerAttributes($borrowernumber, $extended_patron_attributes); - } -return(1, $cardnumber); + if (C4::Context->preference('ExtendedPatronAttributes') && $borrowernumber && ($config{update} ||$config{replicate})) { + foreach my $attribute_type ( C4::Members::AttributeTypes::GetAttributeTypes() ) { + my $code = $attribute_type->{code}; + unless (exists($borrower{$code}) && $borrower{$code} !~ m/^\s*$/ ) { + next; + } + if (C4::Members::Attributes::CheckUniqueness($code, $borrower{$code}, $borrowernumber)) { + C4::Members::Attributes::UpdateBorrowerAttribute($borrowernumber, {code => $code, value => $borrower{$code}}); + } else { + warn "ERROR_extended_unique_id_failed $code $borrower{$code}"; + } + } + } + return(1, $cardnumber, $userid); } # Pass LDAP entry object and local cardnumber (userid). @@ -196,19 +236,17 @@ return(1, $cardnumber); # Edit KOHA_CONF so $memberhash{'xxx'} fits your ldap structure. # Ensure that mandatory fields are correctly filled! # -sub ldap_entry_2_hash ($$) { +sub ldap_entry_2_hash { my $userldapentry = shift; my %borrower = ( cardnumber => shift ); my %memberhash; $userldapentry->exists('uid'); # This is bad, but required! By side-effect, this initializes the attrs hash. if ($debug) { - print STDERR "\nkeys(\%\$userldapentry) = " . join(', ', keys %$userldapentry), "\n", $userldapentry->dump(); foreach (keys %$userldapentry) { print STDERR "\n\nLDAP key: $_\t", sprintf('(%s)', ref $userldapentry->{$_}), "\n"; - hashdump("LDAP key: ",$userldapentry->{$_}); } } - my $x = $userldapentry->{attrs} or return undef; + my $x = $userldapentry->{attrs} or return; foreach (keys %$x) { $memberhash{$_} = join ' ', @{$x->{$_}}; $debug and print STDERR sprintf("building \$memberhash{%s} = ", $_, join(' ', @{$x->{$_}})), "\n"; @@ -216,7 +254,7 @@ sub ldap_entry_2_hash ($$) { $debug and print STDERR "Finsihed \%memberhash has ", scalar(keys %memberhash), " keys\n", "Referencing \%mapping with ", scalar(keys %mapping), " keys\n"; foreach my $key (keys %mapping) { - my $data = $memberhash{$mapping{$key}->{is}}; + my $data = $memberhash{ lc($mapping{$key}->{is}) }; # Net::LDAP returns all names in lowercase $debug and printf STDERR "mapping %20s ==> %-20s (%s)\n", $key, $mapping{$key}->{is}, $data; unless (defined $data) { $data = $mapping{$key}->{content} || ''; # default or failsafe '' @@ -227,10 +265,29 @@ sub ldap_entry_2_hash ($$) { ( substr($borrower{'firstname'},0,1) . substr($borrower{ 'surname' },0,1) . " "); + + # categorycode conversions + if(defined $categorycode_conversions{$borrower{categorycode}}) { + $borrower{categorycode} = $categorycode_conversions{$borrower{categorycode}}; + } + elsif($default_categorycode) { + $borrower{categorycode} = $default_categorycode; + } + + # check if categorycode exists, if not, fallback to default from koha-conf.xml + my $dbh = C4::Context->dbh; + my $sth = $dbh->prepare("SELECT categorycode FROM categories WHERE categorycode = ?"); + $sth->execute( uc($borrower{'categorycode'}) ); + unless ( my $row = $sth->fetchrow_hashref ) { + my $default = $mapping{'categorycode'}->{content}; + $debug && warn "Can't find ", $borrower{'categorycode'}, " default to: $default for ", $borrower{userid}; + $borrower{'categorycode'} = $default + } + return %borrower; } -sub exists_local($) { +sub exists_local { my $arg = shift; my $dbh = C4::Context->dbh; my $select = "SELECT borrowernumber,cardnumber,userid,password FROM borrowers "; @@ -247,45 +304,69 @@ sub exists_local($) { return 0; } +# This function performs a password update, given the userid, borrowerid, +# and digested password. It will verify that things are correct and return the +# borrowers cardnumber. The idea is that it is used to keep the local +# passwords in sync with the LDAP passwords. +# +# $cardnum = _do_changepassword($userid, $borrowerid, $digest) +# +# Note: if the LDAP config has the update_password tag set to a false value, +# then this will not update the password, it will simply return the cardnumber. sub _do_changepassword { - my ($userid, $borrowerid, $digest) = @_; + my ($userid, $borrowerid, $password) = @_; + + if ( exists( $ldap->{update_password} ) && !$ldap->{update_password} ) { + + # We don't store the password in the database + my $sth = C4::Context->dbh->prepare( + 'SELECT cardnumber FROM borrowers WHERE borrowernumber=?'); + $sth->execute($borrowerid); + die "Unable to access borrowernumber " + . "with userid=$userid, " + . "borrowernumber=$borrowerid" + if !$sth->rows; + my ($cardnum) = $sth->fetchrow; + $sth = C4::Context->dbh->prepare( + 'UPDATE borrowers SET password = null WHERE borrowernumber=?'); + $sth->execute($borrowerid); + return $cardnum; + } + my $digest = hash_password($password); + $debug and print STDERR "changing local password for borrowernumber=$borrowerid to '$digest'\n"; - changepassword($userid, $borrowerid, $digest); - - # Confirm changes - my $sth = C4::Context->dbh->prepare("SELECT password,cardnumber FROM borrowers WHERE borrowernumber=? "); - $sth->execute($borrowerid); - if ($sth->rows) { - my ($md5password, $cardnum) = $sth->fetchrow; - ($digest eq $md5password) and return $cardnum; - warn "Password mismatch after update to cardnumber=$cardnum (borrowernumber=$borrowerid)"; - return undef; - } - die "Unexpected error after password update to userid/borrowernumber: $userid / $borrowerid."; + Koha::Patrons->find($borrowerid)->update_password( $userid, $digest ); + + my ($ok, $cardnum) = checkpw_internal(C4::Context->dbh, $userid, $password); + return $cardnum if $ok; + + warn "Password mismatch after update to borrowernumber=$borrowerid"; + return; } -sub update_local($$$$) { - my $userid = shift or return undef; - my $digest = md5_base64(shift) or return undef; - my $borrowerid = shift or return undef; - my $borrower = shift or return undef; - my @keys = keys %$borrower; - my $dbh = C4::Context->dbh; - my $query = "UPDATE borrowers\nSET " . - join(',', map {"$_=?"} @keys) . - "\nWHERE borrowernumber=? "; - my $sth = $dbh->prepare($query); - if ($debug) { - print STDERR $query, "\n", - join "\n", map {"$_ = '" . $borrower->{$_} . "'"} @keys; - print STDERR "\nuserid = $userid\n"; - } - $sth->execute( - ((map {$borrower->{$_}} @keys), $borrowerid) - ); +sub update_local { + my $userid = shift or croak "No userid"; + my $password = shift or croak "No password"; + my $borrowerid = shift or croak "No borrowerid"; + my $borrower = shift or croak "No borrower record"; + + my @keys = keys %$borrower; + my $dbh = C4::Context->dbh; + my $query = "UPDATE borrowers\nSET " . + join(',', map {"$_=?"} @keys) . + "\nWHERE borrowernumber=? "; + my $sth = $dbh->prepare($query); + if ($debug) { + print STDERR $query, "\n", + join "\n", map {"$_ = '" . $borrower->{$_} . "'"} @keys; + print STDERR "\nuserid = $userid\n"; + } + $sth->execute( + ((map {$borrower->{$_}} @keys), $borrowerid) + ); - # MODIFY PASSWORD/LOGIN - _do_changepassword($userid, $borrowerid, $digest); + # MODIFY PASSWORD/LOGIN if password was mapped + _do_changepassword($userid, $borrowerid, $password) if $borrower->{'password'}; } 1; @@ -355,7 +436,8 @@ C4::Auth - Authenticates Koha users | dateexpiry | date | YES | | NULL | | | gonenoaddress | tinyint(1) | YES | | NULL | | | lost | tinyint(1) | YES | | NULL | | - | debarred | tinyint(1) | YES | | NULL | | + | debarred | date | YES | | NULL | | + | debarredcomment | varchar(255) | YES | | NULL | | | contactname | mediumtext | YES | | NULL | | | contactfirstname | text | YES | | NULL | | | contacttitle | text | YES | | NULL | | @@ -404,8 +486,13 @@ Example XML stanza for LDAP configuration in KOHA_CONF. 1 0 + 0 %s@my_domain.com - + + 1 @@ -463,6 +550,14 @@ attribute that the server allows to be used for binding could be used. Currently, principal_name only operates when auth_by_bind is enabled. +=head2 update_password + +If this tag is left out or set to a true value, then the user's LDAP password +will be stored (hashed) in the local Koha database. If you don't want this +to happen, then set the value of this to '0'. Note that if passwords are not +stored locally, and the connection to the LDAP system fails, then the users +will not be able to log in at all. + =head2 Active Directory The auth_by_bind and principal_name settings are recommended for Active Directory.