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