View | Details | Raw Unified | Return to bug 8993
Collapse All | Expand All

(-)a/C4/Auth_with_ldap.pm (-260 / +482 lines)
Lines 1-5 Link Here
1
package C4::Auth_with_ldap;
1
package C4::Auth_with_ldap;
2
3
# Copyright 2000-2002 Katipo Communications
2
# Copyright 2000-2002 Katipo Communications
4
#
3
#
5
# This file is part of Koha.
4
# This file is part of Koha.
Lines 17-26 package C4::Auth_with_ldap; Link Here
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
18
20
use strict;
19
use Modern::Perl;
21
#use warnings; FIXME - Bug 2505
22
use Digest::MD5 qw(md5_base64);
20
use Digest::MD5 qw(md5_base64);
23
24
use C4::Debug;
21
use C4::Debug;
25
use C4::Context;
22
use C4::Context;
26
use C4::Members qw(AddMember changepassword);
23
use C4::Members qw(AddMember changepassword);
Lines 30-63 use C4::Utils qw( :all ); Link Here
30
use List::MoreUtils qw( any );
27
use List::MoreUtils qw( any );
31
use Net::LDAP;
28
use Net::LDAP;
32
use Net::LDAP::Filter;
29
use Net::LDAP::Filter;
30
use YAML;
33
31
34
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $debug);
32
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $debug);
35
33
36
BEGIN {
34
BEGIN {
37
	require Exporter;
35
    require Exporter;
38
    $VERSION = 3.07.00.049;	# set the version for version checking
36
    $VERSION = 3.07.00.049; # set the version for version checking
39
	@ISA    = qw(Exporter);
37
    @ISA     = qw(Exporter);
40
	@EXPORT = qw( checkpw_ldap );
38
    @EXPORT  = qw( checkpw_ldap );
39
}
40
41
# return the ref of the subroutine
42
sub load_subroutine {
43
    require Package::Stash;
44
    my ( $module, $sub ) = @_;
45
    my $stash = Package::Stash->new($module);
46
    unless ( %{ $stash->namespace } ) {
47
        eval "require $module";
48
        $@ and die $@;
49
    }
50
    $stash->get_package_symbol('&'.$sub);
41
}
51
}
42
52
43
# Redefine checkpw_ldap:
53
# Redefine checkpw_ldap:
44
# connect to LDAP (named or anonymous)
54
# connect to LDAP (named or anonymous)
45
# ~ retrieves $userid from KOHA_CONF mapping
55
# ~ retrieves $userid from KOHA_CONF mapping
46
# ~ then compares $password with userPassword 
56
# ~ then compares $password with userPassword
47
# ~ then gets the LDAP entry
57
# ~ then gets the LDAP entry
48
# ~ and calls the memberadd if necessary
58
# ~ and calls the memberadd if necessary
49
59
50
sub ldapserver_error {
60
sub ldapserver_error {
51
	return sprintf('No ldapserver "%s" defined in KOHA_CONF: ' . $ENV{KOHA_CONF}, shift);
61
    return sprintf('No ldapserver "%s" defined in KOHA_CONF: ' . $ENV{KOHA_CONF}, shift);
52
}
62
}
53
63
64
sub debug_msg { $debug and say STDERR @_ }
65
sub logger { say STDERR YAML::Dump @_ }
66
54
use vars qw($mapping @ldaphosts $base $ldapname $ldappassword);
67
use vars qw($mapping @ldaphosts $base $ldapname $ldappassword);
55
my $context = C4::Context->new() 	or die 'C4::Context->new failed';
68
my $context = C4::Context->new() or die 'C4::Context->new failed';
56
my $ldap = C4::Context->config("ldapserver") or die 'No "ldapserver" in server hash from KOHA_CONF: ' . $ENV{KOHA_CONF};
69
my $ldap = C4::Context->config("ldapserver") or die 'No "ldapserver" in server hash from KOHA_CONF: ' . $ENV{KOHA_CONF};
57
my $prefhost  = $ldap->{hostname}	or die ldapserver_error('hostname');
70
58
my $base      = $ldap->{base}		or die ldapserver_error('base');
71
my ( $prefhost, $base ) = ('')x2;
59
$ldapname     = $ldap->{user}		;
72
unless ( $$ldap{authmethod} ) {
60
$ldappassword = $ldap->{pass}		;
73
    say STDERR "deprecated ldap configuration, see documentation";
74
    $base = $$ldap{base} or die ldapserver_error('base');
75
    $prefhost = $$ldap{hostname} or die ldapserver_error('hostname');
76
}
77
78
$ldapname     = $ldap->{user};
79
$ldappassword = $ldap->{pass};
61
our %mapping  = %{$ldap->{mapping}}; # FIXME dpavlin -- don't die because of || (); from 6eaf8511c70eb82d797c941ef528f4310a15e9f9
80
our %mapping  = %{$ldap->{mapping}}; # FIXME dpavlin -- don't die because of || (); from 6eaf8511c70eb82d797c941ef528f4310a15e9f9
62
my @mapkeys = keys %mapping;
81
my @mapkeys = keys %mapping;
63
$debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys (  total  ): ", join ' ', @mapkeys, "\n";
82
$debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys (  total  ): ", join ' ', @mapkeys, "\n";
Lines 65-174 $debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys ( total ): ", Link Here
65
$debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys (populated): ", join ' ', @mapkeys, "\n";
84
$debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys (populated): ", join ' ', @mapkeys, "\n";
66
85
67
my %config = (
86
my %config = (
68
	anonymous => ($ldapname and $ldappassword) ? 0 : 1,
87
    anonymous => ( $ldapname and $ldappassword ) ? 0 : 1,
69
    replicate => defined($ldap->{replicate}) ? $ldap->{replicate} : 1,  #    add from LDAP to Koha database for new user
88
    # add from LDAP to Koha database for new user
70
       update => defined($ldap->{update}   ) ? $ldap->{update}    : 1,  # update from LDAP to Koha database for existing user
89
    replicate => defined( $ldap->{replicate} ) ? $ldap->{replicate} : 1,
90
    # update from LDAP to Koha database for existing user
91
    update    => defined( $ldap->{update} )    ? $ldap->{update}    : 1,
71
);
92
);
72
93
73
sub description {
94
sub description {
74
	my $result = shift or return;
95
    my $result = shift or return;
75
	return "LDAP error #" . $result->code
96
    return "LDAP error #" . $result->code
76
			. ": " . $result->error_name . "\n"
97
        . ": " . $result->error_name . "\n"
77
			. "# " . $result->error_text . "\n";
98
        . "# " . $result->error_text . "\n";
78
}
99
}
79
100
80
sub search_method {
101
sub search_method {
81
    my $db     = shift or return;
102
    my $db        = shift or return;
82
    my $userid = shift or return;
103
    my $userid    = shift or return;
83
	my $uid_field = $mapping{userid}->{is} or die ldapserver_error("mapping for 'userid'");
104
    my $uid_field = $mapping{userid}->{is} or die ldapserver_error("mapping for 'userid'");
84
	my $filter = Net::LDAP::Filter->new("$uid_field=$userid") or die "Failed to create new Net::LDAP::Filter";
105
    my $filter    = Net::LDAP::Filter->new("$uid_field=$userid") or die "Failed to create new Net::LDAP::Filter";
85
	my $search = $db->search(
106
    my $search = $db->search(
86
		  base => $base,
107
        base   => $base,
87
	 	filter => $filter,
108
        filter => $filter,
88
		# attrs => ['*'],
109
        # attrs => ['*'],
89
	) or die "LDAP search failed to return object.";
110
    ) or die "LDAP search failed to return object.";
90
	my $count = $search->count;
111
    my $count = $search->count;
91
	if ($search->code > 0) {
112
    if ($search->code > 0) {
92
		warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count) . description($search);
113
        warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count) . description($search);
93
		return 0;
114
        return 0;
94
	}
115
    }
95
	if ($count != 1) {
116
    if ($count != 1) {
96
		warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count);
117
        warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count);
97
		return 0;
118
        return 0;
98
	}
119
    }
99
    return $search;
120
    return $search;
100
}
121
}
101
122
123
# $cnx is an Net::LDAP object that dies when error occurs
124
# $search are params for the search method (default base is the one set in the config)
125
sub _anon_search {
126
    my ( $cnx, $search ) = @_;
127
128
    my $entry;
129
    for my $branch ( @{ $$ldap{branch} } ) {
130
        debug_msg "search $$search{filter} at $$branch{dn}";
131
132
        my $branch_search = { %$search, base => $$branch{dn}, search => "ObjectClass=*" };
133
        $entry = eval {
134
            $cnx
135
            ->search( %$branch_search )
136
            ->shift_entry
137
        };
138
139
        if ( $entry ) {
140
            debug_msg "found ", $entry->dn;
141
            return { entry => $entry, branch => $branch };
142
        }
143
        elsif ( $@ ) { return { error => 'UNKNOWN', msg => $@ } }
144
        else { $debug and logger { "failed search" => $branch_search } }
145
    }
146
}
147
148
sub set_xattr {
149
    my ( $id, $borrower ) = @_;
150
    if ( my $x = $$borrower{xattr} ) {
151
        #SetBorrowerAttributes is not managing when being sent an Array ref
152
        my $attrs = [ map {
153
            my $key = $_;
154
            my @listattributes;
155
            if (ref ($x->{$key}) eq "ARRAY"){
156
                foreach my $value (@{$x->{$key}}){
157
                  push @listattributes, { code => $key, value => $value }
158
                }
159
            }
160
            else {
161
                push @listattributes, { code => $key, value => $$x{$key} }
162
            }
163
            @listattributes;
164
        } keys %$x ];
165
        $debug and logger { "creating $id" => $attrs };
166
        SetBorrowerAttributes( $id, $attrs );
167
    }
168
}
169
170
sub accept_borrower {
171
    my ($borrower,$userid) = @_;
172
    for ( $$borrower{column}{userid} ) {
173
        $userid ||= $_ or die;
174
        unless ( $_ ) {
175
            $_ = $userid;
176
            next;
177
        }
178
    }
179
180
    my ($id) = exists_local( $userid ) or debug_msg "$userid is newcommer";
181
182
    my $newcommer = not $id;
183
184
    if ( $newcommer ) {
185
        return 0 unless $config{update};
186
        $debug and logger { Member => $$borrower{column} };
187
        $id = AddMember( %{ $$borrower{column} } ) or return 0;
188
    } else {
189
        if ( $config{replicate} ) {
190
            delete $$borrower{column}{dateenrolled};
191
            delete $$borrower{column}{dateexpiry};
192
            my $cardnumber = update_local
193
            ( $userid, $$borrower{column}{password}, $id, $$borrower{column} ); 
194
            if ( my $old_cardnumber = $$borrower{column}{cardnumber} ) {
195
                if ( $old_cardnumber ne $cardnumber ) {
196
                    warn "update_local returned cardnumber '$cardnumber' instead of '$old_cardnumber'";
197
                    return 0;
198
                }
199
            }
200
        }
201
    }
202
203
    if ( $newcommer || $config{'update'} ) {
204
        $debug and logger { "changing attrs for $id" => $$borrower{xattr} };
205
        set_xattr $id, $borrower;
206
    }
207
208
    return 1
209
}
210
211
sub cnx {
212
    my $cnx = Net::LDAP->new( $ldap->{uri}, onerror => 'die' ) or do {
213
        warn "ldap error: $!";
214
    };
215
    # bind MUST success
216
    my $msg = eval {
217
        $cnx->bind ($ldap->{manager}, password => $ldap->{password})
218
    };
219
    debug_msg "ldap $_:", $msg->$_ for qw/ error code /;
220
    if ( $@ ) {
221
        return {
222
            error => 'LDAP_CANTBIND',
223
            msg => $@
224
        };
225
    };
226
    return $cnx;
227
}
228
102
sub checkpw_ldap {
229
sub checkpw_ldap {
103
    my ($dbh, $userid, $password) = @_;
230
    my ($dbh, $userid, $password) = @_;
104
    my @hosts = split(',', $prefhost);
231
    my @hosts = split(',', $prefhost);
105
    my $db = Net::LDAP->new(\@hosts);
232
    my $db = Net::LDAP->new(\@hosts);
106
	#$debug and $db->debug(5);
233
    my $to_borrower = {};
234
235
    my $uattr = $$ldap{userid_from} || $$ldap{mapping}{userid}{is}
236
      or die "userid mapping not set";
237
107
    my $userldapentry;
238
    my $userldapentry;
108
	if ( $ldap->{auth_by_bind} ) {
239
109
        my $principal_name = $ldap->{principal_name};
240
    if ( $$ldap{authmethod} ) {
110
        if ($principal_name and $principal_name =~ /\%/) {
241
        for ( $$ldap{branch} ) {
111
            $principal_name = sprintf($principal_name,$userid);
242
            $_ or die "no branch, no auth";
112
        } else {
243
            ref $_ eq 'HASH' and $_ = [$_];
113
            $principal_name = $userid;
244
        }
245
246
        # This code is an attempt to introduce a new codebase that can be hookable
247
        # and can mangage more cases than the old way
248
249
        # if the filter isn't set, userid mapping is used
250
        $$ldap{filter} ||= "$uattr=%s";
251
252
        my $cnx = cnx or return 0;
253
254
        # login can be either ...
255
        my $login = do {
256
257
            # An Active Directory principal_name. Just replace the %s by the userid
258
            # well ... don't try if not AD
259
            if ( $$ldap{authmethod} ~~ [qw/ principal_name principalname principalName /] ) {
260
                sprintf( $$ldap{principal_name}, $userid )
261
            }
262
263
            # for other LDAP implementation, the standard way is to
264
            # A) Bind with the manager account and search for the DN of the user entry
265
            # B) Bind with the user DN and password.
266
            # Auth is completed if bind success.
267
            # so in this code;
268
            # - i fill $userldapentry for later use
269
            # - i return the DN
270
271
            elsif ( $$ldap{authmethod} ~~ [qw/ searchdn searchDn search_dn /] ) {
272
                $to_borrower = _anon_search($cnx, {
273
                    filter => sprintf($$ldap{filter}, $userid)
274
                }) or do {
275
                    debug_msg "no answer from ldap";
276
                    return 0;
277
                };
278
279
                if ( $$to_borrower{error} ) {
280
                    say STDERR $$to_borrower{msg};
281
                    return 0;
282
                }
283
284
                $userldapentry = $$to_borrower{entry} or do {
285
                    debug_msg "no entry returned? weird ...";
286
                };
287
288
                # login is the dn of the entry
289
                if ( $userldapentry ) { $userldapentry->dn }
290
                else {
291
                    say STDERR "can't authenticate $userid";
292
                    return 0;
293
                }
294
            } else {
295
                say STDERR "$$ldap{authmethod} authmethod is invalid,"
296
                , "please check your ldap configuration in $ENV{KOHA_CONF}";
297
                return 0;
298
            }
299
        };
300
301
        eval { $cnx->bind( $login, password => $password ) };
302
        if ( $@ ) {
303
            say STDERR "ldap bind with $login failed: $@";
304
            return 0;
114
        }
305
        }
115
		my $res = $db->bind( $principal_name, password => $password );
306
        debug_msg "congrats, you're one of us";
116
        if ( $res->code ) {
307
    } else {
117
            $debug and warn "LDAP bind failed as kohauser $principal_name: ". description($res);
308
        # This is the old stuff:
309
        if ( $ldap->{auth_by_bind} ) {
310
            my $principal_name = $ldap->{principal_name};
311
            if ( $principal_name and $principal_name =~ /\%/ ) {
312
                $principal_name = sprintf( $principal_name, $userid );
313
            } else {
314
                $principal_name = $userid;
315
            }
316
            my $res = $db->bind( $principal_name, password => $password );
317
            if ( $res->code ) {
318
                $debug and warn "LDAP bind failed as kohauser $principal_name: " . description($res);
319
                return 0;
320
            }
321
322
            # BUG #5094
323
            # 2010-08-04 JeremyC
324
            # a $userldapentry is only needed if either updating or replicating are enabled
325
            if($config{update} or $config{replicate}) {
326
                my $search = search_method($db, $userid) or return 0;    # warnings are in the sub
327
                $userldapentry = $search->shift_entry;
328
            }
329
330
        } else {
331
            say STDERR "deprecated kludge: use authmethod search_dn instead";
332
            my $res = ($config{anonymous}) ? $db->bind : $db->bind($ldapname, password=>$ldappassword);
333
            if ($res->code) {		# connection refused
334
                warn "LDAP bind failed as ldapuser " . ($ldapname || '[ANONYMOUS]') . ": " . description($res);
335
                return 0;
336
            }
337
            my $search = search_method($db, $userid) or return 0;    # warnings are in the sub
338
            $userldapentry = $search->shift_entry;
339
            my $cmpmesg = $db->compare( $userldapentry, attr => 'userpassword', value => $password );
340
            if ($cmpmesg->code != 6) {
341
                warn "LDAP Auth rejected : invalid password for user '$userid'. " . description($cmpmesg);
118
            return 0;
342
            return 0;
343
            }
119
        }
344
        }
345
    }
120
346
121
	# FIXME dpavlin -- we really need $userldapentry leater on even if using auth_by_bind!
347
    if ( my $t = $$ldap{transformation} ) {
122
348
        $$t{subroutine} ||= 'get_borrower';
123
	# BUG #5094
349
        my $get_borrower = load_subroutine ( @$t{qw/ module subroutine /} );
124
	# 2010-08-04 JeremyC
350
        unless ( $get_borrower ) {
125
	# a $userldapentry is only needed if either updating or replicating are enabled
351
            warn "no get_borrower $$t{subroutine} subroutine in $$t{module}";
126
	if($config{update} or $config{replicate}) {
352
            return 0;
127
	    my $search = search_method($db, $userid) or return 0;   # warnings are in the sub
353
        }
128
	    $userldapentry = $search->shift_entry;
354
        debug_msg "$$t{subroutine} subroutine loaded from $$t{module}";
129
	}
355
        if ( my $b = $get_borrower->( $$to_borrower{entry} ) ) {
130
356
            return accept_borrower $b,$userid;
131
	} else {
357
        }
132
		my $res = ($config{anonymous}) ? $db->bind : $db->bind($ldapname, password=>$ldappassword);
358
        else { return 0 }
133
		if ($res->code) {		# connection refused
359
    }
134
			warn "LDAP bind failed as ldapuser " . ($ldapname || '[ANONYMOUS]') . ": " . description($res);
135
			return 0;
136
		}
137
        my $search = search_method($db, $userid) or return 0;   # warnings are in the sub
138
        $userldapentry = $search->shift_entry;
139
		my $cmpmesg = $db->compare( $userldapentry, attr=>'userpassword', value => $password );
140
		if ($cmpmesg->code != 6) {
141
			warn "LDAP Auth rejected : invalid password for user '$userid'. " . description($cmpmesg);
142
			return 0;
143
		}
144
	}
145
360
146
    # To get here, LDAP has accepted our user's login attempt.
361
    # To get here, LDAP has accepted our user's login attempt.
147
    # But we still have work to do.  See perldoc below for detailed breakdown.
362
    # But we still have work to do.  See perldoc below for detailed breakdown.
148
363
149
    my (%borrower);
364
    my %borrower;
150
	my ($borrowernumber,$cardnumber,$local_userid,$savedpw) = exists_local($userid);
365
    my ($borrowernumber, $cardnumber, $local_userid, $savedpw) = exists_local($userid);
151
366
152
    if (( $borrowernumber and $config{update}   ) or
367
    if (  ( $borrowernumber and $config{update} )
153
        (!$borrowernumber and $config{replicate})   ) {
368
        or ( !$borrowernumber and $config{replicate} ) ) {
154
        %borrower = ldap_entry_2_hash($userldapentry,$userid);
369
        %borrower = ldap_entry_2_hash( $userldapentry, $userid );
155
        $debug and print STDERR "checkpw_ldap received \%borrower w/ " . keys(%borrower), " keys: ", join(' ', keys %borrower), "\n";
370
        $debug and print STDERR "checkpw_ldap received \%borrower w/ " . keys(%borrower), " keys: ", join(' ', keys %borrower), "\n";
156
    }
371
    }
157
372
158
    if ($borrowernumber) {
373
    if ($borrowernumber) {
159
        if ($config{update}) { # A1, B1
374
        if ($config{update}) { # A1, B1
160
            my $c2 = &update_local($local_userid,$password,$borrowernumber,\%borrower) || '';
375
            my $c2 = &update_local($local_userid, $password, $borrowernumber, \%borrower) || '';
161
            ($cardnumber eq $c2) or warn "update_local returned cardnumber '$c2' instead of '$cardnumber'";
376
            ($cardnumber eq $c2) or warn "update_local returned cardnumber '$c2' instead of '$cardnumber'";
162
        } else { # C1, D1
377
        } else { # C1, D1
163
            # maybe update just the password?
378
            # maybe update just the password?
164
		return(1, $cardnumber, $local_userid);
379
            return (1, $cardnumber, $local_userid);
165
        }
380
        }
166
    } elsif ($config{replicate}) { # A2, C2
381
    } elsif ($config{replicate}) { # A2, C2
167
        $borrowernumber = AddMember(%borrower) or die "AddMember failed";
382
        $borrowernumber = AddMember(%borrower) or die "AddMember failed";
168
   } else {
383
    } else {
169
        return 0;   # B2, D2
384
        return 0;                       # B2, D2
170
    }
385
    }
171
	if (C4::Context->preference('ExtendedPatronAttributes') && $borrowernumber && ($config{update} ||$config{replicate})) {
386
    if (C4::Context->preference('ExtendedPatronAttributes') && $borrowernumber && ($config{update} || $config{replicate})) {
172
        my $extended_patron_attributes;
387
        my $extended_patron_attributes;
173
        foreach my $attribute_type ( C4::Members::AttributeTypes::GetAttributeTypes() ) {
388
        foreach my $attribute_type ( C4::Members::AttributeTypes::GetAttributeTypes() ) {
174
                my $code = $attribute_type->{code};
389
                my $code = $attribute_type->{code};
Lines 176-197 sub checkpw_ldap { Link Here
176
                        push @$extended_patron_attributes, { code => $code, value => $borrower{$code} };
391
                        push @$extended_patron_attributes, { code => $code, value => $borrower{$code} };
177
                }
392
                }
178
        }
393
        }
179
		my @errors;
394
180
		#Check before add
395
        my @errors;
181
		for (my $i; $i< scalar(@$extended_patron_attributes)-1;$i++) {
396
        #Check before add
182
			my $attr=$extended_patron_attributes->[$i];
397
        for (my $i; $i < scalar(@$extended_patron_attributes) - 1; $i++) {
183
			unless (C4::Members::Attributes::CheckUniqueness($attr->{code}, $attr->{value}, $borrowernumber)) {
398
            my $attr = $extended_patron_attributes->[$i];
184
				unshift @errors, $i;
399
            unless (C4::Members::Attributes::CheckUniqueness($attr->{code}, $attr->{value}, $borrowernumber)) {
185
				warn "ERROR_extended_unique_id_failed $attr->{code} $attr->{value}";
400
                unshift @errors, $i;
186
			}
401
                warn "ERROR_extended_unique_id_failed $attr->{code} $attr->{value}";
187
		}
402
            }
188
		#Removing erroneous attributes
403
        }
189
		foreach my $index (@errors){
404
190
			@$extended_patron_attributes=splice(@$extended_patron_attributes,$index,1);
405
        #Removing erroneous attributes
191
		}
406
        foreach my $index (@errors) {
192
           C4::Members::Attributes::SetBorrowerAttributes($borrowernumber, $extended_patron_attributes);
407
            @$extended_patron_attributes = splice(@$extended_patron_attributes, $index, 1);
193
  	}
408
        }
194
return(1, $cardnumber, $userid);
409
        C4::Members::Attributes::SetBorrowerAttributes($borrowernumber, $extended_patron_attributes);
410
    }
411
    return (1, $cardnumber, $userid);
195
}
412
}
196
413
197
# Pass LDAP entry object and local cardnumber (userid).
414
# Pass LDAP entry object and local cardnumber (userid).
Lines 200-264 return(1, $cardnumber, $userid); Link Here
200
# Ensure that mandatory fields are correctly filled!
417
# Ensure that mandatory fields are correctly filled!
201
#
418
#
202
sub ldap_entry_2_hash {
419
sub ldap_entry_2_hash {
203
	my $userldapentry = shift;
420
    my $userldapentry = shift;
204
	my %borrower = ( cardnumber => shift );
421
    my %borrower = ( cardnumber => shift );
205
	my %memberhash;
422
    my %memberhash;
206
	$userldapentry->exists('uid');	# This is bad, but required!  By side-effect, this initializes the attrs hash. 
423
    $userldapentry->exists('uid');    # This is bad, but required!  By side-effect, this initializes the attrs hash.
207
	if ($debug) {
424
    if ($debug) {
208
		print STDERR "\nkeys(\%\$userldapentry) = " . join(', ', keys %$userldapentry), "\n", $userldapentry->dump();
425
        print STDERR "\nkeys(\%\$userldapentry) = " . join(', ', keys %$userldapentry), "\n", $userldapentry->dump();
209
		foreach (keys %$userldapentry) {
426
        foreach (keys %$userldapentry) {
210
			print STDERR "\n\nLDAP key: $_\t", sprintf('(%s)', ref $userldapentry->{$_}), "\n";
427
            print STDERR "\n\nLDAP key: $_\t", sprintf('(%s)', ref $userldapentry->{$_}), "\n";
211
			hashdump("LDAP key: ",$userldapentry->{$_});
428
            hashdump("LDAP key: ",$userldapentry->{$_});
212
		}
429
        }
213
	}
430
    }
214
	my $x = $userldapentry->{attrs} or return;
431
    my $x = $userldapentry->{attrs} or return;
215
	foreach (keys %$x) {
432
    foreach (keys %$x) {
216
		$memberhash{$_} = join ' ', @{$x->{$_}};	
433
        $memberhash{$_} = join ' ', @{$x->{$_}};
217
		$debug and print STDERR sprintf("building \$memberhash{%s} = ", $_, join(' ', @{$x->{$_}})), "\n";
434
        $debug and print STDERR sprintf("building \$memberhash{%s} = ", $_, join(' ', @{$x->{$_}})), "\n";
218
	}
435
    }
219
	$debug and print STDERR "Finsihed \%memberhash has ", scalar(keys %memberhash), " keys\n",
436
    $debug and print STDERR "Finsihed \%memberhash has ", scalar(keys %memberhash), " keys\n",
220
					"Referencing \%mapping with ", scalar(keys %mapping), " keys\n";
437
                    "Referencing \%mapping with ", scalar(keys %mapping), " keys\n";
221
	foreach my $key (keys %mapping) {
438
    foreach my $key (keys %mapping) {
222
		my  $data = $memberhash{ lc($mapping{$key}->{is}) }; # Net::LDAP returns all names in lowercase
439
        my  $data = $memberhash{ lc($mapping{$key}->{is}) }; # Net::LDAP returns all names in lowercase
223
		$debug and printf STDERR "mapping %20s ==> %-20s (%s)\n", $key, $mapping{$key}->{is}, $data;
440
        $debug and printf STDERR "mapping %20s ==> %-20s (%s)\n", $key, $mapping{$key}->{is}, $data;
224
		unless (defined $data) { 
441
        unless (defined $data) {
225
			$data = $mapping{$key}->{content} || '';	# default or failsafe ''
442
            $data = $mapping{$key}->{content} || '';    # default or failsafe ''
226
		}
443
        }
227
		$borrower{$key} = ($data ne '') ? $data : ' ' ;
444
        $borrower{$key} = ($data ne '') ? $data : ' ';
228
	}
445
    }
229
	$borrower{initials} = $memberhash{initials} || 
446
    $borrower{initials} = $memberhash{initials} || 
230
		( substr($borrower{'firstname'},0,1)
447
        ( substr($borrower{'firstname'},0,1)
231
  		. substr($borrower{ 'surname' },0,1)
448
          . substr($borrower{ 'surname' },0,1)
232
  		. " ");
449
          . " ");
233
450
234
	# check if categorycode exists, if not, fallback to default from koha-conf.xml
451
    # check if categorycode exists, if not, fallback to default from koha-conf.xml
235
	my $dbh = C4::Context->dbh;
452
    my $dbh = C4::Context->dbh;
236
	my $sth = $dbh->prepare("SELECT categorycode FROM categories WHERE categorycode = ?");
453
    my $sth = $dbh->prepare("SELECT categorycode FROM categories WHERE categorycode = ?");
237
	$sth->execute( uc($borrower{'categorycode'}) );
454
    $sth->execute( uc($borrower{'categorycode'}) );
238
	unless ( my $row = $sth->fetchrow_hashref ) {
455
    unless ( my $row = $sth->fetchrow_hashref ) {
239
		my $default = $mapping{'categorycode'}->{content};
456
        my $default = $mapping{'categorycode'}->{content};
240
		$debug && warn "Can't find ", $borrower{'categorycode'}, " default to: $default for ", $borrower{userid};
457
        $debug && warn "Can't find ", $borrower{'categorycode'}, " default to: $default for ", $borrower{userid};
241
		$borrower{'categorycode'} = $default
458
        $borrower{'categorycode'} = $default
242
	}
459
    }
243
460
244
	return %borrower;
461
    return %borrower;
245
}
462
}
246
463
247
sub exists_local {
464
sub exists_local {
248
	my $arg = shift;
465
    my $arg    = shift;
249
	my $dbh = C4::Context->dbh;
466
    my $dbh    = C4::Context->dbh;
250
	my $select = "SELECT borrowernumber,cardnumber,userid,password FROM borrowers ";
467
    my $select = "SELECT borrowers.borrowernumber,cardnumber,userid,borrowers.password FROM borrowers ";
251
468
252
	my $sth = $dbh->prepare("$select WHERE userid=?");	# was cardnumber=?
469
    my $sth = $dbh->prepare("$select WHERE userid=?");    # was cardnumber=?
253
	$sth->execute($arg);
470
    $sth->execute($arg);
254
	$debug and printf STDERR "Userid '$arg' exists_local? %s\n", $sth->rows;
471
    $debug and printf STDERR "Userid '$arg' exists_local? %s\n", $sth->rows;
255
	($sth->rows == 1) and return $sth->fetchrow;
472
    ($sth->rows == 1) and return $sth->fetchrow;
256
473
257
	$sth = $dbh->prepare("$select WHERE cardnumber=?");
474
    $sth = $dbh->prepare("$select WHERE cardnumber=?");
258
	$sth->execute($arg);
475
    $sth->execute($arg);
259
	$debug and printf STDERR "Cardnumber '$arg' exists_local? %s\n", $sth->rows;
476
    $debug and printf STDERR "Cardnumber '$arg' exists_local? %s\n", $sth->rows;
260
	($sth->rows == 1) and return $sth->fetchrow;
477
    ($sth->rows == 1) and return $sth->fetchrow;
261
	return 0;
478
479
    $sth = $dbh->prepare("$select JOIN borrower_attributes USING (borrowernumber) WHERE attribute=?");
480
    $sth->execute($arg);
481
    $debug and printf STDERR "attribute '$arg' exists_local? %s\n", $sth->rows;
482
    ($sth->rows == 1) and return $sth->fetchrow;
483
    return 0;
262
}
484
}
263
485
264
sub _do_changepassword {
486
sub _do_changepassword {
Lines 266-305 sub _do_changepassword { Link Here
266
    $debug and print STDERR "changing local password for borrowernumber=$borrowerid to '$digest'\n";
488
    $debug and print STDERR "changing local password for borrowernumber=$borrowerid to '$digest'\n";
267
    changepassword($userid, $borrowerid, $digest);
489
    changepassword($userid, $borrowerid, $digest);
268
490
269
	# Confirm changes
491
    # Confirm changes
270
	my $sth = C4::Context->dbh->prepare("SELECT password,cardnumber FROM borrowers WHERE borrowernumber=? ");
492
    my $sth = C4::Context->dbh->prepare("SELECT password,cardnumber FROM borrowers WHERE borrowernumber=? ");
271
	$sth->execute($borrowerid);
493
    $sth->execute($borrowerid);
272
	if ($sth->rows) {
494
    if ($sth->rows) {
273
		my ($md5password, $cardnum) = $sth->fetchrow;
495
        my ($md5password, $cardnum) = $sth->fetchrow;
274
        ($digest eq $md5password) and return $cardnum;
496
        ($digest eq $md5password) and return $cardnum;
275
		warn "Password mismatch after update to cardnumber=$cardnum (borrowernumber=$borrowerid)";
497
        warn "Password mismatch after update to cardnumber=$cardnum (borrowernumber=$borrowerid)";
276
		return;
498
        return;
277
	}
499
    }
278
	die "Unexpected error after password update to userid/borrowernumber: $userid / $borrowerid.";
500
    die "Unexpected error after password update to userid/borrowernumber: $userid / $borrowerid.";
279
}
501
}
280
502
281
sub update_local {
503
sub update_local {
282
	my   $userid   = shift             or return;
504
    my $userid     = shift             or return;
283
	my   $digest   = md5_base64(shift) or return;
505
    my $digest     = md5_base64(shift) or return;
284
	my $borrowerid = shift             or return;
506
    my $borrowerid = shift             or return;
285
	my $borrower   = shift             or return;
507
    my $borrower   = shift             or return;
286
	my @keys = keys %$borrower;
508
    my @keys       = keys %$borrower;
287
	my $dbh = C4::Context->dbh;
509
    my $dbh        = C4::Context->dbh;
288
	my $query = "UPDATE  borrowers\nSET     " . 
510
    my $query = "
289
		join(',', map {"$_=?"} @keys) .
511
        UPDATE borrowers
290
		"\nWHERE   borrowernumber=? "; 
512
        SET " . join( ',', map { "$_=?" } @keys ) . "
291
	my $sth = $dbh->prepare($query);
513
        WHERE   borrowernumber=?
292
	if ($debug) {
514
    ";
293
		print STDERR $query, "\n",
515
    my $sth = $dbh->prepare($query);
294
			join "\n", map {"$_ = '" . $borrower->{$_} . "'"} @keys;
516
    if ($debug) {
295
		print STDERR "\nuserid = $userid\n";
517
        print STDERR $query, "\n",
296
	}
518
            join "\n", map { "$_ = '" . $borrower->{$_} . "'" } @keys;
297
	$sth->execute(
519
        print STDERR "\nuserid = $userid\n";
298
		((map {$borrower->{$_}} @keys), $borrowerid)
520
    }
299
	);
521
    $sth->execute( ( ( map { $borrower->{$_} } @keys ), $borrowerid ) );
300
522
301
	# MODIFY PASSWORD/LOGIN
523
    # MODIFY PASSWORD/LOGIN
302
	_do_changepassword($userid, $borrowerid, $digest);
524
    _do_changepassword($userid, $borrowerid, $digest);
303
}
525
}
304
526
305
1;
527
1;
Lines 327-406 C4::Auth - Authenticates Koha users Link Here
327
	Make sure that ALL required fields are populated by your LDAP database (and mapped in KOHA_CONF).  
549
	Make sure that ALL required fields are populated by your LDAP database (and mapped in KOHA_CONF).  
328
	What are the required fields?  Well, in mysql you can check the database table "borrowers" like this:
550
	What are the required fields?  Well, in mysql you can check the database table "borrowers" like this:
329
551
330
	mysql> show COLUMNS from borrowers;
552
    mysql> show COLUMNS from borrowers;
331
		+---------------------+--------------+------+-----+---------+----------------+
553
        +---------------------+--------------+------+-----+---------+----------------+
332
		| Field               | Type         | Null | Key | Default | Extra          |
554
        | Field               | Type         | Null | Key | Default | Extra          |
333
		+---------------------+--------------+------+-----+---------+----------------+
555
        +---------------------+--------------+------+-----+---------+----------------+
334
		| borrowernumber      | int(11)      | NO   | PRI | NULL    | auto_increment |
556
        | borrowernumber      | int(11)      | NO   | PRI | NULL    | auto_increment |
335
		| cardnumber          | varchar(16)  | YES  | UNI | NULL    |                |
557
        | cardnumber          | varchar(16)  | YES  | UNI | NULL    |                |
336
		| surname             | mediumtext   | NO   |     | NULL    |                |
558
        | surname             | mediumtext   | NO   |     | NULL    |                |
337
		| firstname           | text         | YES  |     | NULL    |                |
559
        | firstname           | text         | YES  |     | NULL    |                |
338
		| title               | mediumtext   | YES  |     | NULL    |                |
560
        | title               | mediumtext   | YES  |     | NULL    |                |
339
		| othernames          | mediumtext   | YES  |     | NULL    |                |
561
        | othernames          | mediumtext   | YES  |     | NULL    |                |
340
		| initials            | text         | YES  |     | NULL    |                |
562
        | initials            | text         | YES  |     | NULL    |                |
341
		| streetnumber        | varchar(10)  | YES  |     | NULL    |                |
563
        | streetnumber        | varchar(10)  | YES  |     | NULL    |                |
342
		| streettype          | varchar(50)  | YES  |     | NULL    |                |
564
        | streettype          | varchar(50)  | YES  |     | NULL    |                |
343
		| address             | mediumtext   | NO   |     | NULL    |                |
565
        | address             | mediumtext   | NO   |     | NULL    |                |
344
		| address2            | text         | YES  |     | NULL    |                |
566
        | address2            | text         | YES  |     | NULL    |                |
345
		| city                | mediumtext   | NO   |     | NULL    |                |
567
        | city                | mediumtext   | NO   |     | NULL    |                |
346
		| state               | mediumtext   | YES  |     | NULL    |                |
568
        | state               | text         | YES  |     | NULL    |                |
347
		| zipcode             | varchar(25)  | YES  |     | NULL    |                |
569
        | zipcode             | varchar(25)  | YES  |     | NULL    |                |
348
		| country             | text         | YES  |     | NULL    |                |
570
        | country             | text         | YES  |     | NULL    |                |
349
		| email               | mediumtext   | YES  |     | NULL    |                |
571
        | email               | mediumtext   | YES  |     | NULL    |                |
350
		| phone               | text         | YES  |     | NULL    |                |
572
        | phone               | text         | YES  |     | NULL    |                |
351
		| mobile              | varchar(50)  | YES  |     | NULL    |                |
573
        | mobile              | varchar(50)  | YES  |     | NULL    |                |
352
		| fax                 | mediumtext   | YES  |     | NULL    |                |
574
        | fax                 | mediumtext   | YES  |     | NULL    |                |
353
		| emailpro            | text         | YES  |     | NULL    |                |
575
        | emailpro            | text         | YES  |     | NULL    |                |
354
		| phonepro            | text         | YES  |     | NULL    |                |
576
        | phonepro            | text         | YES  |     | NULL    |                |
355
		| B_streetnumber      | varchar(10)  | YES  |     | NULL    |                |
577
        | B_streetnumber      | varchar(10)  | YES  |     | NULL    |                |
356
		| B_streettype        | varchar(50)  | YES  |     | NULL    |                |
578
        | B_streettype        | varchar(50)  | YES  |     | NULL    |                |
357
		| B_address           | varchar(100) | YES  |     | NULL    |                |
579
        | B_address           | varchar(100) | YES  |     | NULL    |                |
358
		| B_address2          | text         | YES  |     | NULL    |                |
580
        | B_address2          | text         | YES  |     | NULL    |                |
359
		| B_city              | mediumtext   | YES  |     | NULL    |                |
581
        | B_city              | mediumtext   | YES  |     | NULL    |                |
360
		| B_state             | mediumtext   | YES  |     | NULL    |                |
582
        | B_state             | text         | YES  |     | NULL    |                |
361
		| B_zipcode           | varchar(25)  | YES  |     | NULL    |                |
583
        | B_zipcode           | varchar(25)  | YES  |     | NULL    |                |
362
		| B_country           | text         | YES  |     | NULL    |                |
584
        | B_country           | text         | YES  |     | NULL    |                |
363
		| B_email             | text         | YES  |     | NULL    |                |
585
        | B_email             | text         | YES  |     | NULL    |                |
364
		| B_phone             | mediumtext   | YES  |     | NULL    |                |
586
        | B_phone             | mediumtext   | YES  |     | NULL    |                |
365
		| dateofbirth         | date         | YES  |     | NULL    |                |
587
        | dateofbirth         | date         | YES  |     | NULL    |                |
366
		| branchcode          | varchar(10)  | NO   | MUL |         |                |
588
        | branchcode          | varchar(10)  | NO   | MUL |         |                |
367
		| categorycode        | varchar(10)  | NO   | MUL |         |                |
589
        | categorycode        | varchar(10)  | NO   | MUL |         |                |
368
		| dateenrolled        | date         | YES  |     | NULL    |                |
590
        | dateenrolled        | date         | YES  |     | NULL    |                |
369
		| dateexpiry          | date         | YES  |     | NULL    |                |
591
        | dateexpiry          | date         | YES  |     | NULL    |                |
370
		| gonenoaddress       | tinyint(1)   | YES  |     | NULL    |                |
592
        | gonenoaddress       | tinyint(1)   | YES  |     | NULL    |                |
371
		| lost                | tinyint(1)   | YES  |     | NULL    |                |
593
        | lost                | tinyint(1)   | YES  |     | NULL    |                |
372
		| debarred            | date         | YES  |     | NULL    |                |
594
        | debarred            | date         | YES  |     | NULL    |                |
373
		| debarredcomment     | varchar(255) | YES  |     | NULL    |                |
595
        | debarredcomment     | varchar(255) | YES  |     | NULL    |                |
374
		| contactname         | mediumtext   | YES  |     | NULL    |                |
596
        | contactname         | mediumtext   | YES  |     | NULL    |                |
375
		| contactfirstname    | text         | YES  |     | NULL    |                |
597
        | contactfirstname    | text         | YES  |     | NULL    |                |
376
		| contacttitle        | text         | YES  |     | NULL    |                |
598
        | contacttitle        | text         | YES  |     | NULL    |                |
377
		| guarantorid         | int(11)      | YES  | MUL | NULL    |                |
599
        | guarantorid         | int(11)      | YES  | MUL | NULL    |                |
378
		| borrowernotes       | mediumtext   | YES  |     | NULL    |                |
600
        | borrowernotes       | mediumtext   | YES  |     | NULL    |                |
379
		| relationship        | varchar(100) | YES  |     | NULL    |                |
601
        | relationship        | varchar(100) | YES  |     | NULL    |                |
380
		| ethnicity           | varchar(50)  | YES  |     | NULL    |                |
602
        | ethnicity           | varchar(50)  | YES  |     | NULL    |                |
381
		| ethnotes            | varchar(255) | YES  |     | NULL    |                |
603
        | ethnotes            | varchar(255) | YES  |     | NULL    |                |
382
		| sex                 | varchar(1)   | YES  |     | NULL    |                |
604
        | sex                 | varchar(1)   | YES  |     | NULL    |                |
383
		| password            | varchar(30)  | YES  |     | NULL    |                |
605
        | password            | varchar(30)  | YES  |     | NULL    |                |
384
		| flags               | int(11)      | YES  |     | NULL    |                |
606
        | flags               | int(11)      | YES  |     | NULL    |                |
385
		| userid              | varchar(30)  | YES  | MUL | NULL    |                |
607
        | userid              | varchar(75)  | YES  | MUL | NULL    |                |
386
		| opacnote            | mediumtext   | YES  |     | NULL    |                |
608
        | opacnote            | mediumtext   | YES  |     | NULL    |                |
387
		| contactnote         | varchar(255) | YES  |     | NULL    |                |
609
        | contactnote         | varchar(255) | YES  |     | NULL    |                |
388
		| sort1               | varchar(80)  | YES  |     | NULL    |                |
610
        | sort1               | varchar(80)  | YES  |     | NULL    |                |
389
		| sort2               | varchar(80)  | YES  |     | NULL    |                |
611
        | sort2               | varchar(80)  | YES  |     | NULL    |                |
390
		| altcontactfirstname | varchar(255) | YES  |     | NULL    |                |
612
        | altcontactfirstname | varchar(255) | YES  |     | NULL    |                |
391
		| altcontactsurname   | varchar(255) | YES  |     | NULL    |                |
613
        | altcontactsurname   | varchar(255) | YES  |     | NULL    |                |
392
		| altcontactaddress1  | varchar(255) | YES  |     | NULL    |                |
614
        | altcontactaddress1  | varchar(255) | YES  |     | NULL    |                |
393
		| altcontactaddress2  | varchar(255) | YES  |     | NULL    |                |
615
        | altcontactaddress2  | varchar(255) | YES  |     | NULL    |                |
394
		| altcontactaddress3  | varchar(255) | YES  |     | NULL    |                |
616
        | altcontactaddress3  | varchar(255) | YES  |     | NULL    |                |
395
		| altcontactstate     | mediumtext   | YES  |     | NULL    |                |
617
        | altcontactstate     | text         | YES  |     | NULL    |                |
396
		| altcontactzipcode   | varchar(50)  | YES  |     | NULL    |                |
618
        | altcontactzipcode   | varchar(50)  | YES  |     | NULL    |                |
397
		| altcontactcountry   | text         | YES  |     | NULL    |                |
619
        | altcontactcountry   | text         | YES  |     | NULL    |                |
398
		| altcontactphone     | varchar(50)  | YES  |     | NULL    |                |
620
        | altcontactphone     | varchar(50)  | YES  |     | NULL    |                |
399
		| smsalertnumber      | varchar(50)  | YES  |     | NULL    |                |
621
        | smsalertnumber      | varchar(50)  | YES  |     | NULL    |                |
400
		| privacy             | int(11)      | NO   |     | 1       |                |
622
        | privacy             | int(11)      | NO   |     | 1       |                |
401
		+---------------------+--------------+------+-----+---------+----------------+
623
        +---------------------+--------------+------+-----+---------+----------------+
402
		66 rows in set (0.00 sec)
624
        67 rows in set (0.00 sec)
403
		Where Null="NO", the field is required.
625
        Where Null="NO", the field is required.
404
626
405
=head1 KOHA_CONF and field mapping
627
=head1 KOHA_CONF and field mapping
406
628
(-)a/C4/LDAPAuthMethodTutorial.pod (-1 / +91 lines)
Line 0 Link Here
0
- 
1
=head1 LDAP auth_method configuration
2
3
This document is a step by step explaination for the new LDAP configuration
4
method. The old one (see C4::Auth_with_ldap) still exists and you should give it
5
a try if your needs of mappings and transformations are low.
6
7
=head2 What do I have to ask to the directory administrator
8
9
=head3 How to reach the service
10
11
What we need is build the url of the ldap service we want to reach. So ask him
12
for the URL. If he don't know, ask for the scheme (or protocol), the hostname
13
and the port of the directory. Only hostname and scheme are mandatory.
14
15
The scheme must be ldap or ldaps (ldaps is for crypted ldap, ldap over SSL).
16
The url construction is:
17
18
    scheme://hostname:port
19
    scheme://hostname
20
21
examples
22
23
    host=directory.example.com port=389 scheme=ldap
24
25
gives you:
26
27
    ldap://directory.example.com:389
28
29
Also: Active Directory (the Microsoft implementation) is an LDAP alike directory
30
but Koha have to know it's active directory to use it, so please ask.
31
32
=head3 What is the binding method ?
33
34
Are anonymous allowed to make some searches on the server? If not, what is the
35
credentials for the koha account ? Credentials are a pair (DN, password)
36
37
    uid=koha,ou=people,dc=example,dc=com MYS3CRET
38
39
in Active Directory, it can be a login@example.com with a password
40
41
    koha@example.com MYS3CRET
42
43
=head3 Now configure koha
44
45
This is a basic exemple of things you will add in the C</config> part of
46
C<koha-conf.xml>.
47
48
First of all, you need to tell koha that ldap authentication is now relevant
49
50
    <useldapserver>1</useldapserver>
51
52
If you use anonymous method, you have to give credentials of koha account
53
54
    <ldapserver url="ldap://directory.example.com"
55
          manager="uid=koha,ou=people,dc=example,dc=com"
56
          password="MYS3CRET"
57
          authmethod="search_dn" >
58
59
Also, you can set the values for replication and update (documented in legacy
60
pod):
61
62
    <ldapserver url="ldap://directory.example.com"
63
          manager="uid=koha,ou=people,dc=example,dc=com"
64
          password="MYS3CRET"
65
          authmethod="search_dn"
66
          replicate="1" update="1"
67
    >
68
69
You must now tell koha the ldap branches where to find users
70
71
    <useldapserver>1</useldapserver>
72
    <ldapserver url="ldap://directory.example.com"
73
          manager="uid=koha,ou=people,dc=example,dc=com"
74
          password="MYS3CRET"
75
          authmethod="search_dn"
76
          replicate="1" update="1"
77
    >
78
      <branch dn="ou=people,dc=site1,dc=example,dc=com" />
79
      <branch dn="ou=people,dc=site2,dc=example,dc=com" />
80
      <branch dn="ou=people,dc=site4,dc=example,dc=com" />
81
82
The best part of this new config is that you don't have to rely on simple
83
mappings anymore (but you still can): you can use a perl module to write much
84
sofisticated transformations directly in perl:
85
86
      <transformation module="C4::LDAPTransform::CustomTransform" />
87
88
This module only have to define a subroutine named get_borrower
89
90
91

Return to bug 8993