desertrat has asked for the wisdom of the Perl Monks concerning the following question:

I have a set of scripts to maintain a match table in a database between our local AD domain and an external (non-AD) directory. Net::LDAPS works great for this...when I use sAMAccountName as the search attribute. but since we routinely have people change that (and the match table is only populated whenpeople are onboarded) I need to use permanent unique identifiers. For AD this is objectGUID.

For most of the accounts in our Active Directory this works well, I save a base64 encoded copy of the binary objectGUID in the database and I can retrieve the details for the user regardless of any changes in the AD record. for most users this works both ways...I can retrieve the objectGUID using the sAMAccountName, then use that retrieved objectGUID to retrieve the sAMAccount name back.

for about 10% of the 700 accounts in this system, though, using objectGUID results in a 'Bad filter' error from the AD LDAP server. The code associated with it is param_error code 89

This has to be an error in the data side of the filter expression, since the filter statement is the same for both working and non-working accounts. There seems to be no pattern to the accounts that fail; I only really noticed this when I started trying to do some sweeps of the data to better ID changes in users (ie find people who are no longer in one or the other of the two directories.

I guess I'm asking has anyone ever run into this and how did you fix it? I cannot find any error in my code because it works, most of the time.

The following is a code template that I've used to test this:

#!/usr/bin/perl use strict; use Net::LDAPS; use Data::Dumper; my $ldu ='ldapquery'; my $ldp ='nottherealpassword'; my $unamein = 'johnson'; my $attr = ['objectGUID', 'sAMAccountName', 'employeeNumber', 'mail']; my $ldaps = Net::LDAPS->new("ldaps://host.domain") or return "FAIL LDA +P ERROR $0"; my $searchBase = "DC=host,DC=domain"; # Bind as AD user my $mesg1 = $ldaps->bind("$ldu\@host.domain", password=>$ldp); $mesg1->code && die "Authentication failed: " . $mesg1->error . "\n"; my $searchFilter = "(sAMAccountName=$unamein)"; $mesg1 = $ldaps->search ( # perform a search base => $searchBase, filter => $searchFilter, attrs => $attr ); $mesg1->code; print Dumper $mesg1; my $binguid= $mesg1->entry(0)->get_value('objectGUID'); $searchFilter = "(objectGUID=$binguid)"; $mesg1 = $ldaps->search ( # perform a search base => $searchBase, filter => $searchFilter, attrs => $attr ); $mesg1->code; print Dumper $mesg1; exit;

Running this for a user that works produces the following expected results:

LDAP query filter on sAMAccountName: ----------------------------------------------------- $VAR1 = bless( { 'reference' => [ 'ldaps://host.domain/CN=Configuratio +n,DC=Host,DC.Domain' ], 'errorMessage' => '', 'raw' => undef, 'parent' => bless( { 'net_ldap_scheme' => 'ldaps', 'net_ldap_refcnt' => 1, 'net_ldap_debug' => 0, 'net_ldap_host' => 'host.domain' +, 'net_ldap_rawsocket' => bless( \ +*Symbol::GEN2, 'IO::Socket::SSL' ), 'net_ldap_resp' => {}, 'net_ldap_uri' => 'ldaps://host. +domain', 'net_ldap_socket' => $VAR1->{'pa +rent'}{'net_ldap_rawsocket'}, 'net_ldap_async' => 0, 'net_ldap_mesg' => {}, 'net_ldap_port' => 636, 'net_ldap_version' => 3 }, 'Net::LDAPS' ), 'mesgid' => 6, 'ctrl_hash' => undef, 'entries' => [ bless( { 'asn' => { 'attributes' => [ +{ + 'vals' => [ + '06007454' + ], + 'type' => 'employeeNumber' +}, +{ + 'type' => 'objectGUID', + 'vals' => [ + 'T,.\\@?Ǔi2#FI' + ] +}, +{ + 'type' => 'sAMAccountName', + 'vals' => [ + 'johnson' + ] +}, +{ + 'vals' => [ + 'johnson@host.domain' + ], + 'type' => 'mail' +} ], 'objectName' => 'C +N=johnson,OU=ITStaff,OU=UA,OU=PharmAccounts,DC=Host,DC.Domain' }, 'changetype' => 'modify', 'changes' => [] }, 'Net::LDAP::Entry' ) ], 'controls' => undef, 'resultCode' => 0, 'matchedDN' => '', 'callback' => undef }, 'Net::LDAP::Search' ); LDAP query filter on objectGUID from previous query: ----------------------------------------------------- $VAR1 = bless( { 'entries' => [ bless( { 'changes' => [], 'changetype' => 'modify', 'asn' => { 'objectName' => 'C +N=johnson,OU=ITStaff,DC=Host,DC.Domain', 'attributes' => [ +{ + 'type' => 'employeeNumber', + 'vals' => [ + '06007454' + ] +}, +{ + 'vals' => [ + 'T,.\\@?Ǔi2#FI' + ], + 'type' => 'objectGUID' +}, +{ + 'vals' => [ + 'johnson' + ], + 'type' => 'sAMAccountName' +}, +{ + 'type' => 'mail', + 'vals' => [ + 'johnson@host.domain' + ] +} ] } }, 'Net::LDAP::Entry' ) ], 'controls' => undef, 'matchedDN' => '', 'callback' => undef, 'resultCode' => 0, 'reference' => [ 'ldaps://host.domain/CN=Configuratio +n,DC=Host,DC.Domain' ], 'parent' => bless( { 'net_ldap_scheme' => 'ldaps', 'net_ldap_refcnt' => 1, 'net_ldap_debug' => 0, 'net_ldap_host' => 'host.domain' +, 'net_ldap_rawsocket' => bless( \ +*Symbol::GEN2, 'IO::Socket::SSL' ), 'net_ldap_resp' => {}, 'net_ldap_uri' => 'ldaps://host. +domain', 'net_ldap_socket' => $VAR1->{'pa +rent'}{'net_ldap_rawsocket'}, 'net_ldap_async' => 0, 'net_ldap_mesg' => {}, 'net_ldap_port' => 636, 'net_ldap_version' => 3 }, 'Net::LDAPS' ), 'raw' => undef, 'errorMessage' => '', 'mesgid' => 7, 'ctrl_hash' => undef }, 'Net::LDAP::Search' );

Running this for a user that it does not work for results in the following for the second query; the first works as expected:

LDAP query filter on objectGUID from previous query: ----------------------------------------------------- $VAR1 = bless( { 'parent' => bless( { 'net_ldap_resp' => {}, 'net_ldap_host' => 'host.domain' +, 'net_ldap_version' => 3, 'net_ldap_rawsocket' => bless( \ +*Symbol::GEN2, 'IO::Socket::SSL' ), 'net_ldap_scheme' => 'ldaps', 'net_ldap_socket' => $VAR1->{'pa +rent'}{'net_ldap_rawsocket'}, 'net_ldap_refcnt' => 1, 'net_ldap_debug' => 0, 'net_ldap_uri' => 'host.domain', 'net_ldap_async' => 0, 'net_ldap_port' => 636, 'net_ldap_mesg' => {} }, 'Net::LDAPS' ), 'errorMessage' => 'Bad filter', 'resultCode' => 89, 'mesgid' => 7, 'raw' => undef, 'callback' => undef }, 'Net::LDAP::Search'

Replies are listed 'Best First'.
Re: Peculiar problem with Net::LDAPS and AD LDAP
by Radiola (Monk) on Feb 19, 2021 at 17:59 UTC

    I don't have the answer, but if you have Net::LDAP parse the text filter and convert it back to text again, with something like:

    $f1 = Net::LDAP::Filter->new($goodFilter); $f1->print; $f2 = Net::LDAP::Filter->new($badFilter); $f2->print;

    …maybe that'll make the difference more apparent. That's what Net::LDAP is doing under the hood.

    Also, error 89 (as in the resultCode value of your message object) is actually a generic param error, not necessarily a filter error, although I agree that's first place to look if you can reproduce the problems on the same searches every time.

    – Aaron
    Preliminary operational tests were inconclusive. (The damn thing blew up.)

      Thanks for the suggestion!

      That returns an error: Can't call method "print" on an undefined value for the 'bad' GUID .

      #!/usr/bin/perl use strict; use Net::LDAP::Filter; use MIME::Base64 qw(encode_base64 decode_base64); my $goodFilter = '(objectGUID = '.decode_base64('XrfAr5bivU61wz1WCgsxW +A==').')'; my $badFilter = '(objectGUID = '.decode_base64('6CncwjzJ/Umi4iIdB88efw +==').')'; print "Good filter:\n"; my $f1 = Net::LDAP::Filter->new($goodFilter); $f1->print; print "Bad filter:\n"; my $f2 = Net::LDAP::Filter->new($badFilter); $f2->print;

      Output:

      ./filtertest.pl Good filter: (objectGUID= ^\b7\c0\af\96\e2\bdN\b5\c3=V\0a\0b1X) Bad filter: Can't call method "print" on an undefined value at ./filtertest.pl lin +e 17.

      So, a bug in Net::LDAP::Filter, I think.

        I am convinced now it's a bug, that Net::LDAP::Filter's regexes are inadvertently matching things in the binary data causing the ->new() call to fail and return undef, but I found a work-around. When I looked up the objectGUID reference at LDAPWiki the proper way to create the search term for LDAP is (objectGUID=\12\34\56\78\9a\bc\de\f1) as a kind of escaped byte code.

        When I hand-constructed a searchfilter from a 'broken' objectGUID, the search worked

        So I wrote the following bit of code to turn the binary objectGUID into a hex string matching that format and I now can properly get all the info for the extant accounts.

        sub enc_hex { my @h = split(//,unpack('H*',(shift)); my ($i, $out, $first); foreach $i(@h) {if (!$first){$out.="\\$i"; $first=1;} else {$o +ut.=$i;$first='';}} return $out; }

        Plugging that into my actual module that gets a sAMAccountName from a stored uuencoded objectGUID worked.

        There is probably some Perl Master way to write this as a one-liner, but I'll be able to look at this three years from now and understand what I'm doing, so it'll do!

        Thanks again for the help!