Bug 11629: (follow-up) Add message for librarian that status was updated
[koha.git] / C4 / Auth_with_ldap.pm
1 package C4::Auth_with_ldap;
2
3 # Copyright 2000-2002 Katipo Communications
4 #
5 # This file is part of Koha.
6 #
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.
11 #
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.
15 #
16 # You should have received a copy of the GNU General Public License along
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20 use strict;
21 #use warnings; FIXME - Bug 2505
22 use Carp;
23
24 use C4::Debug;
25 use C4::Context;
26 use C4::Members qw(AddMember changepassword);
27 use C4::Members::Attributes;
28 use C4::Members::AttributeTypes;
29 use C4::Auth qw(checkpw_internal);
30 use Koha::AuthUtils qw(hash_password);
31 use List::MoreUtils qw( any );
32 use Net::LDAP;
33 use Net::LDAP::Filter;
34
35 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $debug);
36
37 BEGIN {
38         require Exporter;
39     $VERSION = 3.07.00.049;     # set the version for version checking
40         @ISA    = qw(Exporter);
41         @EXPORT = qw( checkpw_ldap );
42 }
43
44 # Redefine checkpw_ldap:
45 # connect to LDAP (named or anonymous)
46 # ~ retrieves $userid from KOHA_CONF mapping
47 # ~ then compares $password with userPassword 
48 # ~ then gets the LDAP entry
49 # ~ and calls the memberadd if necessary
50
51 sub ldapserver_error {
52         return sprintf('No ldapserver "%s" defined in KOHA_CONF: ' . $ENV{KOHA_CONF}, shift);
53 }
54
55 use vars qw($mapping @ldaphosts $base $ldapname $ldappassword);
56 my $context = C4::Context->new()        or die 'C4::Context->new failed';
57 my $ldap = C4::Context->config("ldapserver") or die 'No "ldapserver" in server hash from KOHA_CONF: ' . $ENV{KOHA_CONF};
58 my $prefhost  = $ldap->{hostname}       or die ldapserver_error('hostname');
59 my $base      = $ldap->{base}           or die ldapserver_error('base');
60 $ldapname     = $ldap->{user}           ;
61 $ldappassword = $ldap->{pass}           ;
62 our %mapping  = %{$ldap->{mapping}}; # FIXME dpavlin -- don't die because of || (); from 6eaf8511c70eb82d797c941ef528f4310a15e9f9
63 my @mapkeys = keys %mapping;
64 $debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys (  total  ): ", join ' ', @mapkeys, "\n";
65 @mapkeys = grep {defined $mapping{$_}->{is}} @mapkeys;
66 $debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys (populated): ", join ' ', @mapkeys, "\n";
67
68 my %config = (
69         anonymous => ($ldapname and $ldappassword) ? 0 : 1,
70     replicate => defined($ldap->{replicate}) ? $ldap->{replicate} : 1,  #    add from LDAP to Koha database for new user
71        update => defined($ldap->{update}   ) ? $ldap->{update}    : 1,  # update from LDAP to Koha database for existing user
72 );
73
74 sub description {
75         my $result = shift or return;
76         return "LDAP error #" . $result->code
77                         . ": " . $result->error_name . "\n"
78                         . "# " . $result->error_text . "\n";
79 }
80
81 sub search_method {
82     my $db     = shift or return;
83     my $userid = shift or return;
84         my $uid_field = $mapping{userid}->{is} or die ldapserver_error("mapping for 'userid'");
85         my $filter = Net::LDAP::Filter->new("$uid_field=$userid") or die "Failed to create new Net::LDAP::Filter";
86         my $search = $db->search(
87                   base => $base,
88                 filter => $filter,
89                 # attrs => ['*'],
90         ) or die "LDAP search failed to return object.";
91         my $count = $search->count;
92         if ($search->code > 0) {
93                 warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count) . description($search);
94                 return 0;
95         }
96         if ($count != 1) {
97                 warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count);
98                 return 0;
99         }
100     return $search;
101 }
102
103 sub checkpw_ldap {
104     my ($dbh, $userid, $password) = @_;
105     my @hosts = split(',', $prefhost);
106     my $db = Net::LDAP->new(\@hosts);
107     unless ( $db ) {
108         warn "LDAP connexion failed";
109         return 0;
110     }
111
112         #$debug and $db->debug(5);
113     my $userldapentry;
114
115     if ( $ldap->{auth_by_bind} ) {
116         my $principal_name;
117         if ( $ldap->{anonymous_bind} ) {
118
119             # Perform an anonymous bind
120             my $res = $db->bind;
121             if ( $res->code ) {
122                 warn "Anonymous LDAP bind failed: " . description($res);
123                 return 0;
124             }
125
126             # Perform a LDAP search for the given username
127             my $search = search_method( $db, $userid )
128               or return 0;    # warnings are in the sub
129             $userldapentry = $search->shift_entry;
130             $principal_name = $userldapentry->dn;
131         }
132         else {
133             $principal_name = $ldap->{principal_name};
134             if ( $principal_name and $principal_name =~ /\%/ ) {
135                 $principal_name = sprintf( $principal_name, $userid );
136             }
137             else {
138                 $principal_name = $userid;
139             }
140         }
141
142         # Perform a LDAP bind for the given username using the matched DN
143         my $res = $db->bind( $principal_name, password => $password );
144         if ( $res->code ) {
145             warn "LDAP bind failed as kohauser $userid: " . description($res);
146             return 0;
147         }
148         if ( !defined($userldapentry)
149             && ( $config{update} or $config{replicate} ) )
150         {
151             my $search = search_method( $db, $userid ) or return 0;
152             $userldapentry = $search->shift_entry;
153         }
154     } else {
155                 my $res = ($config{anonymous}) ? $db->bind : $db->bind($ldapname, password=>$ldappassword);
156                 if ($res->code) {               # connection refused
157                         warn "LDAP bind failed as ldapuser " . ($ldapname || '[ANONYMOUS]') . ": " . description($res);
158                         return 0;
159                 }
160         my $search = search_method($db, $userid) or return 0;   # warnings are in the sub
161         $userldapentry = $search->shift_entry;
162                 my $cmpmesg = $db->compare( $userldapentry, attr=>'userpassword', value => $password );
163                 if ($cmpmesg->code != 6) {
164                         warn "LDAP Auth rejected : invalid password for user '$userid'. " . description($cmpmesg);
165                         return 0;
166                 }
167         }
168
169     # To get here, LDAP has accepted our user's login attempt.
170     # But we still have work to do.  See perldoc below for detailed breakdown.
171
172     my (%borrower);
173         my ($borrowernumber,$cardnumber,$local_userid,$savedpw) = exists_local($userid);
174
175     if (( $borrowernumber and $config{update}   ) or
176         (!$borrowernumber and $config{replicate})   ) {
177         %borrower = ldap_entry_2_hash($userldapentry,$userid);
178         $debug and print STDERR "checkpw_ldap received \%borrower w/ " . keys(%borrower), " keys: ", join(' ', keys %borrower), "\n";
179     }
180
181     if ($borrowernumber) {
182         if ($config{update}) { # A1, B1
183             my $c2 = &update_local($local_userid,$password,$borrowernumber,\%borrower) || '';
184             ($cardnumber eq $c2) or warn "update_local returned cardnumber '$c2' instead of '$cardnumber'";
185         } else { # C1, D1
186             # maybe update just the password?
187                 return(1, $cardnumber, $local_userid);
188         }
189     } elsif ($config{replicate}) { # A2, C2
190         $borrowernumber = AddMember(%borrower) or die "AddMember failed";
191    } else {
192         return 0;   # B2, D2
193     }
194     if (C4::Context->preference('ExtendedPatronAttributes') && $borrowernumber && ($config{update} ||$config{replicate})) {
195         my @extended_patron_attributes;
196         foreach my $attribute_type ( C4::Members::AttributeTypes::GetAttributeTypes() ) {
197             my $code = $attribute_type->{code};
198             if ( exists($borrower{$code}) && $borrower{$code} !~ m/^\s*$/ ) { # skip empty values
199                 push @extended_patron_attributes, { code => $code, value => $borrower{$code} };
200             }
201         }
202         #Check before add
203         my @unique_attr;
204         foreach my $attr ( @extended_patron_attributes ) {
205             if (C4::Members::Attributes::CheckUniqueness($attr->{code}, $attr->{value}, $borrowernumber)) {
206                 push @unique_attr, $attr;
207             } else {
208                 warn "ERROR_extended_unique_id_failed $attr->{code} $attr->{value}";
209             }
210         }
211         C4::Members::Attributes::SetBorrowerAttributes($borrowernumber, \@unique_attr);
212     }
213 return(1, $cardnumber, $userid);
214 }
215
216 # Pass LDAP entry object and local cardnumber (userid).
217 # Returns borrower hash.
218 # Edit KOHA_CONF so $memberhash{'xxx'} fits your ldap structure.
219 # Ensure that mandatory fields are correctly filled!
220 #
221 sub ldap_entry_2_hash {
222         my $userldapentry = shift;
223         my %borrower = ( cardnumber => shift );
224         my %memberhash;
225         $userldapentry->exists('uid');  # This is bad, but required!  By side-effect, this initializes the attrs hash. 
226         if ($debug) {
227                 foreach (keys %$userldapentry) {
228                         print STDERR "\n\nLDAP key: $_\t", sprintf('(%s)', ref $userldapentry->{$_}), "\n";
229                 }
230         }
231         my $x = $userldapentry->{attrs} or return;
232         foreach (keys %$x) {
233                 $memberhash{$_} = join ' ', @{$x->{$_}};        
234                 $debug and print STDERR sprintf("building \$memberhash{%s} = ", $_, join(' ', @{$x->{$_}})), "\n";
235         }
236         $debug and print STDERR "Finsihed \%memberhash has ", scalar(keys %memberhash), " keys\n",
237                                         "Referencing \%mapping with ", scalar(keys %mapping), " keys\n";
238         foreach my $key (keys %mapping) {
239                 my  $data = $memberhash{ lc($mapping{$key}->{is}) }; # Net::LDAP returns all names in lowercase
240                 $debug and printf STDERR "mapping %20s ==> %-20s (%s)\n", $key, $mapping{$key}->{is}, $data;
241                 unless (defined $data) { 
242                         $data = $mapping{$key}->{content} || '';        # default or failsafe ''
243                 }
244                 $borrower{$key} = ($data ne '') ? $data : ' ' ;
245         }
246         $borrower{initials} = $memberhash{initials} || 
247                 ( substr($borrower{'firstname'},0,1)
248                 . substr($borrower{ 'surname' },0,1)
249                 . " ");
250
251         # check if categorycode exists, if not, fallback to default from koha-conf.xml
252         my $dbh = C4::Context->dbh;
253         my $sth = $dbh->prepare("SELECT categorycode FROM categories WHERE categorycode = ?");
254         $sth->execute( uc($borrower{'categorycode'}) );
255         unless ( my $row = $sth->fetchrow_hashref ) {
256                 my $default = $mapping{'categorycode'}->{content};
257                 $debug && warn "Can't find ", $borrower{'categorycode'}, " default to: $default for ", $borrower{userid};
258                 $borrower{'categorycode'} = $default
259         }
260
261         return %borrower;
262 }
263
264 sub exists_local {
265         my $arg = shift;
266         my $dbh = C4::Context->dbh;
267         my $select = "SELECT borrowernumber,cardnumber,userid,password FROM borrowers ";
268
269         my $sth = $dbh->prepare("$select WHERE userid=?");      # was cardnumber=?
270         $sth->execute($arg);
271         $debug and printf STDERR "Userid '$arg' exists_local? %s\n", $sth->rows;
272         ($sth->rows == 1) and return $sth->fetchrow;
273
274         $sth = $dbh->prepare("$select WHERE cardnumber=?");
275         $sth->execute($arg);
276         $debug and printf STDERR "Cardnumber '$arg' exists_local? %s\n", $sth->rows;
277         ($sth->rows == 1) and return $sth->fetchrow;
278         return 0;
279 }
280
281 sub _do_changepassword {
282     my ($userid, $borrowerid, $password) = @_;
283
284     my $digest = hash_password($password);
285
286     $debug and print STDERR "changing local password for borrowernumber=$borrowerid to '$digest'\n";
287     changepassword($userid, $borrowerid, $digest);
288
289     my ($ok, $cardnum) = checkpw_internal(C4::Context->dbh, $userid, $password);
290     return $cardnum if $ok;
291
292     warn "Password mismatch after update to borrowernumber=$borrowerid";
293     return;
294 }
295
296 sub update_local {
297     my $userid     = shift or croak "No userid";
298     my $password   = shift or croak "No password";
299     my $borrowerid = shift or croak "No borrowerid";
300     my $borrower   = shift or croak "No borrower record";
301
302     my @keys = keys %$borrower;
303     my $dbh = C4::Context->dbh;
304     my $query = "UPDATE  borrowers\nSET     " .
305         join(',', map {"$_=?"} @keys) .
306         "\nWHERE   borrowernumber=? ";
307     my $sth = $dbh->prepare($query);
308     if ($debug) {
309         print STDERR $query, "\n",
310             join "\n", map {"$_ = '" . $borrower->{$_} . "'"} @keys;
311         print STDERR "\nuserid = $userid\n";
312     }
313     $sth->execute(
314         ((map {$borrower->{$_}} @keys), $borrowerid)
315     );
316
317     # MODIFY PASSWORD/LOGIN
318     _do_changepassword($userid, $borrowerid, $password);
319 }
320
321 1;
322 __END__
323
324 =head1 NAME
325
326 C4::Auth - Authenticates Koha users
327
328 =head1 SYNOPSIS
329
330   use C4::Auth_with_ldap;
331
332 =head1 LDAP Configuration
333
334     This module is specific to LDAP authentification. It requires Net::LDAP package and one or more
335         working LDAP servers.
336         To use it :
337            * Modify ldapserver element in KOHA_CONF
338            * Establish field mapping in <mapping> element.
339
340         For example, if your user records are stored according to the inetOrgPerson schema, RFC#2798,
341         the username would match the "uid" field, and the password should match the "userpassword" field.
342
343         Make sure that ALL required fields are populated by your LDAP database (and mapped in KOHA_CONF).  
344         What are the required fields?  Well, in mysql you can check the database table "borrowers" like this:
345
346         mysql> show COLUMNS from borrowers;
347                 +---------------------+--------------+------+-----+---------+----------------+
348                 | Field               | Type         | Null | Key | Default | Extra          |
349                 +---------------------+--------------+------+-----+---------+----------------+
350                 | borrowernumber      | int(11)      | NO   | PRI | NULL    | auto_increment |
351                 | cardnumber          | varchar(16)  | YES  | UNI | NULL    |                |
352                 | surname             | mediumtext   | NO   |     | NULL    |                |
353                 | firstname           | text         | YES  |     | NULL    |                |
354                 | title               | mediumtext   | YES  |     | NULL    |                |
355                 | othernames          | mediumtext   | YES  |     | NULL    |                |
356                 | initials            | text         | YES  |     | NULL    |                |
357                 | streetnumber        | varchar(10)  | YES  |     | NULL    |                |
358                 | streettype          | varchar(50)  | YES  |     | NULL    |                |
359                 | address             | mediumtext   | NO   |     | NULL    |                |
360                 | address2            | text         | YES  |     | NULL    |                |
361                 | city                | mediumtext   | NO   |     | NULL    |                |
362                 | state               | mediumtext   | YES  |     | NULL    |                |
363                 | zipcode             | varchar(25)  | YES  |     | NULL    |                |
364                 | country             | text         | YES  |     | NULL    |                |
365                 | email               | mediumtext   | YES  |     | NULL    |                |
366                 | phone               | text         | YES  |     | NULL    |                |
367                 | mobile              | varchar(50)  | YES  |     | NULL    |                |
368                 | fax                 | mediumtext   | YES  |     | NULL    |                |
369                 | emailpro            | text         | YES  |     | NULL    |                |
370                 | phonepro            | text         | YES  |     | NULL    |                |
371                 | B_streetnumber      | varchar(10)  | YES  |     | NULL    |                |
372                 | B_streettype        | varchar(50)  | YES  |     | NULL    |                |
373                 | B_address           | varchar(100) | YES  |     | NULL    |                |
374                 | B_address2          | text         | YES  |     | NULL    |                |
375                 | B_city              | mediumtext   | YES  |     | NULL    |                |
376                 | B_state             | mediumtext   | YES  |     | NULL    |                |
377                 | B_zipcode           | varchar(25)  | YES  |     | NULL    |                |
378                 | B_country           | text         | YES  |     | NULL    |                |
379                 | B_email             | text         | YES  |     | NULL    |                |
380                 | B_phone             | mediumtext   | YES  |     | NULL    |                |
381                 | dateofbirth         | date         | YES  |     | NULL    |                |
382                 | branchcode          | varchar(10)  | NO   | MUL |         |                |
383                 | categorycode        | varchar(10)  | NO   | MUL |         |                |
384                 | dateenrolled        | date         | YES  |     | NULL    |                |
385                 | dateexpiry          | date         | YES  |     | NULL    |                |
386                 | gonenoaddress       | tinyint(1)   | YES  |     | NULL    |                |
387                 | lost                | tinyint(1)   | YES  |     | NULL    |                |
388                 | debarred            | date         | YES  |     | NULL    |                |
389                 | debarredcomment     | varchar(255) | YES  |     | NULL    |                |
390                 | contactname         | mediumtext   | YES  |     | NULL    |                |
391                 | contactfirstname    | text         | YES  |     | NULL    |                |
392                 | contacttitle        | text         | YES  |     | NULL    |                |
393                 | guarantorid         | int(11)      | YES  | MUL | NULL    |                |
394                 | borrowernotes       | mediumtext   | YES  |     | NULL    |                |
395                 | relationship        | varchar(100) | YES  |     | NULL    |                |
396                 | ethnicity           | varchar(50)  | YES  |     | NULL    |                |
397                 | ethnotes            | varchar(255) | YES  |     | NULL    |                |
398                 | sex                 | varchar(1)   | YES  |     | NULL    |                |
399                 | password            | varchar(30)  | YES  |     | NULL    |                |
400                 | flags               | int(11)      | YES  |     | NULL    |                |
401                 | userid              | varchar(30)  | YES  | MUL | NULL    |                |
402                 | opacnote            | mediumtext   | YES  |     | NULL    |                |
403                 | contactnote         | varchar(255) | YES  |     | NULL    |                |
404                 | sort1               | varchar(80)  | YES  |     | NULL    |                |
405                 | sort2               | varchar(80)  | YES  |     | NULL    |                |
406                 | altcontactfirstname | varchar(255) | YES  |     | NULL    |                |
407                 | altcontactsurname   | varchar(255) | YES  |     | NULL    |                |
408                 | altcontactaddress1  | varchar(255) | YES  |     | NULL    |                |
409                 | altcontactaddress2  | varchar(255) | YES  |     | NULL    |                |
410                 | altcontactaddress3  | varchar(255) | YES  |     | NULL    |                |
411                 | altcontactstate     | mediumtext   | YES  |     | NULL    |                |
412                 | altcontactzipcode   | varchar(50)  | YES  |     | NULL    |                |
413                 | altcontactcountry   | text         | YES  |     | NULL    |                |
414                 | altcontactphone     | varchar(50)  | YES  |     | NULL    |                |
415                 | smsalertnumber      | varchar(50)  | YES  |     | NULL    |                |
416                 | privacy             | int(11)      | NO   |     | 1       |                |
417                 +---------------------+--------------+------+-----+---------+----------------+
418                 66 rows in set (0.00 sec)
419                 Where Null="NO", the field is required.
420
421 =head1 KOHA_CONF and field mapping
422
423 Example XML stanza for LDAP configuration in KOHA_CONF.
424
425  <config>
426   ...
427   <useldapserver>1</useldapserver>
428   <!-- LDAP SERVER (optional) -->
429   <ldapserver id="ldapserver">
430     <hostname>localhost</hostname>
431     <base>dc=metavore,dc=com</base>
432     <user>cn=Manager,dc=metavore,dc=com</user>             <!-- DN, if not anonymous -->
433     <pass>metavore</pass>          <!-- password, if not anonymous -->
434     <replicate>1</replicate>       <!-- add new users from LDAP to Koha database -->
435     <update>1</update>             <!-- update existing users in Koha database -->
436     <auth_by_bind>0</auth_by_bind> <!-- set to 1 to authenticate by binding instead of
437                                         password comparison, e.g., to use Active Directory -->
438     <anonymous_bind>0</anonymous_bind> <!-- set to 1 if users should be searched using
439                                             an anonymous bind, even when auth_by_bind is on -->
440     <principal_name>%s@my_domain.com</principal_name>
441                                    <!-- optional, for auth_by_bind: a printf format to make userPrincipalName from koha userid.
442                                         Not used with anonymous_bind. -->
443
444     <mapping>                  <!-- match koha SQL field names to your LDAP record field names -->
445       <firstname    is="givenname"      ></firstname>
446       <surname      is="sn"             ></surname>
447       <address      is="postaladdress"  ></address>
448       <city         is="l"              >Athens, OH</city>
449       <zipcode      is="postalcode"     ></zipcode>
450       <branchcode   is="branch"         >MAIN</branchcode>
451       <userid       is="uid"            ></userid>
452       <password     is="userpassword"   ></password>
453       <email        is="mail"           ></email>
454       <categorycode is="employeetype"   >PT</categorycode>
455       <phone        is="telephonenumber"></phone>
456     </mapping> 
457   </ldapserver> 
458  </config>
459
460 The <mapping> subelements establish the relationship between mysql fields and LDAP attributes. The element name
461 is the column in mysql, with the "is" characteristic set to the LDAP attribute name.  Optionally, any content
462 between the element tags is taken as the default value.  In this example, the default categorycode is "PT" (for
463 patron).  
464
465 =head1 CONFIGURATION
466
467 Once a user has been accepted by the LDAP server, there are several possibilities for how Koha will behave, depending on 
468 your configuration and the presence of a matching Koha user in your local DB:
469
470                          LOCAL_USER
471  OPTION UPDATE REPLICATE  EXISTS?  RESULT
472    A1      1       1        1      OK : We're updating them anyway.
473    A2      1       1        0      OK : We're adding them anyway.
474    B1      1       0        1      OK : We update them.
475    B2      1       0        0     FAIL: We cannot add new user.
476    C1      0       1        1      OK : We do nothing.  (maybe should update password?)
477    C2      0       1        0      OK : We add the new user.
478    D1      0       0        1      OK : We do nothing.  (maybe should update password?)
479    D2      0       0        0     FAIL: We cannot add new user.
480
481 Note: failure here just means that Koha will fallback to checking the local DB.  That is, a given user could login with
482 their LDAP password OR their local one.  If this is a problem, then you should enable update and supply a mapping for 
483 password.  Then the local value will be updated at successful LDAP login and the passwords will be synced.
484
485 If you choose NOT to update local users, the borrowers table will not be affected at all.
486 Note that this means that patron passwords may appear to change if LDAP is ever disabled, because
487 the local table never contained the LDAP values.  
488
489 =head2 auth_by_bind
490
491 Binds as the user instead of retrieving their record.  Recommended if update disabled.
492
493 =head2 principal_name
494
495 Provides an optional sprintf-style format for manipulating the userid before the bind.
496 Even though the userPrincipalName is one intended target, any uniquely identifying
497 attribute that the server allows to be used for binding could be used.
498
499 Currently, principal_name only operates when auth_by_bind is enabled.
500
501 =head2 Active Directory 
502
503 The auth_by_bind and principal_name settings are recommended for Active Directory.
504
505 Under default Active Directory rules, we cannot determine the distinguishedName attribute from the Koha userid as reliably as
506 we would typically under openldap.  Instead of:
507
508     distinguishedName: CN=barnes.7,DC=my_company,DC=com
509
510 We might get:
511
512     distinguishedName: CN=Barnes\, Jim,OU=Test Accounts,OU=User Accounts,DC=my_company,DC=com
513
514 Matching that would require us to know more info about the account (firstname, surname) and to include punctuation and whitespace
515 in Koha userids.  But the userPrincipalName should be consistent, something like:
516
517     userPrincipalName: barnes.7@my_company.com
518
519 Therefore it is often easier to bind to Active Directory with userPrincipalName, effectively the
520 canonical email address for that user, or what it would be if email were enabled for them.  If Koha userid values 
521 will match the username portion of the userPrincipalName, and the domain suffix is the same for all users, then use principal_name
522 like this:
523     <principal_name>%s@core.my_company.com</principal_name>
524
525 The user of the previous example, barnes.7, would then attempt to bind as:
526     barnes.7@core.my_company.com
527
528 =head1 SEE ALSO
529
530 CGI(3)
531
532 Net::LDAP()
533
534 XML::Simple()
535
536 Digest::MD5(3)
537
538 sprintf()
539
540 =cut
541
542 # For reference, here's an important difference in the data structure we rely on.
543 # ========================================
544 # Using attrs instead of {asn}->attributes
545 # ========================================
546 #
547 #       LDAP key: ->{             cn} = ARRAY w/ 3 members.
548 #       LDAP key: ->{             cn}->{           sss} = sss
549 #       LDAP key: ->{             cn}->{   Steve Smith} = Steve Smith
550 #       LDAP key: ->{             cn}->{Steve S. Smith} = Steve S. Smith
551 #
552 #       LDAP key: ->{      givenname} = ARRAY w/ 1 members.
553 #       LDAP key: ->{      givenname}->{Steve} = Steve
554 #