1# Connector::Proxy::Net::LDAP 2# 3# Proxy class for accessing LDAP directories 4# 5# Written by Scott Hardin, Martin Bartosch and Oliver Welter for the OpenXPKI project 2012 6# 7package Connector::Proxy::Net::LDAP; 8 9use strict; 10use warnings; 11use English; 12use Net::LDAP; 13use Template; 14use Data::Dumper; 15 16use Moose; 17extends 'Connector::Proxy'; 18 19has base => ( 20 is => 'rw', 21 isa => 'Str', 22 required => 1, 23); 24 25has binddn => ( 26 is => 'rw', 27 isa => 'Str', 28); 29 30has password => ( 31 is => 'rw', 32 isa => 'Str', 33); 34 35has filter => ( 36 is => 'rw', 37 # TODO: this does not work (currently); NB: do we need that? 38# isa => 'Str|Net::LDAP::Filter', 39 isa => 'Str', 40 required => 1, 41); 42 43has attrs => ( 44 is => 'rw', 45 isa => 'ArrayRef|Str', 46 trigger => \&_convert_attrs 47); 48 49has scope => ( 50 is => 'rw', 51 isa => 'Str', 52); 53 54has timeout => ( 55 is => 'rw', 56 isa => 'Int', 57); 58 59has keepalive => ( 60 is => 'rw', 61 isa => 'Int', 62); 63 64has timelimit => ( 65 is => 'rw', 66 isa => 'Int', 67); 68 69has sizelimit => ( 70 is => 'rw', 71 isa => 'Int', 72); 73 74has multihomed => ( 75 is => 'rw', 76 isa => 'Int', 77); 78 79has localaddr => ( 80 is => 'rw', 81 isa => 'Str', 82); 83 84has raw => ( 85 is => 'rw', 86 isa => 'RegexpRef|Undef', 87 default => sub { return qr/(?i:;binary)/; }, 88); 89 90has debug => ( 91 is => 'rw', 92 isa => 'Int', 93 default => 0, 94); 95 96# ssl certificate options in SSLUserAgent format 97has certificate_file => ( 98 is => 'rw', 99 isa => 'Str', 100); 101 102has certificate_key_file => ( 103 is => 'rw', 104 isa => 'Str', 105); 106 107has ca_certificate_path => ( 108 is => 'rw', 109 isa => 'Str', 110); 111 112has ca_certificate_file => ( 113 is => 'rw', 114 isa => 'Str', 115); 116 117has ssl_ignore_mode => ( 118 is => 'rw', 119 isa => 'Bool', 120 default => 0, 121); 122 123# ssl options in Net::LDAP format 124has verify => ( 125 is => 'rw', 126 isa => 'Str|Undef', 127 lazy => 1, 128 default => sub { shift->ssl_ignore_mode() ? 'none' : undef } 129); 130 131has capath => ( 132 is => 'rw', 133 isa => 'Str|Undef', 134 lazy => 1, 135 default => sub { shift->ca_certificate_path(); } 136); 137 138has cafile => ( 139 is => 'rw', 140 isa => 'Str|Undef', 141 lazy => 1, 142 default => sub { shift->ca_certificate_file(); } 143); 144 145has sslversion => ( 146 is => 'rw', 147 isa => 'Str', 148); 149 150has ciphers => ( 151 is => 'rw', 152 isa => 'Str', 153); 154 155has clientcert => ( 156 is => 'rw', 157 isa => 'Str|Undef', 158 lazy => 1, 159 default => sub { shift->certificate_file(); } 160); 161 162has clientkey => ( 163 is => 'rw', 164 isa => 'Str|Undef', 165 lazy => 1, 166 default => sub { shift->certificate_key_file(); } 167); 168 169has checkcrl => ( 170 is => 'rw', 171 isa => 'Int', 172); 173 174has bind => ( 175 is => 'ro', 176 isa => 'Net::LDAP', 177 reader => '_bind', 178 builder => '_init_bind', 179 clearer => '_purge_bind', 180 lazy => 1, 181); 182 183has action => ( 184 is => 'rw', 185 isa => 'Str', 186 lazy => 1, 187 default => 'replace', 188); 189 190has create => ( 191 is => 'ro', 192 isa => 'HashRef', 193 default => sub { return {} }, 194); 195 196has schema => ( 197 is => 'ro', 198 isa => 'HashRef', 199); 200 201sub _build_config { 202 my $self = shift; 203 204} 205 206sub _build_options { 207 my $self = shift; 208 209 my %options; 210 foreach my $key (@_) { 211 if (defined $self->$key()) { 212 $options{$key} = $self->$key(); 213 } 214 } 215 return %options; 216} 217 218sub _build_new_options { 219 my $self = shift; 220 return $self->_build_options(qw( 221 timeout verify keepalive debug raw multihomed localaddr 222 verify sslversion ciphers capath cafile capath clientcert clientkey checkcrl 223 )); 224} 225 226sub _build_bind_options { 227 my $self = shift; 228 return $self->_build_options(qw( password )); 229} 230 231# the argument passed to this method will be used as template parameters 232# in the expansion of the filter attribute 233sub _build_search_options { 234 my $self = shift; 235 my $arg = shift; 236 my $params = shift; 237 238 my %options = $self->_build_options(qw( base scope sizelimit timelimit )); 239 240 my $filter = $self->filter(); 241 242 # template expansion is performed on filter strings only, not 243 # on Net::LDAP::Filter objects 244 my $value; 245 if (ref $filter eq '') { 246 Template->new()->process(\$filter, $arg, \$value) || $self->_log_and_die("Error processing argument template."); 247 $options{filter} = $value; 248 } else { 249 $options{filter} = $filter; 250 } 251 252 253 # Add the attributes to the query to return only the ones we are asked for 254 # Will not work if we allow Filters 255 $options{attrs} = $self->attrs unless( $params->{noattrs} ); 256 257 $self->log()->debug('LDAP Search options ' . Dumper %options); 258 259 return %options; 260} 261 262# If the attr property is set using a string (necessary atm for Config::Std) 263# its converted to an arrayref. Might be removed if Config::* improves 264# This might create indefinite loops if something goes wrong on the conversion! 265sub _convert_attrs { 266 my ( $self, $new, $old ) = @_; 267 268 # Test if the given value is a non empty scalar 269 if ($new && !ref $new && (!$old || $new ne $old)) { 270 my @attrs = split(" ", $new); 271 $self->attrs( \@attrs ) 272 } 273 274} 275 276sub _init_bind { 277 278 my $self = shift; 279 280 $self->log()->debug('Open bind to to ' . $self->LOCATION()); 281 282 my $ldap = Net::LDAP->new( 283 $self->LOCATION(), 284 $self->_build_new_options(), 285 ); 286 287 if (! $ldap) { 288 $self->_log_and_die("Could not instantiate ldap object ($@)"); 289 } 290 291 my $mesg; 292 if (defined $self->binddn()) { 293 $self->log()->debug('Binding with ' . $self->binddn()); 294 my %options = $self->_build_bind_options(); 295 $self->log()->warn('Binding with DN but without password') if (!defined $options{password}); 296 $mesg = $ldap->bind( 297 $self->binddn(), 298 %options, 299 ); 300 } else { 301 # anonymous bind 302 $self->log()->debug('Binding anonymously'); 303 $mesg = $ldap->bind( 304 $self->_build_bind_options(), 305 ); 306 } 307 308 if ($mesg->is_error()) { 309 $self->_log_and_die(sprintf("LDAP bind failed with error code %s (error: %s)", $mesg->code(), $mesg->error_desc())); 310 } 311 return $ldap; 312} 313 314sub ldap { 315 # ToDo - check if still bound 316 my $self = shift; 317 return $self->_bind; 318} 319 320# It looks like a half closed connection (server gone / load balancer etc) 321# causes an operational error and not a connection error 322# so the list of codes to use this reconnect foo is somewhat experimental 323sub _is_transient_error { 324 my $self = shift; 325 my $mesg = shift; 326 if ($mesg->is_error() && (grep { $_ == $mesg->code() } (1,81,82))) { 327 $self->log()->debug('Connection lost - try rebind and rerun query'); 328 $self->_purge_bind(); 329 return 1; 330 } 331 return 0; 332} 333 334sub _getbyDN { 335 336 my $self = shift; 337 my $dn = shift; 338 my $params = shift; 339 340 my $ldap = $self->ldap(); 341 342 my %options = $self->_build_options(qw( sizelimit timelimit )); 343 $options{attrs} = $params->{attrs} if ($params->{attrs}); 344 345 my $mesg = $ldap->search( base => $dn, scope => 'base', filter => '(objectclass=*)', %options ); 346 347 if ($self->_is_transient_error($mesg)) { 348 $mesg = $ldap->search( base => $dn, scope => 'base', filter => '(objectclass=*)', %options ); 349 } 350 351 if ($mesg->is_error()) { 352 $self->log()->debug("LDAP getByDN failed with error code " . $mesg->code() . " (error: " . $mesg->error_desc() .")" ); 353 return; 354 } 355 356 if ( $mesg->count() == 1) { 357 358 my $entry = $mesg->entry(0); 359 # For testing 360 if (lc($entry->dn()) ne lc($dn)) { 361 $self->log()->warn('Search by DN with result looks fishy. Request: '.$dn.' - Entry: '.$entry->dn()); 362 } 363 return $entry; 364 } 365 366 # Query is ambigous - can this happen ? 367 if ( $mesg->count() > 1) { 368 $self->_log_and_die("There is more than one matching node.", 369 sprintf('Search by DN got multiple (%01d) results (%s)', $mesg->count(), $dn)); 370 } 371 372 # If autocreation is not requested, return undef 373 if (!$params->{create}) { 374 return; 375 } 376 377 # No match, so split up the DN and walk upwards 378 my $base_dn = $self->base; 379 380 # we cant do much if the base dn does not exists 381 if ($dn eq $base_dn) { 382 $self->_log_and_die('Request to auto-create the base dn'); 383 } 384 385 # Strip the basedn from the dn and tokenize the rest 386 my $path = $dn; 387 $path =~ s/$base_dn\z//; 388 389 if (!$path) { 390 $self->_log_and_die('Request to auto-create empty path'); 391 } 392 393 my @dn_attributes = $self->_splitDN( $path ); 394 395 my $currentPath = $base_dn; 396 my @nextComponent; 397 my $i; 398 for ($i = scalar(@dn_attributes)-1; $i >= 0; $i--) { 399 400 # For the moment we just implement single value components 401 my $nextComponentKey = $dn_attributes[$i][0]; 402 my $nextComponentValue = $dn_attributes[$i][1]; 403 404 my $nextComponent = $nextComponentKey.'='.$nextComponentValue; 405 406 # Search for the next node 407 #print "Probe $currentPath - $nextComponent: "; 408 $mesg = $ldap->search( base => $currentPath, scope => 'one', filter => '('.$nextComponent.')' ); 409 410 # found, push to path and test next 411 if ( $mesg->count() == 1) { 412 #print "Found\n"; 413 $currentPath = $nextComponent.','.$currentPath; 414 next; 415 } 416 417 #print Dumper( $mesg ); 418 #print "not Found - i: $i\n\n"; 419 420 # Reuse counter and list to build the missing nodes 421 while ($i >= 0) { 422 $nextComponentKey = $dn_attributes[$i][0]; 423 $nextComponentValue = $dn_attributes[$i][1]; 424 $currentPath = $self->_createPathItem($currentPath, $nextComponentKey, $nextComponentValue); 425 $i--; 426 } 427 } 428 429 return $self->_getbyDN( $dn ); 430} 431 432sub _createPathItem { 433 434 my $self = shift; 435 $self->log()->trace("Create Path called with " . Dumper \@_) if ($self->log()->is_trace); 436 437 my $currentPath = shift; 438 my $nextComponentKey = shift; 439 my $nextComponentValue; 440 441 my $values = {}; 442 if (ref $nextComponentKey) { 443 $values = $nextComponentKey; 444 } else { 445 $nextComponentValue = shift; 446 $values = { lc($nextComponentKey) => $nextComponentValue }; 447 } 448 my $attributes = shift; 449 450 my $rdnkey = lc(join("+", sort keys %{$values})); 451 452 my $newDN = join( "+", map { sprintf("%s=%s", $_, $values->{$_}) } sort keys %{$values}); 453 $newDN .= ','.$currentPath; 454 455 my $schema = $self->schema(); 456 $self->_log_and_die("No schema data to create nodes") if (!$schema); 457 458 $schema = $schema->{$rdnkey} || $schema->{default}; 459 $self->_log_and_die("No schema data for create path item ($rdnkey)") if (!$schema); 460 461 my @attrib; 462 if (!$schema->{objectclass}) { 463 $self->_log_and_die("No objectclass defined for path item $rdnkey"); 464 } 465 466 push @attrib, "objectclass"; 467 if (ref $schema->{objectclass} eq 'ARRAY') { 468 push @attrib, $schema->{objectclass}; 469 } else { 470 my @classnames = split " ", $schema->{objectclass}; 471 push @attrib, \@classnames; 472 } 473 474 # Default Values to push 475 $values = { %{$values}, %{$schema->{values} } } if ($schema->{values}); 476 477 # append attributes to value hash, this will overwrite values from the 478 $values = { %{$attributes}, %{$values} }; 479 480 foreach my $key ( keys %{$values}) { 481 my $val = $values->{$key}; 482 next unless defined $val; 483 if ($val eq 'copy:self') { 484 die "copy:self does not work with multivalued rdns" unless defined $nextComponentValue; 485 $val = $nextComponentValue; 486 } 487 push @attrib, $key, $val; 488 } 489 490 #print "Create Node $newDN \n"; 491 #print Dumper( $attrib ); 492 493 $self->log()->trace("Create Node $newDN with attributes " . Dumper \@attrib) if ($self->log()->is_trace); 494 $self->log()->debug("Create Node $newDN with attributes " . Dumper \@attrib); 495 496 my $result = $self->ldap()->add( $newDN, attr => \@attrib ); 497 498 if ($result->is_error()) { 499 $self->_log_and_die($result->error_desc); 500 } 501 502 return $newDN; 503 504} 505 506sub _triggerAutoCreate { 507 508 my $self = shift; 509 my $args = shift; 510 my $data = shift || {}; 511 512 my $path = $self->base(); 513 my @rdn; 514 515 my $tt = Template->new({}); 516 517 my $create_info = $self->create(); 518 if ($create_info->{basedn}) { 519 $path = $create_info->{basedn}; 520 } 521 522 if ($create_info->{dn}) { 523 my $dn; 524 $tt->process(\$create_info->{dn}, { ARGS => $args, DATA => $data }, \$dn) || $self->_log_and_die("Error processing argument template for DN."); 525 $self->log()->debug('Auto-Create with full dn from template'); 526 if (!$dn || $dn !~ /(([^=]+)=(.*?[^\\])\s*,)(.+)/) { 527 $self->_log_and_die("Unable to split DN from template"); 528 } 529 @rdn = ($2, $3); 530 # triggers creation of path components below later 531 $path = $4; 532 533 } elsif ($create_info->{rdn}) { 534 if (ref $create_info->{rdn}) { 535 my $hash; 536 foreach my $rdtpl (@{$create_info->{rdn}}){ 537 my $rdn; 538 $tt->process(\$rdtpl, { ARGS => $args, DATA => $data }, \$rdn) || $self->_log_and_die("Error processing argument template for RDN $rdtpl."); 539 next unless ($rdn); 540 my @t = split("=", $rdn, 2); 541 $hash->{$t[0]} = $t[1]; 542 } 543 @rdn = ($hash); 544 $self->log()->debug('Auto-Create with RDN template (Multivalued)'); 545 } else { 546 my $rdn; 547 $tt->process(\$create_info->{rdn}, { ARGS => $args, DATA => $data }, \$rdn) || $self->_log_and_die("Error processing argument template for RDN " . $create_info->{rdn}); 548 @rdn = split("=", $rdn, 2); 549 $self->log()->debug('Auto-Create with RDN template'); 550 } 551 552 } elsif ($create_info->{rdnkey}) { 553 @rdn = ($create_info->{rdnkey}, $args->[0]); 554 $self->log()->debug('Auto-Create with RDN key') if($self->log()->is_debug); 555 556 } else { 557 my $schema = $self->schema(); 558 if ($schema->{'cn'}) { 559 $self->log()->debug('Auto-Create with commonName based on schema:'); 560 @rdn = ('cn', $args->[0]); 561 } else { 562 $self->log()->warn('Auto-Create not configured'); 563 return; 564 } 565 } 566 567 # create the components for the path first, without usage of extra data 568 if ($path ne $self->base()) { 569 $path = $self->_getbyDN( $path, { create => 1 } ); 570 } 571 572 # now create the leaf node now using the extra data 573 my $nodeDN = $self->_createPathItem( $path, @rdn, $data ); 574 $self->log()->debug('Auto-Create done - nodeDN: '.$nodeDN); 575 return $nodeDN; 576 577} 578 579sub _splitDN { 580 581 my $self = shift; 582 my $dn = shift; 583 584 my @parsed; 585 while ($dn =~ /(([^=]+)=(.*?[^\\])\s*,)(.*)/) { 586 push @parsed, [ $2, $3 ]; 587 $self->log()->debug(sprintf 'Split-Result: Key: %s, Value: %s, Remainder: %s ', $2, $3, $4); 588 $dn = $4; 589 }; 590 591 # soemthing is really wrong - likely broken input with comma in the end 592 if (!$dn) { 593 $self->_log_and_die('Empty dn part in splitDN'); 594 } 595 596 # Split last remainder at = 597 my @last = split ("=", $dn); 598 push @parsed, \@last; 599 600 return @parsed; 601} 602 603sub _run_search { 604 605 my $self = shift; 606 my $arg = shift; 607 my $params = shift; 608 609 my %option = $self->_build_search_options( $arg, $params ); 610 611 my $mesg = $self->ldap()->search( %option ); 612 613 # Lost connection, try to rebind and rerun query 614 if ($self->_is_transient_error($mesg)) { 615 $mesg = $self->ldap()->search( %option ); 616 } 617 618 return $mesg; 619 620} 621 622no Moose; 623__PACKAGE__->meta->make_immutable; 624 6251; 626__END__ 627 628=head1 NAME 629 630Connector::Proxy::Net::LDAP 631 632=head1 DESCRIPTION 633 634This is the base class for all LDAP Proxy modules. It does not offer any 635external functionality but bundles common configuration options. 636 637=head1 USAGE 638 639=head2 minimal setup 640 641 my $conn = Connector::Proxy::Net::LDAP->new({ 642 LOCATION => 'ldap://localhost:389', 643 base => 'dc=example,dc=org', 644 filter => '(cn=[% ARGS.0 %])', 645 }); 646 647 $conn->get('John Doe'); 648 649Above code will run a query of C<cn=test@example.org against the server> 650using an anonymous bind. 651 652=head2 using bind credentials 653 654 my $conn = Connector::Proxy::Net::LDAP->new( { 655 LOCATION => 'ldap://localhost:389', 656 base => 'dc=example,dc=org', 657 filter => '(cn=[% ARGS.0 %])', 658 binddn => 'cn=admin,dc=openxpki,dc=org', 659 password => 'admin', 660 attrs => ['usercertificate;binary','usercertificate'], 661 }); 662 663Uses bind credentials and queries for entries having (at least) one of the 664mentioned attributes. 665 666=head2 connection control 667 668Following controls are passed to Net::LDAP->new from class parameters 669with the same name, see Net::LDAP for details. 670 671=over 672 673=item timeout 674 675=item keepalive 676 677=item multihomed 678 679=item localaddr 680 681=item debug 682 683=item raw 684 685Enables utf8 for returned attribute values. The default value is 686qr/;binary/, set this to a Regex reference to change the attribute 687pattern for utf8 conversion or set I<undef> to disable it. 688 689=back 690 691=head3 SSL connection options 692 693SSl related options are passed to Net::LDAP->new, see Net::LDAP for 694details. The attribute names in brackets are identical to the ones 695used in the HTTP based connectors and mapped to their equivalents. 696 697Note that mapping takes place at first init, so modifications to those 698values after the first connection will not be visibile. The native 699parameter names are superior. 700 701=over 702 703=item verify (ssl_ignore_mode - 'reqiured' if true) 704 705=item sslversion 706 707=item ciphers 708 709=item capath (ca_certificate_path) 710 711=item cafile (ca_certificate_file) 712 713=item clientcert (certificate_file) 714 715=item clientkey (certificate_key_file) 716 717=item checkcrl 718 719=back 720 721=head2 setting values 722 723You can control how existing attributes in the node are treated setting the 724I<action> parameter in the connectors base configuration. 725 726 connector: 727 LOCATION:... 728 .... 729 action: replace 730 731=over 732 733=item replace 734 735This is the default (the action parameter may be omitted). The passed value is 736set as the only value in the attribute. Any values (even if there are more 737than one) are removed. If undef is passed, the whole attribute is removed 738from the node. 739 740=item append 741 742The given value is appended to exisiting attributes. If undef is passed, the request is ignored. 743 744=item delete 745 746The given value is deleted from the attribute entry. If there are more items in the attribute, 747the remaining values are left untouched. If the value is not present or undef is passed, 748the request is ignored. 749 750=back 751 752=head2 autocreation of missing nodes 753 754If you want the connector to autocreate missing nodes (on a set operation), 755you need to provide the ldap properties for each rdn item. 756 757 schema: 758 cn: 759 objectclass: inetOrgPerson pkiUser 760 values: 761 sn: copy:self 762 ou: IT Department 763 764You can specify multiple objectclass entries seperated by space or as list. 765 766The objects attribute matching the RDN component is always set, you can 767use the special word C<copy:self> to copy the attribute value within the 768object. The values section is optional. 769 770If schema for I<CN> is given and the filter does not find a result, the 771node name is constructed from using the first path argument as CN and the 772base dn of the connector as path. All defined attribute values that have 773been passed are also added to the object on creation. Auto-Creation is not 774applied if action is set to delete. 775 776For creating the actual leaf node, there are additional options by adding 777the node I<create> to the configuration. 778 779=head3 set another component class for the node 780 781 create: 782 rdnkey: emailAddress 783 784Will use the given class name with the first argument as value plus the 785base dn to build the node DN. The old syntax with rdnkey + value pattern 786(which was broken anyway) is no longer supported, use the full rdn template 787as given below if required. 788 789=head3 set another path to the node 790 791 create: 792 basedn: ou=Webservers,ou=Servers,dc=company,dc=org 793 794=head3 use templating to generate the local component 795 796The given base dn will be prefixed with the component assigned to the 797leaf, e.g. cn=www.example.org,ou=Webservers,ou=Servers,dc=company,dc=org 798 799=head3 use templating to generate the local component 800 801 create: 802 rdn: emailAddress=[% ARGS.0 %] 803 804Same result as the first example, the path arguments are all in ARGS, 805additional data (depends on the subclass implementation) are made 806available in the DATA key. 807 808Multivalued RDNs can be constructed using a list: 809 810 create: 811 rdn: 812 - emailAddress=[% ARGS.0 %] 813 - CN=[% ARGS.1 %] 814 815=head3 use temlating for full DN 816 817 create: 818 dn: emailAddress=[% ARGS.0 %],ou=People,dc=company,dc=org 819 820Same as setting basedn and rdn, components of the path are created if 821there is a matching schema definition. Limitation: this module does not 822support different value patterns for the same class name. 823 824=head2 Full example using Connector::Multi 825 826 [ca1] 827 myrepo@ = connector:connectors.ldap 828 829 [connectors] 830 831 [connectors.ldap] 832 class = Connector::Proxy::Net::LDAP 833 LOCATION = ldap://ldaphost:389 834 base = dc=openxpki,dc=org 835 filter = (cn=[% ARGS.0 %]) 836 attrs = userCertificate;binary 837 binddn = cn=admin,dc=openxpki,dc=org 838 password = admin 839 action = replace 840 841 [connectors.ldap.create] 842 basedn: ou=Webservers,ou=Server CA3,dc=openxpki,dc=org 843 rdnkey: cn 844 value: [% ARGS.0 %] 845 846 [connectors.ldap.schema.cn] 847 objectclass: inetOrgPerson 848 849 [connectors.ldap.schema.cn.values] 850 sn: copy:self 851 852 [connectors.ldap.schema.ou] 853 objectclass: organizationalUnit 854 855=head1 internal methods 856 857=head2 _getByDN 858 859Search a node by DN. 860 861 $self->_getByDN( 'cn=John Doe,ou=people,dc=openxpki,dc=org' ); 862 863Returns the ldap entry object or undef if not found. Pass C<{create => 1}> and 864configure your connector to auto create a new node if none is found. 865 866=head2 _createPathItem 867 868Used internally by _getByDN to create new nodes. 869 870=head2 _triggerAutoCreate 871 872Used internally to assemble the DN for a missing node. 873Returns the ldap entry or undef if autocreation is not possible. 874 875=head2 _splitDN 876 877Very simple approch to split a DN path into its components. 878Please B<do not> use quoting of path components, as this is 879not supported. RDNs must be split by a Comma, Comma inside a value 880must be escaped using a backslash character. Multivalued RDNs are not supported. 881 882=head2 _run_search 883 884This is a wrapper for 885 886 my $mesg = $ldap->search( $self->_build_search_options( $args, $param ) ); 887 888that will take care of stale/lost connections to the server. The result 889object is returned by the method, the ldap object is taken from the class. 890