1# BEGIN BPS TAGGED BLOCK {{{
2#
3# COPYRIGHT:
4#
5# This software is Copyright (c) 1996-2021 Best Practical Solutions, LLC
6#                                          <sales@bestpractical.com>
7#
8# (Except where explicitly superseded by other copyright notices)
9#
10#
11# LICENSE:
12#
13# This work is made available to you under the terms of Version 2 of
14# the GNU General Public License. A copy of that license should have
15# been provided with this software, but in any event can be snarfed
16# from www.gnu.org.
17#
18# This work is distributed in the hope that it will be useful, but
19# WITHOUT ANY WARRANTY; without even the implied warranty of
20# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21# General Public License for more details.
22#
23# You should have received a copy of the GNU General Public License
24# along with this program; if not, write to the Free Software
25# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26# 02110-1301 or visit their web page on the internet at
27# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28#
29#
30# CONTRIBUTION SUBMISSION POLICY:
31#
32# (The following paragraph is not intended to limit the rights granted
33# to you to modify and distribute this software under the terms of
34# the GNU General Public License and is only of importance to you if
35# you choose to contribute your changes and enhancements to the
36# community by submitting them to Best Practical Solutions, LLC.)
37#
38# By intentionally submitting any modifications, corrections or
39# derivatives to this work, or any other work intended for use with
40# Request Tracker, to Best Practical Solutions, LLC, you confirm that
41# you are the copyright holder for those contributions and you grant
42# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43# royalty-free, perpetual, license to use, copy, create derivative
44# works based on those contributions, and sublicense and distribute
45# those contributions and any derivatives thereof.
46#
47# END BPS TAGGED BLOCK }}}
48
49package RT::Authen::ExternalAuth::LDAP;
50
51use Net::LDAP qw(LDAP_SUCCESS LDAP_PARTIAL_RESULTS);
52use Net::LDAP::Util qw(ldap_error_name escape_filter_value);
53use Net::LDAP::Filter;
54
55use warnings;
56use strict;
57
58=head1 NAME
59
60RT::Authen::ExternalAuth::LDAP - LDAP source for RT authentication
61
62=head1 DESCRIPTION
63
64Provides the LDAP implementation for L<RT::Authen::ExternalAuth>.
65
66=head1 SYNOPSIS
67
68    Set($ExternalSettings, {
69        # AN EXAMPLE LDAP SERVICE
70        'My_LDAP'       =>  {
71            'type'                      =>  'ldap',
72
73            'server'                    =>  'server.domain.tld',
74            'user'                      =>  'rt_ldap_username',
75            'pass'                      =>  'rt_ldap_password',
76
77            'base'                      =>  'ou=Organisational Unit,dc=domain,dc=TLD',
78            'filter'                    =>  '(FILTER_STRING)',
79            'd_filter'                  =>  '(FILTER_STRING)',
80
81            'group'                     =>  'GROUP_NAME',
82            'group_attr'                =>  'GROUP_ATTR',
83
84            'tls'                       =>  { verify => "require", capath => "/path/to/ca.pem" },
85
86            'net_ldap_args'             => [    version =>  3   ],
87
88            'attr_match_list' => [
89                'Name',
90                'EmailAddress',
91            ],
92            'attr_map' => {
93                'Name' => 'sAMAccountName',
94                'EmailAddress' => 'mail',
95                'Organization' => 'physicalDeliveryOfficeName',
96                'RealName' => 'cn',
97                'Gecos' => 'sAMAccountName',
98                'WorkPhone' => 'telephoneNumber',
99                'Address1' => 'streetAddress',
100                'City' => 'l',
101                'State' => 'st',
102                'Zip' => 'postalCode',
103                'Country' => 'co'
104            },
105        },
106    } );
107
108=head1 CONFIGURATION
109
110LDAP-specific options are described here. Shared options
111are described in L<RT::Authen::ExternalAuth>.
112
113The example in the L</SYNOPSIS> lists all available options
114and they are described below. Note that many of these values
115are specific to LDAP, so you should consult your LDAP
116documentation for details.
117
118=over 4
119
120=item server
121
122The server hosting the LDAP or AD service.
123
124=item user, pass
125
126The username and password RT should use to connect to the LDAP
127server.
128
129If you can bind to your LDAP server anonymously you may be able to omit these
130options.  Many servers do not allow anonymous binds, or restrict what information
131they can see or how much information they can retrieve.  If your server does not
132allow anonymous binds then you must have a service account created for this
133component to function.
134
135=item base
136
137The LDAP search base.
138
139=item filter
140
141The filter to use to match RT users. You B<must> specify it
142and it B<must> be a valid LDAP filter encased in parentheses.
143
144For example:
145
146    filter => '(objectClass=*)',
147
148=item d_filter
149
150The filter that will only match disabled users. Optional.
151B<Must> be a valid LDAP filter encased in parentheses.
152
153For example with Active Directory the following can be used:
154
155    d_filter => '(userAccountControl:1.2.840.113556.1.4.803:=2)'
156
157=item group
158
159Does authentication depend on group membership? What group name?
160
161=item group_attr
162
163What is the attribute for the group object that determines membership?
164
165=item group_scope
166
167What is the scope of the group search? C<base>, C<one> or C<sub>.
168Optional; defaults to C<base>, which is good enough for most cases.
169C<sub> is appropriate when you have nested groups.
170
171=item group_attr_value
172
173What is the attribute of the user entry that should be matched against
174group_attr above? Optional; defaults to C<dn>.
175
176=item tls
177
178Should we try to use TLS to encrypt connections?  Either a scalar, for
179simple enabling, or a hash of values to pass to L<Net::LDAP/start_tls>.
180By default, L<Net::LDAP> does B<no> certificate validation!  To validate
181certificates, pass:
182
183    tls => { verify => 'require',
184             cafile => "/etc/ssl/certs/ca.pem",  # Path CA file
185           },
186
187=item net_ldap_args
188
189What other args should be passed to Net::LDAP->new($host,@args)?
190
191=back
192
193=cut
194
195sub GetAuth {
196
197    my ($service, $username, $password) = @_;
198
199    my $config = RT->Config->Get('ExternalSettings')->{$service};
200    $RT::Logger->debug( "Trying external auth service:",$service);
201
202    my $base            = $config->{'base'};
203    my $filter          = $config->{'filter'};
204    my $group           = $config->{'group'};
205    my $group_attr      = $config->{'group_attr'};
206    my $group_attr_val  = $config->{'group_attr_value'} || 'dn';
207    my $group_scope     = $config->{'group_scope'} || 'base';
208    my $attr_map        = $config->{'attr_map'};
209    my @attrs           = ('dn');
210    my $attr_match_list = $config->{'attr_match_list'};
211
212    # Make sure we fetch the user attribute we'll need for the group check
213    push @attrs, $group_attr_val
214        unless lc $group_attr_val eq 'dn';
215
216    # Empty parentheses as filters cause Net::LDAP to barf.
217    # We take care of this by using Net::LDAP::Filter, but
218    # there's no harm in fixing this right now.
219    undef $filter if defined $filter and $filter eq "()";
220
221    # Now let's get connected
222    my $ldap = _GetBoundLdapObj($config);
223    return 0 unless ($ldap);
224
225    # loop over each of the attr_match_list members for LDAP search
226    my $ldap_msg;
227    foreach my $attr_match ( @{$attr_match_list} ) {
228        unless ( defined $attr_map->{$attr_match} ) {
229            $RT::Logger->error( "Invalid LDAP mapping for $attr_match, no defined fields in attr_map" );
230            next;
231        }
232
233        my $search_filter = Net::LDAP::Filter->new(
234            '(&' .
235            $filter .
236            '(' .
237            $attr_map->{$attr_match} .
238            '=' .
239            escape_filter_value($username) .
240            '))'
241        );
242
243        $RT::Logger->debug( "LDAP Search === ",
244                            "Base:",
245                            $base,
246                            "== Filter:",
247                            $search_filter->as_string,
248                            "== Attrs:",
249                            join(',',@attrs) );
250
251        $ldap_msg = $ldap->search( base   => $base,
252                                   filter => $search_filter,
253                                   attrs  => \@attrs );
254
255        unless ( $ldap_msg->code == LDAP_SUCCESS || $ldap_msg->code == LDAP_PARTIAL_RESULTS ) {
256            $RT::Logger->critical( "search for",
257                                   $search_filter->as_string,
258                                   "failed:",
259                                   ldap_error_name($ldap_msg->code),
260                                   $ldap_msg->code );
261            # Didn't even get a partial result - jump straight to the next external auth service
262            return 0;
263        }
264
265        if ( $ldap_msg->count != 1 ) {
266            $RT::Logger->info( $service,
267                               "AUTH FAILED:",
268                               $username,
269                               "User not found or more than one user found" );
270            # We got no user, or too many users.. try the next attr_match_list field.
271            next;
272        }
273        else {
274            # User was found
275            last;
276        }
277    }
278
279    # if we didn't match anything, go to the next external auth service
280    return 0 unless $ldap_msg->first_entry;
281
282    my $ldap_entry = $ldap_msg->first_entry;
283    my $ldap_dn    = $ldap_entry->dn;
284
285    $RT::Logger->debug( "Found LDAP DN:",
286                        $ldap_dn);
287
288    # THIS bind determines success or failure on the password.
289    $ldap_msg = $ldap->bind($ldap_dn, password => $password);
290
291    unless ($ldap_msg->code == LDAP_SUCCESS) {
292        $RT::Logger->info(  $service,
293                            "AUTH FAILED",
294                            $username,
295                            "(can't bind:",
296                            ldap_error_name($ldap_msg->code),
297                            $ldap_msg->code,
298                            ")");
299        # Could not bind to the LDAP server as the user we found with the password
300        # we were given, therefore the password must be wrong so we fail and
301        # jump straight to the next external auth service
302        return 0;
303    }
304
305    # The user is authenticated ok, but is there an LDAP Group to check?
306    if ($group) {
307        my $group_val = lc $group_attr_val eq 'dn'
308                            ? $ldap_dn
309                            : $ldap_entry->get_value($group_attr_val);
310
311        # Fallback to the DN if the user record doesn't have a value
312        unless (defined $group_val) {
313            $group_val = $ldap_dn;
314            $RT::Logger->debug("Attribute '$group_attr_val' has no value; falling back to '$group_val'");
315        }
316
317        # We only need the dn for the actual group since all we care about is existence
318        @attrs  = qw(dn);
319        my $search_filter = Net::LDAP::Filter->new("(${group_attr}=" . escape_filter_value($group_val) . ")");
320
321        $RT::Logger->debug( "LDAP Search === ",
322                            "Base:",
323                            $group,
324                            "== Scope:",
325                            $group_scope,
326                            "== Filter:",
327                            $search_filter->as_string,
328                            "== Attrs:",
329                            join(',',@attrs));
330
331        $ldap_msg = $ldap->search(  base   => $group,
332                                    filter => $search_filter,
333                                    attrs  => \@attrs,
334                                    scope  => $group_scope);
335
336        # And the user isn't a member:
337        unless ($ldap_msg->code == LDAP_SUCCESS ||
338                $ldap_msg->code == LDAP_PARTIAL_RESULTS) {
339            $RT::Logger->critical(  "Search for",
340                                    $search_filter->as_string,
341                                    "failed:",
342                                    ldap_error_name($ldap_msg->code),
343                                    $ldap_msg->code);
344
345            # Fail auth - jump to next external auth service
346            return 0;
347        }
348
349        unless ($ldap_msg->count == 1) {
350            $RT::Logger->debug(
351                "LDAP group membership check returned",
352                $ldap_msg->count, "results"
353            );
354            $RT::Logger->info(  $service,
355                                "AUTH FAILED:",
356                                $username);
357
358            # Fail auth - jump to next external auth service
359            return 0;
360        }
361    }
362
363    # Any other checks you want to add? Add them here.
364
365    # If we've survived to this point, we're good.
366    $RT::Logger->info(  (caller(0))[3],
367                        "External Auth OK (",
368                        $service,
369                        "):",
370                        $username);
371    return 1;
372
373}
374
375
376sub CanonicalizeUserInfo {
377
378    my ($service, $key, $value) = @_;
379
380    my $found = 0;
381    my %params = (Name         => undef,
382                  EmailAddress => undef,
383                  RealName     => undef);
384
385    # Load the config
386    my $config = RT->Config->Get('ExternalSettings')->{$service};
387
388    # Figure out what's what
389    my $base            = $config->{'base'};
390    my $filter          = $config->{'filter'};
391
392    # Get the list of unique attrs we need
393    my @attrs;
394    for my $field ( values %{ $config->{'attr_map'} } ) {
395        if ( ref $field eq 'CODE' ) {
396            push @attrs, $field->();
397        }
398        elsif ( ref $field eq 'ARRAY' ) {
399            push @attrs, @$field;
400        }
401        else {
402            push @attrs, $field;
403        }
404    }
405
406    # This is a bit confusing and probably broken. Something to revisit..
407    my $filter_addition = ($key && $value) ? "(". $key . "=". escape_filter_value($value) .")" : "";
408    if(defined($filter) && ($filter ne "()")) {
409        $filter = Net::LDAP::Filter->new(   "(&" .
410                                            $filter .
411                                            $filter_addition .
412                                            ")"
413                                        );
414    } else {
415        $RT::Logger->debug( "LDAP Filter invalid or not present.");
416    }
417
418    unless (defined($base)) {
419        $RT::Logger->critical(  (caller(0))[3],
420                                "LDAP baseDN not defined");
421        # Drop out to the next external information service
422        return ($found, %params);
423    }
424
425    # Get a Net::LDAP object based on the config we provide
426    my $ldap = _GetBoundLdapObj($config);
427
428    # Jump to the next external information service if we can't get one,
429    # errors should be logged by _GetBoundLdapObj so we don't have to.
430    return ($found, %params) unless ($ldap);
431
432    # Do a search for them in LDAP
433    $RT::Logger->debug( "LDAP Search === ",
434                        "Base:",
435                        $base,
436                        "== Filter:",
437                        $filter->as_string,
438                        "== Attrs:",
439                        join(',',@attrs));
440
441    my $ldap_msg = $ldap->search(base   => $base,
442                                 filter => $filter,
443                                 attrs  => \@attrs);
444
445    # If we didn't get at LEAST a partial result, just die now.
446    if ($ldap_msg->code != LDAP_SUCCESS and
447        $ldap_msg->code != LDAP_PARTIAL_RESULTS) {
448        $RT::Logger->critical(  (caller(0))[3],
449                                ": Search for ",
450                                $filter->as_string,
451                                " failed: ",
452                                ldap_error_name($ldap_msg->code),
453                                $ldap_msg->code);
454        # $found remains as 0
455
456        # Drop out to the next external information service
457        $ldap_msg = $ldap->unbind();
458        if ($ldap_msg->code != LDAP_SUCCESS) {
459            $RT::Logger->critical(  (caller(0))[3],
460                                    ": Could not unbind: ",
461                                    ldap_error_name($ldap_msg->code),
462                                    $ldap_msg->code);
463        }
464        undef $ldap;
465        undef $ldap_msg;
466        return ($found, %params);
467
468    } else {
469        # If there's only one match, we're good; more than one and
470        # we don't know which is the right one so we skip it.
471        if ($ldap_msg->count == 1) {
472            RT->Logger->debug("Found one matching record");
473            my $entry = $ldap_msg->first_entry();
474            foreach my $key (keys(%{$config->{'attr_map'}})) {
475                # XXX TODO: This legacy code wants to be removed since modern
476                # configs will always fall through to the else and the logic is
477                # weird even if you do have the old config.
478                if ($RT::LdapAttrMap and $RT::LdapAttrMap->{$key} eq 'dn') {
479                    $params{$key} = $entry->dn();
480                }
481                else {
482
483                    my $external_field = $config->{'attr_map'}{$key};
484                    my @list = grep defined && length, ref $external_field eq 'ARRAY' ? @$external_field : ($external_field);
485                    unless (@list) {
486                        $RT::Logger->error("Invalid LDAP mapping for $key, no defined fields");
487                        next;
488                    }
489
490                    my @values;
491                    foreach my $e (@list) {
492                        if ( ref $e eq 'CODE' ) {
493                            push @values,
494                              $e->(
495                                external_entry => $entry,
496                                mapping        => $config->{'attr_map'},
497                                rt_field       => $key,
498                                external_field => $external_field,
499                              );
500                        }
501                        elsif ( ref $e ) {
502                            $RT::Logger->error("Invalid type of LDAP mapping for $key, value is $e");
503                            next;
504                        }
505                        else {
506                            push @values, $entry->get_value( $e, asref => 1 );
507                        }
508                    }
509
510                    # Use the first value if multiple values are set in ldap
511                    @values = map { ref $_ eq 'ARRAY' ? $_->[0] : $_ } grep defined, @values;
512                    $params{$key} = join ' ', grep defined && length, @values;
513                }
514            }
515            $found = 1;
516        } else {
517            # Drop out to the next external information service
518            RT->Logger->debug("Found " . $ldap_msg->count . " records. Need a single matching record"
519                . " to populate user data, so continuing with other configured auth services.");
520            $ldap_msg = $ldap->unbind();
521            if ($ldap_msg->code != LDAP_SUCCESS) {
522                $RT::Logger->critical(  (caller(0))[3],
523                                        ": Could not unbind: ",
524                                        ldap_error_name($ldap_msg->code),
525                                        $ldap_msg->code);
526            }
527            undef $ldap;
528            undef $ldap_msg;
529            return ($found, %params);
530        }
531    }
532    $ldap_msg = $ldap->unbind();
533    if ($ldap_msg->code != LDAP_SUCCESS) {
534        $RT::Logger->critical(  (caller(0))[3],
535                                ": Could not unbind: ",
536                                ldap_error_name($ldap_msg->code),
537                                $ldap_msg->code);
538    }
539
540    undef $ldap;
541    undef $ldap_msg;
542
543    return ($found, %params);
544}
545
546sub UserExists {
547    my ($username,$service) = @_;
548   $RT::Logger->debug("UserExists params:\nusername: $username , service: $service");
549    my $config              = RT->Config->Get('ExternalSettings')->{$service};
550
551    my $base                = $config->{'base'};
552    my $filter              = $config->{'filter'};
553
554    # While LDAP filters must be surrounded by parentheses, an empty set
555    # of parentheses is an invalid filter and will cause failure
556    # This shouldn't matter since we are now using Net::LDAP::Filter below,
557    # but there's no harm in doing this to be sure
558    undef $filter if defined $filter and $filter eq "()";
559
560    my $attr_map        = $config->{'attr_map'};
561    my $attr_match_list = $config->{'attr_match_list'};
562
563    my @attrs;
564    foreach my $attr_match (@{$attr_match_list}) {
565        push @attrs, $attr_map->{$attr_match}
566            if defined $attr_map->{$attr_match};
567    }
568
569    # Ensure we try to get back a Name value from LDAP on the initial LDAP search.
570    push @attrs, $attr_map->{'Name'};
571
572    my $ldap = _GetBoundLdapObj($config);
573    return unless $ldap;
574
575    # loop over each of the attr_match_list members for the initial lookup
576    foreach my $attr_match ( @{$attr_match_list} ) {
577        unless ( defined $attr_map->{$attr_match} ) {
578            $RT::Logger->error( "Invalid LDAP mapping for $attr_match, no defined fields in attr_map" );
579            next;
580        }
581
582        my $search_filter = Net::LDAP::Filter->new(
583            '(&' .
584            $filter .
585            '(' .
586            $attr_map->{$attr_match} .
587            '=' .
588            escape_filter_value($username) .
589            '))'
590        );
591
592        # Check that the user exists in the LDAP service
593        $RT::Logger->debug( "LDAP Search === ",
594                            "Base:",
595                            $base,
596                            "== Filter:",
597                            ($search_filter ? $search_filter->as_string : ''),
598                            "== Attrs:",
599                            join(',',@attrs) );
600
601        my $user_found = $ldap->search( base   => $base,
602                                        filter => $search_filter,
603                                        attrs  => \@attrs );
604
605        unless ( $user_found->code == LDAP_SUCCESS || $user_found->code == LDAP_PARTIAL_RESULTS ) {
606            $RT::Logger->debug( "search for",
607                                $filter->as_string,
608                                "failed:",
609                                ldap_error_name($user_found->code),
610                                $user_found->code );
611            # Didn't even get a partial result - jump straight to the next external auth service
612            return 0;
613        }
614
615        if ( $user_found->count < 1 ) {
616            # If 0 or negative integer, no user found or major failure
617            $RT::Logger->debug( "User Check Failed :: (",
618                                $service,
619                                ")",
620                                $username,
621                                "User not found" );
622            next;
623        }
624        elsif ( $user_found->count > 1 ) {
625            # If more than one result returned, jump to the next attr because the username field should be unique!
626            $RT::Logger->debug( "User Check Failed :: (",
627                                $service,
628                                ")",
629                                $username,
630                                "More than one user with that username!" );
631            next;
632        }
633        else {
634            # User was found in LDAP
635            return $attr_match;
636        }
637    }
638
639    # No valid user was found using each of the search filters.
640    # go to the next external auth service.
641    return 0;
642}
643
644sub UserDisabled {
645
646    my ($username,$service) = @_;
647
648    # FIRST, check that the user exists in the LDAP service
649    my $field = UserExists( $username, $service );
650
651    unless($field) {
652        $RT::Logger->debug("User (",$username,") doesn't exist! - Assuming not disabled for the purposes of disable checking");
653        return 0;
654    }
655
656    my $config          = RT->Config->Get('ExternalSettings')->{$service};
657    my $base            = $config->{'base'};
658    my $filter          = $config->{'filter'};
659    my $d_filter        = $config->{'d_filter'};
660    my $search_filter;
661
662    # While LDAP filters must be surrounded by parentheses, an empty set
663    # of parentheses is an invalid filter and will cause failure
664    # This shouldn't matter since we are now using Net::LDAP::Filter below,
665    # but there's no harm in doing this to be sure
666    undef $filter   if defined $filter   and $filter eq "()";
667    undef $d_filter if defined $d_filter and $d_filter eq "()";
668
669    unless ($d_filter) {
670        # If we don't know how to check for disabled users, consider them all enabled.
671        $RT::Logger->debug("No d_filter specified for this LDAP service (",
672                            $service,
673                            "), so considering all users enabled");
674        return 0;
675    }
676
677    if (defined($config->{'attr_map'}->{$field})) {
678        # Construct the complex filter
679        $search_filter = Net::LDAP::Filter->new(   '(&' .
680                                                    $filter .
681                                                    $d_filter .
682                                                    '(' .
683                                                    $config->{'attr_map'}->{$field} .
684                                                    '=' .
685                                                    escape_filter_value($username) .
686                                                    '))'
687                                                );
688    } else {
689        $RT::Logger->debug("You haven't specified an LDAP attribute to match the RT \"$field\" attribute for this service (",
690                            $service,
691                            "), so it's impossible look up the disabled status of this user (",
692                            $username,
693                            ") so I'm just going to assume the user is not disabled");
694        return 0;
695
696    }
697
698    my $ldap = _GetBoundLdapObj($config);
699    next unless $ldap;
700
701    # We only need the UID for confirmation now,
702    # the other information would waste time and bandwidth
703    my @attrs = ('uid');
704
705    $RT::Logger->debug( "LDAP Search === ",
706                        "Base:",
707                        $base,
708                        "== Filter:",
709                        ($search_filter ? $search_filter->as_string : ''),
710                        "== Attrs:",
711                        join(',',@attrs));
712
713    my $disabled_users = $ldap->search(base   => $base,
714                                       filter => $search_filter,
715                                       attrs  => \@attrs);
716    # If ANY results are returned,
717    # we are going to assume the user should be disabled
718    if ($disabled_users->count) {
719        undef $disabled_users;
720        return 1;
721    } else {
722        undef $disabled_users;
723        return 0;
724    }
725}
726# {{{ sub _GetBoundLdapObj
727
728sub _GetBoundLdapObj {
729
730    # Config as hashref
731    my $config = shift;
732
733    # Figure out what's what
734    my $ldap_server     = $config->{'server'};
735    my $ldap_user       = $config->{'user'};
736    my $ldap_pass       = $config->{'pass'};
737    my $ldap_tls        = $config->{'tls'};
738    $ldap_tls = $ldap_tls ? {} : undef unless ref $ldap_tls;
739    my $ldap_args       = $config->{'net_ldap_args'};
740
741    my $ldap = new Net::LDAP($ldap_server, @$ldap_args);
742
743    unless ($ldap) {
744        $RT::Logger->critical(  (caller(0))[3],
745                                ": Cannot connect to",
746                                $ldap_server);
747        return undef;
748    }
749
750    if ($ldap_tls) {
751        # Thanks to David Narayan for the fault tolerance bits
752        eval { $ldap->start_tls( %{$ldap_tls} ); };
753        if ($@) {
754            $RT::Logger->critical(  (caller(0))[3],
755                                    "Can't start TLS: ",
756                                    $@);
757            return;
758        }
759
760    }
761
762    my $msg = undef;
763
764    if (($ldap_user) and ($ldap_pass)) {
765        $msg = $ldap->bind($ldap_user, password => $ldap_pass);
766    } elsif (($ldap_user) and ( ! $ldap_pass)) {
767        $msg = $ldap->bind($ldap_user);
768    } else {
769        $msg = $ldap->bind;
770    }
771
772    unless ($msg->code == LDAP_SUCCESS) {
773        $RT::Logger->critical(  (caller(0))[3],
774                                "Can't bind:",
775                                ldap_error_name($msg->code),
776                                $msg->code);
777        return undef;
778    } else {
779        return $ldap;
780    }
781}
782
783# }}}
784
785RT::Base->_ImportOverlays();
786
7871;
788