Cleanup LDAP Group Membership

I was going to call this article, “We don’t need no stinkin’ referential integrity”, but decided against it in the interests of having people who really need this information being able to find it. Some time ago I mentioned how enforcing referential integrity was an important functionality that should be included with modern directory server software. It’s something I’ve used for years with good results.

Occasionally, however, things don’t work out like you plan. In my case the referential integrity plugin for our enterprise directory had been turned off for some time, and as a result membership in a number of groups remained the same, even though many of the users left the company and had their entries removed from the directory.

To address this I dusted off an old script I’d written before we began using the plugin. Basically what it does is go out the the directory and find the group dns of every group under a particular branch of the directory tree and then search each in turn for any members that don’t exist on the directory (or at least no in the “active” part of the directory). It’s pretty simple perl and Net::LDAP stuff that I hope others find useful too.

#!/usr/bin/perl
# groupclean.pl Removes entries of inactive users from membership
# of groups under $groupbranch.
use strict;
use Net::LDAP;
use Net::LDAP::Entry;
#
my $HOME = $ENV{'HOME'};
our($ldapHost,$ldapUsr,$ldapPas);
require "$HOME/etc/app.conf";
my $groupbranch = "ou=groups,dc=example,dc=com";
my $nocase = "cn,uniquemember";
my $ldapgrpdb = "$HOME/data/ldapgrp.db";
my $ldapgrpsrc = "$HOME/data/ldapgrpsrc.ldif";
my $ldapgrptar = "$HOME/data/ldapgrptar.ldif";
#
process_groups();
#
sub process_groups {
#
 my $timestamp = localtime();
 print "$timestamptInitiating LDAP Group Maintenance Routinen";
 print "tWorking on $ldapHostn";
 # First gather the dns of all the groups
 my @groupdns;
 my $basedn = $groupbranch;
 my $filter = "(objectclass=groupofuniquenames)";
 my @attrs = qw(objectclass);
#
 my $ldap = Net::LDAP->new($ldapHost, version => '3');
 my $mesg = $ldap->bind($ldapUsr, password =>$ldapPass);
#
 $mesg = $ldap->search (
			base=> $basedn,
			scope=> 'sub',
			filter=> $filter,
			attrs=> @attrs
			);
 # Make a list of groupdns
 while (my $entry = $mesg->shift_entry()) {
	my $groupdn = $entry->dn;
	push @groupdns, $groupdn;
 }
#
 my $totalcount =0;
 my $delcount=0;
 # Loop through the list, one dn at a time
 foreach my $groupdn(@groupdns) {
    @attrs = qw(uniquemember cn);
    # Get the member dns in the group
   $mesg = $ldap->search (
			base=> $groupdn,
			scope=> 'base',
			filter=> $filter,
			attrs=> @attrs
			);
    #
    while (my $entry = $mesg->shift_entry()) {
 	my @memberdns = $entry->get_value('uniquemember');
	my $groupname = $entry->get_value('cn');
	print "Working on group $groupdnn";
	@attrs = qw(cn);
	# Loop through each group member and search for them on LDAP
	foreach my $memberdn (@memberdns) {
	    my $result = search_member($memberdn);
	    if ($result == 0) {
		print "$memberdn not active, removingn";
		delete_member($groupdn, $memberdn);
		$delcount++;
	     }
	     $totalcount++;
	}
     }
  print "n";
 }
#
 print "tTotal members checked $totalcountn";
 print "tMembers removed $delcountn";
 $ldap->unbind;
 $timestamp = localtime();
 print "$timestamptGroup Maintenance Completedn";
}
#
sub search_member {
#
 my $memberdn = @_[0];
 my $basedn = "dc=example,dc=com";
 my $filter = "(objectclass=inetorgperson)";
 my @attrs = qw(objectclass uid);
 my $ldap = Net::LDAP->new($dirHost, version => '3');
 my $mesg = $ldap->bind($dirUsr, password => $dirPass);
#
 $mesg = $ldap->search (
			base=> $memberdn,
			scope=> 'base',
			filter=> $filter,
			attrs=> @attrs
			);
#
 if ($mesg->count shift_entry()) {
        my $dn = $entry->dn;
        my $uid = $entry->get_value(’uid’);
        if ($dn =~ /Retired/gi) {
	    return (0);
        }
        else {
	    return (1);
        }
    }
 }
 $ldap->unbind();
}
#
sub delete_member {
 my $groupdn = @_[0];
 my $memberdn = @_[1];
 my $ldap = Net::LDAP->new($ldapHost, version => ‘3′);
 my $mesg = $ldap->bind($ldapUsr, password =>$ldapPass);
 $mesg = $ldap->modify ($groupdn,
		delete => {’uniquemember’ => $memberdn }
		);
 $ldap->unbind;
}
#
__END__;

You’ll notice in the search_member subroutine I return a “0” if no entry corresponding to the uniquemember in a group is found. I also do this if the dn is found, but contains the text “Retired” in the dn, like “uid=12345678, ou=Retired,dc=example,dc=com”. Entries of terminated users are not always outright deleted in some environments, instead being stored in a container separate from where active entries are kept. This allows administrators to have a database of terminated user information available if needed. Environments that use Oracle Identity Manager or some other database-driven user administration system would not need to do this, since user information is kept in that application’s database.

Now there is at least one thing that makes this script something less than optimal. That would be the fact that I open and close an LDAP connection for each seach and delete operation. The better way to do this would be to open up that connection in main and then use the already instantiated connection objects (”$ldap”, “$mesg”) to do my LDAP commands against.

Another thing to consider is that once your groups become numerous and/or populous enough this is going to run v-e-r-y slowly. I hit a wall, performance-wise when I got up over 1,000 groups with 4,000 to 5,000 members each using Sun Directory. Your mileage will vary, depending upon the server product you’re using. But this is not just a limitation of any given LDAP server product, it has been demonstrated that groups over 5,000 members each aren’t processed very well under the LDAP protocol they all use in common. All that coding and decoding according to ASN.1 rules . As far as I can see the only real solution there is to do such maintenance operations at the database level, something that will hopefully be a lot easier to do with directories that have an RDBMS backend like OID (Oracle Internet Directory).