From a462c8939e0e17cb0eada85513292b60c9230b5c Mon Sep 17 00:00:00 2001 From: Joe Atzberger Date: Thu, 29 Nov 2007 17:43:12 -0600 Subject: [PATCH] LDAP - further integration Signed-off-by: Chris Cormack Signed-off-by: Joshua Ferraro --- C4/Auth_with_ldap.pm | 366 +++++++++++++++++++++++-------------------- t/Auth_with_ldap.t | 14 +- 2 files changed, 206 insertions(+), 174 deletions(-) diff --git a/C4/Auth_with_ldap.pm b/C4/Auth_with_ldap.pm index 42f1c71d80..2798da33d6 100644 --- a/C4/Auth_with_ldap.pm +++ b/C4/Auth_with_ldap.pm @@ -33,94 +33,10 @@ BEGIN { $VERSION = 3.01; # set the version for version checking $debug = $ENV{DEBUG} || 0; @ISA = qw(Exporter C4::Auth); - @EXPORT = qw( checkauth ); + @EXPORT = qw( checkpw ); } -=head1 NAME - -C4::Auth - Authenticates Koha users - -=head1 SYNOPSIS - - use C4::Auth_with_ldap; - -=head1 LDAP specific - - This module is specific to LDAP authentification. It requires Net::LDAP package and one or more - working LDAP servers. - To use it : - * Modify ldapserver and ldapinfos via web "Preferences". - * Modify the values (right side) of %mapping pairs, to match your LDAP fields. - * Modify $ldapname and $ldappassword, if required. - - It is assumed your user records are stored according to the inetOrgPerson schema, RFC#2798. - Thus the username must match the "uid" field, and the password must match the "userPassword" field. - - Make sure that the required fields are populated in your LDAP database. What are they? Well, in - mysql you can check the database table "borrowers" like this: - - mysql> show COLUMNS from borrowers; - +------------------+--------------+------+-----+---------+----------------+ - | Field | Type | Null | Key | Default | Extra | - +------------------+--------------+------+-----+---------+----------------+ - | borrowernumber | int(11) | NO | PRI | NULL | auto_increment | - | cardnumber | varchar(16) | YES | UNI | NULL | | - | surname | mediumtext | NO | | | | - | firstname | text | YES | | NULL | | - | title | mediumtext | YES | | NULL | | - | othernames | mediumtext | YES | | NULL | | - | initials | text | YES | | NULL | | - | streetnumber | varchar(10) | YES | | NULL | | - | streettype | varchar(50) | YES | | NULL | | - | address | mediumtext | NO | | | | - | address2 | text | YES | | NULL | | - | city | mediumtext | NO | | | | - | zipcode | varchar(25) | YES | | NULL | | - | email | mediumtext | YES | | NULL | | - | phone | text | YES | | NULL | | - | mobile | varchar(50) | YES | | NULL | | - | fax | mediumtext | YES | | NULL | | - | emailpro | text | YES | | NULL | | - | phonepro | text | YES | | NULL | | - | B_streetnumber | varchar(10) | YES | | NULL | | - | B_streettype | varchar(50) | YES | | NULL | | - | B_address | varchar(100) | YES | | NULL | | - | B_city | mediumtext | YES | | NULL | | - | B_zipcode | varchar(25) | YES | | NULL | | - | B_email | text | YES | | NULL | | - | B_phone | mediumtext | YES | | NULL | | - | dateofbirth | date | YES | | NULL | | - | branchcode | varchar(10) | NO | MUL | | | - | categorycode | varchar(10) | NO | MUL | | | - | dateenrolled | date | YES | | NULL | | - | dateexpiry | date | YES | | NULL | | - | gonenoaddress | tinyint(1) | YES | | NULL | | - | lost | tinyint(1) | YES | | NULL | | - | debarred | tinyint(1) | YES | | NULL | | - | contactname | mediumtext | YES | | NULL | | - | contactfirstname | text | YES | | NULL | | - | contacttitle | text | YES | | NULL | | - | guarantorid | int(11) | YES | | NULL | | - | borrowernotes | mediumtext | YES | | NULL | | - | relationship | varchar(100) | YES | | NULL | | - | ethnicity | varchar(50) | YES | | NULL | | - | ethnotes | varchar(255) | YES | | NULL | | - | sex | varchar(1) | YES | | NULL | | - | password | varchar(30) | YES | | NULL | | - | flags | int(11) | YES | | NULL | | - | userid | varchar(30) | YES | MUL | NULL | | - | opacnote | mediumtext | YES | | NULL | | - | contactnote | varchar(255) | YES | | NULL | | - | sort1 | varchar(80) | YES | | NULL | | - | sort2 | varchar(80) | YES | | NULL | | - +------------------+--------------+------+-----+---------+----------------+ - 50 rows in set (0.01 sec) - - Then %mappings establishes the relationship between mysql field and LDAP attribute. - -=cut - -# Redefine checkauth: +# Redefine checkpw: # connect to LDAP (named or anonymous) # ~ retrieves $userid from "uid" # ~ then compares $password with userPassword @@ -140,9 +56,9 @@ $ldapname = $ldap->{user} or die ldapserver_error('user'); $ldappassword = $ldap->{pass} or die ldapserver_error('pass'); our %mapping = %{$ldap->{mapping}} or die ldapserver_error('mapping'); my @mapkeys = keys %mapping; -print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys ( total ): ", join ' ', @mapkeys, "\n"; +$debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys ( total ): ", join ' ', @mapkeys, "\n"; @mapkeys = grep {defined $mapping{$_}->{is}} @mapkeys; -print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys (populated): ", join ' ', @mapkeys, "\n"; +$debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys (populated): ", join ' ', @mapkeys, "\n"; my %config = ( anonymous => ($ldapname and $ldappassword) ? 0 : 1, @@ -157,7 +73,7 @@ sub description ($) { . "# " . $result->error_text . "\n"; } -sub checkauth { +sub checkpw { my ($dbh, $userid, $password) = @_; if ( $userid eq C4::Context->config('user') && $password eq C4::Context->config('pass') ) @@ -188,21 +104,24 @@ sub checkauth { } my $userldapentry = $search->shift_entry; - my $cmpmesg = $db->compare( $userldapentry, attr=>'userPassword', value => $password ); - if($cmpmesg->code != 6) { + 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; } - unless($config{update} or $config{replicate}) { + unless ($config{update} or $config{replicate}) { return 1; } my %borrower = ldap_entry_2_hash($userldapentry,$userid); - if (exists_local($userid)) { - ($config{update} ) and &update_local($userid,$password,%borrower); + $debug and print "checkpw received \%borrower w/ " . keys(%borrower), " keys: ", join(' ', keys %borrower), "\n"; + my ($borrowernumber,$cardnumber,$userid,$savedpw) = exists_local($userid); + if ($borrowernumber) { + ($config{update} ) and my $c2 = &update_local($userid,$password,$borrowernumber,\%borrower) || ''; + ($cardnumber eq $c2) or warn "update_local returned cardnumber '$c2' instead of '$cardnumber'"; } else { - ($config{replicate}) and warn "Replicating!!" and AddMember(%borrower); + ($config{replicate}) and $borrowernumber = AddMember(%borrower); } - return 1; + return(1, $cardnumber); } # Pass LDAP entry object and local cardnumber (userid). @@ -214,38 +133,24 @@ sub ldap_entry_2_hash ($$) { my $userldapentry = shift; my %borrower = ( cardnumber => shift ); my %memberhash; - print "keys(\%\$userldapentry) = " . join(', ', keys %$userldapentry), "\n"; - print $userldapentry->dump(); - foreach (keys %$userldapentry) { - print "\n\nLDAP key: $_\t", sprintf('(%s)', ref $userldapentry->{$_}), "\n"; - hashdump("LDAP key: ",$userldapentry->{$_}); + if ($debug) { + print "keys(\%\$userldapentry) = " . join(', ', keys %$userldapentry), "\n", $userldapentry->dump(); + foreach (keys %$userldapentry) { + print "\n\nLDAP key: $_\t", sprintf('(%s)', ref $userldapentry->{$_}), "\n"; + hashdump("LDAP key: ",$userldapentry->{$_}); + } } - warn "->{asn}->{attributes} : " . $userldapentry->{asn}->{attributes} ; - my $x = $userldapentry->{asn}->{attributes} or return undef; + my $x = $userldapentry->{attrs} or return undef; my $key; - -# asn (HASH) -# LDAP key: ->{attributes} = ARRAY w/ 17 members. -# LDAP key: ->{attributes}->{HASH(0x9234290)} = HASH w/ 2 keys. -# LDAP key: ->{attributes}->{HASH(0x9234290)}->{type} = cn -# LDAP key: ->{attributes}->{HASH(0x9234290)}->{vals} = ARRAY w/ 3 members. -# LDAP key: ->{attributes}->{HASH(0x9234290)}->{vals}->{ sss} = sss -# LDAP key: ->{attributes}->{HASH(0x9234290)}->{vals}->{ Steve Smith} = Steve Smith -# LDAP key: ->{attributes}->{HASH(0x9234290)}->{vals}->{Steve S. Smith} = Steve S. Smith -# $x $anon -# LDAP key: ->{attributes}->{HASH(0x9234490)} = HASH w/ 2 keys. -# LDAP key: ->{attributes}->{HASH(0x9234490)}->{type} = o -# LDAP key: ->{attributes}->{HASH(0x9234490)}->{vals} = ARRAY w/ 1 members. -# LDAP key: ->{attributes}->{HASH(0x9234490)}->{vals}->{metavore} = metavore -# $x=([ cn=>['sss','Steve Smith','Steve S. Smith'], sss, o=>['metavore'], ]) -# . . . . . - - foreach my $anon (@$x) { - $key = $anon->{type} or next; - $memberhash{$key} = join " ", @{$anon->{vals}}; + foreach (keys %$x) { + $memberhash{$_} = join ' ', @{$x->{$_}}; + $debug and print sprintf("building \$memberhash{%s} = ", $_), join ' ', @{$x->{$_}}, "\n"; } + $debug and print "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}}; + $debug and printf "mapping %20s ==> %-20s ($data)\n", $key, $mapping{$key}->{is}; unless (defined $data) { $data = $mapping{$key}->{content} || ''; # default or failsafe '' } @@ -254,71 +159,196 @@ sub ldap_entry_2_hash ($$) { $borrower{initials} = $memberhash{initials} || ( substr($borrower{'firstname'},0,1) . substr($borrower{ 'surname' },0,1) - . " "); + . " "); return %borrower; } sub exists_local($) { - my $sth = C4::Context->dbh->prepare("SELECT password from borrowers WHERE cardnumber=?"); - $sth->execute(shift); - return ($sth->rows) ? 1 : 0 ; + my $arg = shift; + my $dbh = C4::Context->dbh; + my $select = "SELECT borrowernumber,cardnumber,userid,password from borrowers "; + + my $sth = $dbh->prepare("$select WHERE userid=?"); # was cardnumber=? + $sth->execute($arg); + $debug and printf "Userid '$arg' exists_local? %s\n", $sth->rows; + ($sth->rows == 1) and return $sth->fetchrow; + + $sth = $dbh->prepare("$select WHERE cardnumber=?"); + $sth->execute($arg); + $debug and printf "Cardnumber '$arg' exists_local? %s\n", $sth->rows; + ($sth->rows == 1) and return $sth->fetchrow; + return 0; } -sub update_local($$%) { - # warn "MODIFY borrower"; - my $userid = shift or return undef; - my $digest = md5_base64(shift) or return undef; - my %borrower = shift or return undef; +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 $dbh = C4::Context->dbh; - my $sth = $dbh->prepare(" -UPDATE borrowers -SET firstname=?,surname=?,initials=?,address=?,city=?,phone=?, categorycode=?,branchcode=?,email=?,sort1=? -WHERE cardnumber=? - "); + my $query = "UPDATE borrowers\nSET " . + join(',', map {"$_=?"} keys %$borrower) . # don't need to sort: keys order is deterministic + "\nWHERE borrowernumber=? "; + my $sth = $dbh->prepare($query); + if ($debug) { + print STDERR $query, "\n", + join "\n", map {"$_ = " . $borrower->{$_}} + keys %$borrower; + print STDERR "\nuserid = $userid\n"; + } $sth->execute( - $borrower{firstname}, $borrower{surname}, - $borrower{initials}, $borrower{address}, - $borrower{city}, $borrower{phone}, - $borrower{categorycode}, $borrower{branchcode}, - $borrower{email}, $borrower{sort1}, - $userid + (map {$borrower->{$_}} keys %$borrower), $borrowerid # relies on deterministic keys order to match above ); # MODIFY PASSWORD/LOGIN # search borrowerid - $sth = $dbh->prepare("SELECT borrowernumber from borrowers WHERE cardnumber=? "); - $sth->execute($userid); - my ($borrowerid) = $sth->fetchrow; - # warn "change local password for $borrowerid setting $password"; + $debug and print "changing local password for borrowernumber=$borrowerid to '$digest'\n"; changepassword($userid, $borrowerid, $digest); # Confirm changes - my $cardnumber; - $sth = $dbh->prepare("SELECT password,cardnumber from borrowers WHERE userid=? "); - $cardnumber = confirmer($sth,$userid,$digest) and return $cardnumber; - $sth = $dbh->prepare("SELECT password,cardnumber from borrowers WHERE cardnumber=? "); - $cardnumber = confirmer($sth,$userid,$digest) and return $cardnumber; - die "Unexpected error after password update to $userid / $cardnumber."; -} - -sub confirmer($$$) { - my $sth = shift or return undef; - my $userid = shift or return undef; - my $digest = shift or return undef; - $sth->execute($userid); + $sth = $dbh->prepare("SELECT password,cardnumber FROM borrowers WHERE borrowernumber=? "); + $sth->execute($borrowerid); if ($sth->rows) { - my ($md5password, $othernum) = $sth->fetchrow; - ($digest eq $md5password) and return $othernum; - warn "Password mismatch after update to userid=$userid"; + my ($md5password, $cardnum) = $sth->fetchrow; + ($digest eq $md5password) and return $cardnum; + warn "Password mismatch after update to cardnumber=$cardnum (borrowernumber=$borrowerid)"; return undef; - } - warn "Could not recover record after updating password for userid=$userid"; - return 0; + } + die "Unexpected error after password update to userid/borrowernumber: $userid / $borrowerid."; } + 1; __END__ -=back +=head1 NAME + +C4::Auth - Authenticates Koha users + +=head1 SYNOPSIS + + use C4::Auth_with_ldap; + +=head1 LDAP Configuration + + This module is specific to LDAP authentification. It requires Net::LDAP package and one or more + working LDAP servers. + To use it : + * Modify ldapserver element in KOHA_CONF + * Establish field mapping in element. + + It is assumed your user records are stored according to the inetOrgPerson schema, RFC#2798. + Thus the username must match the "uid" field, and the password must match the "userpassword" field. + + Make sure that the required fields are populated in your LDAP database (and mapped in KOHA_CONF). + What are the required fields? Well, in mysql you can check the database table "borrowers" like this: + + mysql> show COLUMNS from borrowers; + +------------------+--------------+------+-----+---------+----------------+ + | Field | Type | Null | Key | Default | Extra | + +------------------+--------------+------+-----+---------+----------------+ + | borrowernumber | int(11) | NO | PRI | NULL | auto_increment | + | cardnumber | varchar(16) | YES | UNI | NULL | | + | surname | mediumtext | NO | | | | + | firstname | text | YES | | NULL | | + | title | mediumtext | YES | | NULL | | + | othernames | mediumtext | YES | | NULL | | + | initials | text | YES | | NULL | | + | streetnumber | varchar(10) | YES | | NULL | | + | streettype | varchar(50) | YES | | NULL | | + | address | mediumtext | NO | | | | + | address2 | text | YES | | NULL | | + | city | mediumtext | NO | | | | + | zipcode | varchar(25) | YES | | NULL | | + | email | mediumtext | YES | | NULL | | + | phone | text | YES | | NULL | | + | mobile | varchar(50) | YES | | NULL | | + | fax | mediumtext | YES | | NULL | | + | emailpro | text | YES | | NULL | | + | phonepro | text | YES | | NULL | | + | B_streetnumber | varchar(10) | YES | | NULL | | + | B_streettype | varchar(50) | YES | | NULL | | + | B_address | varchar(100) | YES | | NULL | | + | B_city | mediumtext | YES | | NULL | | + | B_zipcode | varchar(25) | YES | | NULL | | + | B_email | text | YES | | NULL | | + | B_phone | mediumtext | YES | | NULL | | + | dateofbirth | date | YES | | NULL | | + | branchcode | varchar(10) | NO | MUL | | | + | categorycode | varchar(10) | NO | MUL | | | + | dateenrolled | date | YES | | NULL | | + | dateexpiry | date | YES | | NULL | | + | gonenoaddress | tinyint(1) | YES | | NULL | | + | lost | tinyint(1) | YES | | NULL | | + | debarred | tinyint(1) | YES | | NULL | | + | contactname | mediumtext | YES | | NULL | | + | contactfirstname | text | YES | | NULL | | + | contacttitle | text | YES | | NULL | | + | guarantorid | int(11) | YES | | NULL | | + | borrowernotes | mediumtext | YES | | NULL | | + | relationship | varchar(100) | YES | | NULL | | + | ethnicity | varchar(50) | YES | | NULL | | + | ethnotes | varchar(255) | YES | | NULL | | + | sex | varchar(1) | YES | | NULL | | + | password | varchar(30) | YES | | NULL | | + | flags | int(11) | YES | | NULL | | + | userid | varchar(30) | YES | MUL | NULL | | # UNIQUE in next release. + | opacnote | mediumtext | YES | | NULL | | + | contactnote | varchar(255) | YES | | NULL | | + | sort1 | varchar(80) | YES | | NULL | | + | sort2 | varchar(80) | YES | | NULL | | + +------------------+--------------+------+-----+---------+----------------+ + 50 rows in set (0.01 sec) + + Where Null="NO", the field is required. + +=cut + +=head1 KOHA_CONF and field mapping + +Example XML stanza for LDAP conifugration in KOHA_CONF: + + + + localhost + dc=metavore,dc=com + cn=Manager,dc=metavore,dc=com + metavore + 1 + 1 + + + +
+ Athens, OH + + MAIN + + + + PT + +
+
+ +The subelements establishe the relationship between mysql fields and LDAP attributes. The element name +is the column in mysql, with the "is" characteristic set to the LDAP attribute name. Optionally, any content +between the element tags is taken as the default value. In this example, the default categorycode is "PT" (for +patron). + +=cut + +# ======================================== +# Using attrs instead of {asn}->attributes +# ======================================== +# +# LDAP key: ->{ cn} = ARRAY w/ 3 members. +# LDAP key: ->{ cn}->{ sss} = sss +# LDAP key: ->{ cn}->{ Steve Smith} = Steve Smith +# LDAP key: ->{ cn}->{Steve S. Smith} = Steve S. Smith +# +# LDAP key: ->{ givenname} = ARRAY w/ 1 members. +# LDAP key: ->{ givenname}->{Steve} = Steve +# =head1 SEE ALSO @@ -326,6 +356,8 @@ CGI(3) Net::LDAP() +XML::Simple() + Digest::MD5(3) =cut diff --git a/t/Auth_with_ldap.t b/t/Auth_with_ldap.t index 92278d5507..9e8587bfe8 100755 --- a/t/Auth_with_ldap.t +++ b/t/Auth_with_ldap.t @@ -15,16 +15,16 @@ BEGIN { rch => 'password2', jmf => 'password3', ); - plan tests => 3 + scalar(keys %cases); + plan tests => 7 + scalar(keys %cases); use_ok('C4::Context'); - use_ok('C4::Auth_with_ldap', qw(checkauth)); + use_ok('C4::Auth_with_ldap', qw(checkpw)); } -sub do_checkauth (;$$) { +sub do_checkpw (;$$) { my ($user,$pass) = (shift,shift); diag "($user,$pass)"; my $ret; - return ($ret = checkauth($dbh,$user,$pass), sprintf("(%s,%s) returns '%s'",$user,$pass,$ret)); + return ($ret = checkpw($dbh,$user,$pass), sprintf("(%s,%s) returns '%s'",$user,$pass,$ret)); } ok($context= C4::Context->new(), "Getting new C4::Context object"); @@ -34,12 +34,12 @@ ok($dbh = $context->dbh(), "Getting dbh from \$context object"); diag("The basis of Authentication is that we don't auth everybody."); diag("Let's make sure we reject on bad calls."); my $ret; -ok(!($ret = checkauth($dbh)), "should reject ( no arguments) returns '$ret'"); -ok(!($ret = checkauth($dbh,'','')), "should reject (empty arguments) returns '$ret'"); +ok(!($ret = checkpw($dbh)), "should reject ( no arguments) returns '$ret'"); +ok(!($ret = checkpw($dbh,'','')), "should reject (empty arguments) returns '$ret'"); print "\n"; diag("Now let's check " . scalar(keys %cases) . " test cases: "); foreach (sort keys %cases) { - ok do_checkauth($_, $cases{$_}); + ok do_checkpw($_, $cases{$_}); } 1; -- 2.39.5