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

(-)a/C4/Auth_with_ldap.pm (-255 / +478 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-104 $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);
Lines 109-176 sub checkpw_ldap { Link Here
109
    }
236
    }
110
237
111
	#$debug and $db->debug(5);
238
	#$debug and $db->debug(5);
239
    my $to_borrower = {};
240
241
    my $uattr = $$ldap{userid_from} || $$ldap{mapping}{userid}{is}
242
      or die "userid mapping not set";
243
112
    my $userldapentry;
244
    my $userldapentry;
113
245
114
  if ( $ldap->{auth_by_bind} ) {
246
    if ( $$ldap{authmethod} ) {
115
    # Perform an anonymous bind
247
        for ( $$ldap{branch} ) {
116
    my $res = $db->bind;
248
            $_ or die "no branch, no auth";
117
    if ( $res->code ) {
249
            ref $_ eq 'HASH' and $_ = [$_];
118
      $debug and warn "Anonymous LDAP bind failed: ". description($res);
250
        }
119
      return 0;
120
    }
121
251
122
    # Perform a LDAP search for the given username
252
        # This code is an attempt to introduce a new codebase that can be hookable
123
    my $search = search_method($db, $userid) or return 0;   # warnings are in the sub
253
        # and can mangage more cases than the old way
124
    $userldapentry = $search->shift_entry;
254
255
        # if the filter isn't set, userid mapping is used
256
        $$ldap{filter} ||= "$uattr=%s";
257
258
        my $cnx = cnx or return 0;
259
260
        # login can be either ...
261
        my $login = do {
262
263
            # An Active Directory principal_name. Just replace the %s by the userid
264
            # well ... don't try if not AD
265
            if ( $$ldap{authmethod} ~~ [qw/ principal_name principalname principalName /] ) {
266
                sprintf( $$ldap{principal_name}, $userid )
267
            }
268
269
            # for other LDAP implementation, the standard way is to
270
            # A) Bind with the manager account and search for the DN of the user entry
271
            # B) Bind with the user DN and password.
272
            # Auth is completed if bind success.
273
            # so in this code;
274
            # - i fill $userldapentry for later use
275
            # - i return the DN
276
277
            elsif ( $$ldap{authmethod} ~~ [qw/ searchdn searchDn search_dn /] ) {
278
                $to_borrower = _anon_search($cnx, {
279
                    filter => sprintf($$ldap{filter}, $userid)
280
                }) or do {
281
                    debug_msg "no answer from ldap";
282
                    return 0;
283
                };
284
285
                if ( $$to_borrower{error} ) {
286
                    say STDERR $$to_borrower{msg};
287
                    return 0;
288
                }
125
289
126
    # Perform a LDAP bind for the given username using the matched DN
290
                $userldapentry = $$to_borrower{entry} or do {
127
    $res = $db->bind( $userldapentry->dn, password => $password );
291
                    debug_msg "no entry returned? weird ...";
128
    if ( $res->code ) {
292
                };
129
      $debug and warn "LDAP bind failed as kohauser $userid: ". description($res);
293
130
      return 0;
294
                # login is the dn of the entry
295
                if ( $userldapentry ) { $userldapentry->dn }
296
                else {
297
                    say STDERR "can't authenticate $userid";
298
                    return 0;
299
                }
300
            } else {
301
                say STDERR "$$ldap{authmethod} authmethod is invalid,"
302
                , "please check your ldap configuration in $ENV{KOHA_CONF}";
303
                return 0;
304
            }
305
        };
306
307
        eval { $cnx->bind( $login, password => $password ) };
308
        if ( $@ ) {
309
            say STDERR "ldap bind with $login failed: $@";
310
            return 0;
311
        }
312
        debug_msg "congrats, you're one of us";
313
    } else {
314
        # This is the old stuff:
315
        if ( $ldap->{auth_by_bind} ) {
316
            # Perform an anonymous bind
317
            my $res = $db->bind;
318
            if ( $res->code ) {
319
              $debug and warn "Anonymous LDAP bind failed: ". description($res);
320
              return 0;
321
            }
322
323
            # Perform a LDAP search for the given username
324
            my $search = search_method($db, $userid) or return 0;   # warnings are in the sub
325
            $userldapentry = $search->shift_entry;
326
327
            # Perform a LDAP bind for the given username using the matched DN
328
            $res = $db->bind( $userldapentry->dn, password => $password );
329
            if ( $res->code ) {
330
              $debug and warn "LDAP bind failed as kohauser $userid: ". description($res);
331
              return 0;
332
            }
333
        } else {
334
            say STDERR "deprecated kludge: use authmethod search_dn instead";
335
            my $res = ($config{anonymous}) ? $db->bind : $db->bind($ldapname, password=>$ldappassword);
336
            if ($res->code) {		# connection refused
337
                warn "LDAP bind failed as ldapuser " . ($ldapname || '[ANONYMOUS]') . ": " . description($res);
338
                return 0;
339
            }
340
            my $search = search_method($db, $userid) or return 0;    # warnings are in the sub
341
            $userldapentry = $search->shift_entry;
342
            my $cmpmesg = $db->compare( $userldapentry, attr => 'userpassword', value => $password );
343
            if ($cmpmesg->code != 6) {
344
                warn "LDAP Auth rejected : invalid password for user '$userid'. " . description($cmpmesg);
345
            return 0;
346
            }
347
        }
131
    }
348
    }
132
349
133
  } else {
350
    if ( my $t = $$ldap{transformation} ) {
134
		my $res = ($config{anonymous}) ? $db->bind : $db->bind($ldapname, password=>$ldappassword);
351
        $$t{subroutine} ||= 'get_borrower';
135
		if ($res->code) {		# connection refused
352
        my $get_borrower = load_subroutine ( @$t{qw/ module subroutine /} );
136
			warn "LDAP bind failed as ldapuser " . ($ldapname || '[ANONYMOUS]') . ": " . description($res);
353
        unless ( $get_borrower ) {
137
			return 0;
354
            warn "no get_borrower $$t{subroutine} subroutine in $$t{module}";
138
		}
355
            return 0;
139
        my $search = search_method($db, $userid) or return 0;   # warnings are in the sub
356
        }
140
        $userldapentry = $search->shift_entry;
357
        debug_msg "$$t{subroutine} subroutine loaded from $$t{module}";
141
		my $cmpmesg = $db->compare( $userldapentry, attr=>'userpassword', value => $password );
358
        if ( my $b = $get_borrower->( $$to_borrower{entry} ) ) {
142
		if ($cmpmesg->code != 6) {
359
            return accept_borrower $b,$userid;
143
			warn "LDAP Auth rejected : invalid password for user '$userid'. " . description($cmpmesg);
360
        }
144
			return 0;
361
        else { return 0 }
145
		}
362
    }
146
	}
147
363
148
    # To get here, LDAP has accepted our user's login attempt.
364
    # To get here, LDAP has accepted our user's login attempt.
149
    # But we still have work to do.  See perldoc below for detailed breakdown.
365
    # But we still have work to do.  See perldoc below for detailed breakdown.
150
366
151
    my (%borrower);
367
    my %borrower;
152
	my ($borrowernumber,$cardnumber,$local_userid,$savedpw) = exists_local($userid);
368
    my ($borrowernumber, $cardnumber, $local_userid, $savedpw) = exists_local($userid);
153
369
154
    if (( $borrowernumber and $config{update}   ) or
370
    if (  ( $borrowernumber and $config{update} )
155
        (!$borrowernumber and $config{replicate})   ) {
371
        or ( !$borrowernumber and $config{replicate} ) ) {
156
        %borrower = ldap_entry_2_hash($userldapentry,$userid);
372
        %borrower = ldap_entry_2_hash( $userldapentry, $userid );
157
        $debug and print STDERR "checkpw_ldap received \%borrower w/ " . keys(%borrower), " keys: ", join(' ', keys %borrower), "\n";
373
        $debug and print STDERR "checkpw_ldap received \%borrower w/ " . keys(%borrower), " keys: ", join(' ', keys %borrower), "\n";
158
    }
374
    }
159
375
160
    if ($borrowernumber) {
376
    if ($borrowernumber) {
161
        if ($config{update}) { # A1, B1
377
        if ($config{update}) { # A1, B1
162
            my $c2 = &update_local($local_userid,$password,$borrowernumber,\%borrower) || '';
378
            my $c2 = &update_local($local_userid, $password, $borrowernumber, \%borrower) || '';
163
            ($cardnumber eq $c2) or warn "update_local returned cardnumber '$c2' instead of '$cardnumber'";
379
            ($cardnumber eq $c2) or warn "update_local returned cardnumber '$c2' instead of '$cardnumber'";
164
        } else { # C1, D1
380
        } else { # C1, D1
165
            # maybe update just the password?
381
            # maybe update just the password?
166
		return(1, $cardnumber, $local_userid);
382
            return (1, $cardnumber, $local_userid);
167
        }
383
        }
168
    } elsif ($config{replicate}) { # A2, C2
384
    } elsif ($config{replicate}) { # A2, C2
169
        $borrowernumber = AddMember(%borrower) or die "AddMember failed";
385
        $borrowernumber = AddMember(%borrower) or die "AddMember failed";
170
   } else {
386
    } else {
171
        return 0;   # B2, D2
387
        return 0;                       # B2, D2
172
    }
388
    }
173
	if (C4::Context->preference('ExtendedPatronAttributes') && $borrowernumber && ($config{update} ||$config{replicate})) {
389
    if (C4::Context->preference('ExtendedPatronAttributes') && $borrowernumber && ($config{update} || $config{replicate})) {
174
        my $extended_patron_attributes;
390
        my $extended_patron_attributes;
175
        foreach my $attribute_type ( C4::Members::AttributeTypes::GetAttributeTypes() ) {
391
        foreach my $attribute_type ( C4::Members::AttributeTypes::GetAttributeTypes() ) {
176
                my $code = $attribute_type->{code};
392
                my $code = $attribute_type->{code};
Lines 178-199 sub checkpw_ldap { Link Here
178
                        push @$extended_patron_attributes, { code => $code, value => $borrower{$code} };
394
                        push @$extended_patron_attributes, { code => $code, value => $borrower{$code} };
179
                }
395
                }
180
        }
396
        }
181
		my @errors;
397
182
		#Check before add
398
        my @errors;
183
		for (my $i; $i< scalar(@$extended_patron_attributes)-1;$i++) {
399
        #Check before add
184
			my $attr=$extended_patron_attributes->[$i];
400
        for (my $i; $i < scalar(@$extended_patron_attributes) - 1; $i++) {
185
			unless (C4::Members::Attributes::CheckUniqueness($attr->{code}, $attr->{value}, $borrowernumber)) {
401
            my $attr = $extended_patron_attributes->[$i];
186
				unshift @errors, $i;
402
            unless (C4::Members::Attributes::CheckUniqueness($attr->{code}, $attr->{value}, $borrowernumber)) {
187
				warn "ERROR_extended_unique_id_failed $attr->{code} $attr->{value}";
403
                unshift @errors, $i;
188
			}
404
                warn "ERROR_extended_unique_id_failed $attr->{code} $attr->{value}";
189
		}
405
            }
190
		#Removing erroneous attributes
406
        }
191
		foreach my $index (@errors){
407
192
			@$extended_patron_attributes=splice(@$extended_patron_attributes,$index,1);
408
        #Removing erroneous attributes
193
		}
409
        foreach my $index (@errors) {
194
           C4::Members::Attributes::SetBorrowerAttributes($borrowernumber, $extended_patron_attributes);
410
            @$extended_patron_attributes = splice(@$extended_patron_attributes, $index, 1);
195
  	}
411
        }
196
return(1, $cardnumber, $userid);
412
        C4::Members::Attributes::SetBorrowerAttributes($borrowernumber, $extended_patron_attributes);
413
    }
414
    return (1, $cardnumber, $userid);
197
}
415
}
198
416
199
# Pass LDAP entry object and local cardnumber (userid).
417
# Pass LDAP entry object and local cardnumber (userid).
Lines 202-266 return(1, $cardnumber, $userid); Link Here
202
# Ensure that mandatory fields are correctly filled!
420
# Ensure that mandatory fields are correctly filled!
203
#
421
#
204
sub ldap_entry_2_hash {
422
sub ldap_entry_2_hash {
205
	my $userldapentry = shift;
423
    my $userldapentry = shift;
206
	my %borrower = ( cardnumber => shift );
424
    my %borrower = ( cardnumber => shift );
207
	my %memberhash;
425
    my %memberhash;
208
	$userldapentry->exists('uid');	# This is bad, but required!  By side-effect, this initializes the attrs hash. 
426
    $userldapentry->exists('uid');    # This is bad, but required!  By side-effect, this initializes the attrs hash.
209
	if ($debug) {
427
    if ($debug) {
210
		print STDERR "\nkeys(\%\$userldapentry) = " . join(', ', keys %$userldapentry), "\n", $userldapentry->dump();
428
        print STDERR "\nkeys(\%\$userldapentry) = " . join(', ', keys %$userldapentry), "\n", $userldapentry->dump();
211
		foreach (keys %$userldapentry) {
429
        foreach (keys %$userldapentry) {
212
			print STDERR "\n\nLDAP key: $_\t", sprintf('(%s)', ref $userldapentry->{$_}), "\n";
430
            print STDERR "\n\nLDAP key: $_\t", sprintf('(%s)', ref $userldapentry->{$_}), "\n";
213
			hashdump("LDAP key: ",$userldapentry->{$_});
431
            hashdump("LDAP key: ",$userldapentry->{$_});
214
		}
432
        }
215
	}
433
    }
216
	my $x = $userldapentry->{attrs} or return;
434
    my $x = $userldapentry->{attrs} or return;
217
	foreach (keys %$x) {
435
    foreach (keys %$x) {
218
		$memberhash{$_} = join ' ', @{$x->{$_}};	
436
        $memberhash{$_} = join ' ', @{$x->{$_}};
219
		$debug and print STDERR sprintf("building \$memberhash{%s} = ", $_, join(' ', @{$x->{$_}})), "\n";
437
        $debug and print STDERR sprintf("building \$memberhash{%s} = ", $_, join(' ', @{$x->{$_}})), "\n";
220
	}
438
    }
221
	$debug and print STDERR "Finsihed \%memberhash has ", scalar(keys %memberhash), " keys\n",
439
    $debug and print STDERR "Finsihed \%memberhash has ", scalar(keys %memberhash), " keys\n",
222
					"Referencing \%mapping with ", scalar(keys %mapping), " keys\n";
440
                    "Referencing \%mapping with ", scalar(keys %mapping), " keys\n";
223
	foreach my $key (keys %mapping) {
441
    foreach my $key (keys %mapping) {
224
		my  $data = $memberhash{ lc($mapping{$key}->{is}) }; # Net::LDAP returns all names in lowercase
442
        my  $data = $memberhash{ lc($mapping{$key}->{is}) }; # Net::LDAP returns all names in lowercase
225
		$debug and printf STDERR "mapping %20s ==> %-20s (%s)\n", $key, $mapping{$key}->{is}, $data;
443
        $debug and printf STDERR "mapping %20s ==> %-20s (%s)\n", $key, $mapping{$key}->{is}, $data;
226
		unless (defined $data) { 
444
        unless (defined $data) {
227
			$data = $mapping{$key}->{content} || '';	# default or failsafe ''
445
            $data = $mapping{$key}->{content} || '';    # default or failsafe ''
228
		}
446
        }
229
		$borrower{$key} = ($data ne '') ? $data : ' ' ;
447
        $borrower{$key} = ($data ne '') ? $data : ' ';
230
	}
448
    }
231
	$borrower{initials} = $memberhash{initials} || 
449
    $borrower{initials} = $memberhash{initials} || 
232
		( substr($borrower{'firstname'},0,1)
450
        ( substr($borrower{'firstname'},0,1)
233
  		. substr($borrower{ 'surname' },0,1)
451
          . substr($borrower{ 'surname' },0,1)
234
  		. " ");
452
          . " ");
235
453
236
	# check if categorycode exists, if not, fallback to default from koha-conf.xml
454
    # check if categorycode exists, if not, fallback to default from koha-conf.xml
237
	my $dbh = C4::Context->dbh;
455
    my $dbh = C4::Context->dbh;
238
	my $sth = $dbh->prepare("SELECT categorycode FROM categories WHERE categorycode = ?");
456
    my $sth = $dbh->prepare("SELECT categorycode FROM categories WHERE categorycode = ?");
239
	$sth->execute( uc($borrower{'categorycode'}) );
457
    $sth->execute( uc($borrower{'categorycode'}) );
240
	unless ( my $row = $sth->fetchrow_hashref ) {
458
    unless ( my $row = $sth->fetchrow_hashref ) {
241
		my $default = $mapping{'categorycode'}->{content};
459
        my $default = $mapping{'categorycode'}->{content};
242
		$debug && warn "Can't find ", $borrower{'categorycode'}, " default to: $default for ", $borrower{userid};
460
        $debug && warn "Can't find ", $borrower{'categorycode'}, " default to: $default for ", $borrower{userid};
243
		$borrower{'categorycode'} = $default
461
        $borrower{'categorycode'} = $default
244
	}
462
    }
245
463
246
	return %borrower;
464
    return %borrower;
247
}
465
}
248
466
249
sub exists_local {
467
sub exists_local {
250
	my $arg = shift;
468
    my $arg    = shift;
251
	my $dbh = C4::Context->dbh;
469
    my $dbh    = C4::Context->dbh;
252
	my $select = "SELECT borrowernumber,cardnumber,userid,password FROM borrowers ";
470
    my $select = "SELECT borrowers.borrowernumber,cardnumber,userid,borrowers.password FROM borrowers ";
253
471
254
	my $sth = $dbh->prepare("$select WHERE userid=?");	# was cardnumber=?
472
    my $sth = $dbh->prepare("$select WHERE userid=?");    # was cardnumber=?
255
	$sth->execute($arg);
473
    $sth->execute($arg);
256
	$debug and printf STDERR "Userid '$arg' exists_local? %s\n", $sth->rows;
474
    $debug and printf STDERR "Userid '$arg' exists_local? %s\n", $sth->rows;
257
	($sth->rows == 1) and return $sth->fetchrow;
475
    ($sth->rows == 1) and return $sth->fetchrow;
258
476
259
	$sth = $dbh->prepare("$select WHERE cardnumber=?");
477
    $sth = $dbh->prepare("$select WHERE cardnumber=?");
260
	$sth->execute($arg);
478
    $sth->execute($arg);
261
	$debug and printf STDERR "Cardnumber '$arg' exists_local? %s\n", $sth->rows;
479
    $debug and printf STDERR "Cardnumber '$arg' exists_local? %s\n", $sth->rows;
262
	($sth->rows == 1) and return $sth->fetchrow;
480
    ($sth->rows == 1) and return $sth->fetchrow;
263
	return 0;
481
482
    $sth = $dbh->prepare("$select JOIN borrower_attributes USING (borrowernumber) WHERE attribute=?");
483
    $sth->execute($arg);
484
    $debug and printf STDERR "attribute '$arg' exists_local? %s\n", $sth->rows;
485
    ($sth->rows == 1) and return $sth->fetchrow;
486
    return 0;
264
}
487
}
265
488
266
sub _do_changepassword {
489
sub _do_changepassword {
Lines 268-307 sub _do_changepassword { Link Here
268
    $debug and print STDERR "changing local password for borrowernumber=$borrowerid to '$digest'\n";
491
    $debug and print STDERR "changing local password for borrowernumber=$borrowerid to '$digest'\n";
269
    changepassword($userid, $borrowerid, $digest);
492
    changepassword($userid, $borrowerid, $digest);
270
493
271
	# Confirm changes
494
    # Confirm changes
272
	my $sth = C4::Context->dbh->prepare("SELECT password,cardnumber FROM borrowers WHERE borrowernumber=? ");
495
    my $sth = C4::Context->dbh->prepare("SELECT password,cardnumber FROM borrowers WHERE borrowernumber=? ");
273
	$sth->execute($borrowerid);
496
    $sth->execute($borrowerid);
274
	if ($sth->rows) {
497
    if ($sth->rows) {
275
		my ($md5password, $cardnum) = $sth->fetchrow;
498
        my ($md5password, $cardnum) = $sth->fetchrow;
276
        ($digest eq $md5password) and return $cardnum;
499
        ($digest eq $md5password) and return $cardnum;
277
		warn "Password mismatch after update to cardnumber=$cardnum (borrowernumber=$borrowerid)";
500
        warn "Password mismatch after update to cardnumber=$cardnum (borrowernumber=$borrowerid)";
278
		return;
501
        return;
279
	}
502
    }
280
	die "Unexpected error after password update to userid/borrowernumber: $userid / $borrowerid.";
503
    die "Unexpected error after password update to userid/borrowernumber: $userid / $borrowerid.";
281
}
504
}
282
505
283
sub update_local {
506
sub update_local {
284
	my   $userid   = shift             or return;
507
    my $userid     = shift             or return;
285
	my   $digest   = md5_base64(shift) or return;
508
    my $digest     = md5_base64(shift) or return;
286
	my $borrowerid = shift             or return;
509
    my $borrowerid = shift             or return;
287
	my $borrower   = shift             or return;
510
    my $borrower   = shift             or return;
288
	my @keys = keys %$borrower;
511
    my @keys       = keys %$borrower;
289
	my $dbh = C4::Context->dbh;
512
    my $dbh        = C4::Context->dbh;
290
	my $query = "UPDATE  borrowers\nSET     " . 
513
    my $query = "
291
		join(',', map {"$_=?"} @keys) .
514
        UPDATE borrowers
292
		"\nWHERE   borrowernumber=? "; 
515
        SET " . join( ',', map { "$_=?" } @keys ) . "
293
	my $sth = $dbh->prepare($query);
516
        WHERE   borrowernumber=?
294
	if ($debug) {
517
    ";
295
		print STDERR $query, "\n",
518
    my $sth = $dbh->prepare($query);
296
			join "\n", map {"$_ = '" . $borrower->{$_} . "'"} @keys;
519
    if ($debug) {
297
		print STDERR "\nuserid = $userid\n";
520
        print STDERR $query, "\n",
298
	}
521
            join "\n", map { "$_ = '" . $borrower->{$_} . "'" } @keys;
299
	$sth->execute(
522
        print STDERR "\nuserid = $userid\n";
300
		((map {$borrower->{$_}} @keys), $borrowerid)
523
    }
301
	);
524
    $sth->execute( ( ( map { $borrower->{$_} } @keys ), $borrowerid ) );
302
525
303
	# MODIFY PASSWORD/LOGIN
526
    # MODIFY PASSWORD/LOGIN
304
	_do_changepassword($userid, $borrowerid, $digest);
527
    _do_changepassword($userid, $borrowerid, $digest);
305
}
528
}
306
529
307
1;
530
1;
Lines 329-408 C4::Auth - Authenticates Koha users Link Here
329
	Make sure that ALL required fields are populated by your LDAP database (and mapped in KOHA_CONF).  
552
	Make sure that ALL required fields are populated by your LDAP database (and mapped in KOHA_CONF).  
330
	What are the required fields?  Well, in mysql you can check the database table "borrowers" like this:
553
	What are the required fields?  Well, in mysql you can check the database table "borrowers" like this:
331
554
332
	mysql> show COLUMNS from borrowers;
555
    mysql> show COLUMNS from borrowers;
333
		+---------------------+--------------+------+-----+---------+----------------+
556
        +---------------------+--------------+------+-----+---------+----------------+
334
		| Field               | Type         | Null | Key | Default | Extra          |
557
        | Field               | Type         | Null | Key | Default | Extra          |
335
		+---------------------+--------------+------+-----+---------+----------------+
558
        +---------------------+--------------+------+-----+---------+----------------+
336
		| borrowernumber      | int(11)      | NO   | PRI | NULL    | auto_increment |
559
        | borrowernumber      | int(11)      | NO   | PRI | NULL    | auto_increment |
337
		| cardnumber          | varchar(16)  | YES  | UNI | NULL    |                |
560
        | cardnumber          | varchar(16)  | YES  | UNI | NULL    |                |
338
		| surname             | mediumtext   | NO   |     | NULL    |                |
561
        | surname             | mediumtext   | NO   |     | NULL    |                |
339
		| firstname           | text         | YES  |     | NULL    |                |
562
        | firstname           | text         | YES  |     | NULL    |                |
340
		| title               | mediumtext   | YES  |     | NULL    |                |
563
        | title               | mediumtext   | YES  |     | NULL    |                |
341
		| othernames          | mediumtext   | YES  |     | NULL    |                |
564
        | othernames          | mediumtext   | YES  |     | NULL    |                |
342
		| initials            | text         | YES  |     | NULL    |                |
565
        | initials            | text         | YES  |     | NULL    |                |
343
		| streetnumber        | varchar(10)  | YES  |     | NULL    |                |
566
        | streetnumber        | varchar(10)  | YES  |     | NULL    |                |
344
		| streettype          | varchar(50)  | YES  |     | NULL    |                |
567
        | streettype          | varchar(50)  | YES  |     | NULL    |                |
345
		| address             | mediumtext   | NO   |     | NULL    |                |
568
        | address             | mediumtext   | NO   |     | NULL    |                |
346
		| address2            | text         | YES  |     | NULL    |                |
569
        | address2            | text         | YES  |     | NULL    |                |
347
		| city                | mediumtext   | NO   |     | NULL    |                |
570
        | city                | mediumtext   | NO   |     | NULL    |                |
348
		| state               | mediumtext   | YES  |     | NULL    |                |
571
        | state               | text         | YES  |     | NULL    |                |
349
		| zipcode             | varchar(25)  | YES  |     | NULL    |                |
572
        | zipcode             | varchar(25)  | YES  |     | NULL    |                |
350
		| country             | text         | YES  |     | NULL    |                |
573
        | country             | text         | YES  |     | NULL    |                |
351
		| email               | mediumtext   | YES  |     | NULL    |                |
574
        | email               | mediumtext   | YES  |     | NULL    |                |
352
		| phone               | text         | YES  |     | NULL    |                |
575
        | phone               | text         | YES  |     | NULL    |                |
353
		| mobile              | varchar(50)  | YES  |     | NULL    |                |
576
        | mobile              | varchar(50)  | YES  |     | NULL    |                |
354
		| fax                 | mediumtext   | YES  |     | NULL    |                |
577
        | fax                 | mediumtext   | YES  |     | NULL    |                |
355
		| emailpro            | text         | YES  |     | NULL    |                |
578
        | emailpro            | text         | YES  |     | NULL    |                |
356
		| phonepro            | text         | YES  |     | NULL    |                |
579
        | phonepro            | text         | YES  |     | NULL    |                |
357
		| B_streetnumber      | varchar(10)  | YES  |     | NULL    |                |
580
        | B_streetnumber      | varchar(10)  | YES  |     | NULL    |                |
358
		| B_streettype        | varchar(50)  | YES  |     | NULL    |                |
581
        | B_streettype        | varchar(50)  | YES  |     | NULL    |                |
359
		| B_address           | varchar(100) | YES  |     | NULL    |                |
582
        | B_address           | varchar(100) | YES  |     | NULL    |                |
360
		| B_address2          | text         | YES  |     | NULL    |                |
583
        | B_address2          | text         | YES  |     | NULL    |                |
361
		| B_city              | mediumtext   | YES  |     | NULL    |                |
584
        | B_city              | mediumtext   | YES  |     | NULL    |                |
362
		| B_state             | mediumtext   | YES  |     | NULL    |                |
585
        | B_state             | text         | YES  |     | NULL    |                |
363
		| B_zipcode           | varchar(25)  | YES  |     | NULL    |                |
586
        | B_zipcode           | varchar(25)  | YES  |     | NULL    |                |
364
		| B_country           | text         | YES  |     | NULL    |                |
587
        | B_country           | text         | YES  |     | NULL    |                |
365
		| B_email             | text         | YES  |     | NULL    |                |
588
        | B_email             | text         | YES  |     | NULL    |                |
366
		| B_phone             | mediumtext   | YES  |     | NULL    |                |
589
        | B_phone             | mediumtext   | YES  |     | NULL    |                |
367
		| dateofbirth         | date         | YES  |     | NULL    |                |
590
        | dateofbirth         | date         | YES  |     | NULL    |                |
368
		| branchcode          | varchar(10)  | NO   | MUL |         |                |
591
        | branchcode          | varchar(10)  | NO   | MUL |         |                |
369
		| categorycode        | varchar(10)  | NO   | MUL |         |                |
592
        | categorycode        | varchar(10)  | NO   | MUL |         |                |
370
		| dateenrolled        | date         | YES  |     | NULL    |                |
593
        | dateenrolled        | date         | YES  |     | NULL    |                |
371
		| dateexpiry          | date         | YES  |     | NULL    |                |
594
        | dateexpiry          | date         | YES  |     | NULL    |                |
372
		| gonenoaddress       | tinyint(1)   | YES  |     | NULL    |                |
595
        | gonenoaddress       | tinyint(1)   | YES  |     | NULL    |                |
373
		| lost                | tinyint(1)   | YES  |     | NULL    |                |
596
        | lost                | tinyint(1)   | YES  |     | NULL    |                |
374
		| debarred            | date         | YES  |     | NULL    |                |
597
        | debarred            | date         | YES  |     | NULL    |                |
375
		| debarredcomment     | varchar(255) | YES  |     | NULL    |                |
598
        | debarredcomment     | varchar(255) | YES  |     | NULL    |                |
376
		| contactname         | mediumtext   | YES  |     | NULL    |                |
599
        | contactname         | mediumtext   | YES  |     | NULL    |                |
377
		| contactfirstname    | text         | YES  |     | NULL    |                |
600
        | contactfirstname    | text         | YES  |     | NULL    |                |
378
		| contacttitle        | text         | YES  |     | NULL    |                |
601
        | contacttitle        | text         | YES  |     | NULL    |                |
379
		| guarantorid         | int(11)      | YES  | MUL | NULL    |                |
602
        | guarantorid         | int(11)      | YES  | MUL | NULL    |                |
380
		| borrowernotes       | mediumtext   | YES  |     | NULL    |                |
603
        | borrowernotes       | mediumtext   | YES  |     | NULL    |                |
381
		| relationship        | varchar(100) | YES  |     | NULL    |                |
604
        | relationship        | varchar(100) | YES  |     | NULL    |                |
382
		| ethnicity           | varchar(50)  | YES  |     | NULL    |                |
605
        | ethnicity           | varchar(50)  | YES  |     | NULL    |                |
383
		| ethnotes            | varchar(255) | YES  |     | NULL    |                |
606
        | ethnotes            | varchar(255) | YES  |     | NULL    |                |
384
		| sex                 | varchar(1)   | YES  |     | NULL    |                |
607
        | sex                 | varchar(1)   | YES  |     | NULL    |                |
385
		| password            | varchar(30)  | YES  |     | NULL    |                |
608
        | password            | varchar(30)  | YES  |     | NULL    |                |
386
		| flags               | int(11)      | YES  |     | NULL    |                |
609
        | flags               | int(11)      | YES  |     | NULL    |                |
387
		| userid              | varchar(30)  | YES  | MUL | NULL    |                |
610
        | userid              | varchar(75)  | YES  | MUL | NULL    |                |
388
		| opacnote            | mediumtext   | YES  |     | NULL    |                |
611
        | opacnote            | mediumtext   | YES  |     | NULL    |                |
389
		| contactnote         | varchar(255) | YES  |     | NULL    |                |
612
        | contactnote         | varchar(255) | YES  |     | NULL    |                |
390
		| sort1               | varchar(80)  | YES  |     | NULL    |                |
613
        | sort1               | varchar(80)  | YES  |     | NULL    |                |
391
		| sort2               | varchar(80)  | YES  |     | NULL    |                |
614
        | sort2               | varchar(80)  | YES  |     | NULL    |                |
392
		| altcontactfirstname | varchar(255) | YES  |     | NULL    |                |
615
        | altcontactfirstname | varchar(255) | YES  |     | NULL    |                |
393
		| altcontactsurname   | varchar(255) | YES  |     | NULL    |                |
616
        | altcontactsurname   | varchar(255) | YES  |     | NULL    |                |
394
		| altcontactaddress1  | varchar(255) | YES  |     | NULL    |                |
617
        | altcontactaddress1  | varchar(255) | YES  |     | NULL    |                |
395
		| altcontactaddress2  | varchar(255) | YES  |     | NULL    |                |
618
        | altcontactaddress2  | varchar(255) | YES  |     | NULL    |                |
396
		| altcontactaddress3  | varchar(255) | YES  |     | NULL    |                |
619
        | altcontactaddress3  | varchar(255) | YES  |     | NULL    |                |
397
		| altcontactstate     | mediumtext   | YES  |     | NULL    |                |
620
        | altcontactstate     | text         | YES  |     | NULL    |                |
398
		| altcontactzipcode   | varchar(50)  | YES  |     | NULL    |                |
621
        | altcontactzipcode   | varchar(50)  | YES  |     | NULL    |                |
399
		| altcontactcountry   | text         | YES  |     | NULL    |                |
622
        | altcontactcountry   | text         | YES  |     | NULL    |                |
400
		| altcontactphone     | varchar(50)  | YES  |     | NULL    |                |
623
        | altcontactphone     | varchar(50)  | YES  |     | NULL    |                |
401
		| smsalertnumber      | varchar(50)  | YES  |     | NULL    |                |
624
        | smsalertnumber      | varchar(50)  | YES  |     | NULL    |                |
402
		| privacy             | int(11)      | NO   |     | 1       |                |
625
        | privacy             | int(11)      | NO   |     | 1       |                |
403
		+---------------------+--------------+------+-----+---------+----------------+
626
        +---------------------+--------------+------+-----+---------+----------------+
404
		66 rows in set (0.00 sec)
627
        67 rows in set (0.00 sec)
405
		Where Null="NO", the field is required.
628
        Where Null="NO", the field is required.
406
629
407
=head1 KOHA_CONF and field mapping
630
=head1 KOHA_CONF and field mapping
408
631
(-)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