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

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