LDAP Books

Every once and awhile someone asks me to recommend a book or two about LDAP. Fortunately all of these requests so far have been from people looking for a good introduction to the topic, and not for something providing advanced techniques along the lines of Mastering Regular Expressions(O'Reilly) or the Perl Cookbook(O'Reilly). Introductory texts on LDAP abound, most follow the pattern of spending their first half explaining the protocol, its accompanying API, and giving a few examples. This is in stark contrast to the LDAP-derived Active Directory, which now can boast several advanced tomes, including my favorites, Robbie Allen's Active Directory Cookbook(O'Reilly) and his older Managing Enterprise Active Directory Services (Addison-Wesley).

About the best introductory book I've found is also one of the more recent, Gerry Carter's LDAP Administration(O'Reilly), which covers alot of ground for a book that's supposed to focus mostly on OpenLDAP (here's a secret, while OpenLDAP is used as an example LDAP directory through the book, Gerry actually provides a pretty balanced presentation of generic LDAP, including some space on Active Directory). For IT managers, the recently updated Understanding and Deploying LDAP Directory Services (Addison-Wesley) provides the best resource for architectural issues. Those managing a Sun Directory environment will find the Sun Blueprints series title LDAP in the Solaris Operating Environment somewhat helpful, although most of the material there appears to be a rehash of the manuals (which are actually pretty good as software manuals go, the Deployment Guide and Command Reference, are must-reads).

For a directory administrator like myself, unfortunately, there is currently no advanced book to help with the practical day to day problems of synchronizing (which is really just a function of "transforming" and then "diffing" entries) directories, writing connectors to non-LDAP systems and best practices for data management. While all the books currently out there provide examples of code using the Netscape SDKs and PerLDAP, these tools are now pretty long in the tooth, and don't have the price/performance punch of the current Sun Java/JNDI and the pure Perl Net::LDAP (a/k/a perl-ldap) offerings. To Gerry Carter's credit Net::LDAP gets good coverage in his book (as it does in Robbie Allen's older book, only marketing forces can explain why he went pure VBScript in his newer writings). For OpenLDAP administrators there still is no book with coverage of tuning the Berkeley DB backend, which as I've pointed out elsewhere on this blog is a critical task if that product is to be used in an enterprise environment. What is missing, and sorely needed, is an "LDAP Administrator's Cookbook" and a companion "Advanced LDAP Directory Management" to cover best practices.

I am, of course, open to any offers.



Using Spreadsheet::ParseExcel

In my own experience as a directory administrator only about 25% of my time is spent managing the servers and software that support the environment. The lion's share of the effort in my job is managing the import and export of data through the directory environment, in most cases working with groups outside the IT department across the globe. Recently we had some weird results when a csv file with employee data from our offices in Brazil was imported into the directory. The short version of the story is that in several attributes (surname, locality, state), some of the characters used didn't quite match up with a known character set. Our assumption is that the data was originally created in Excel and then dumped to CSV format, probably using a localized copy of the program and the Portugese (Brazil) keyboard template. Among other things this got me going in a direction I'd avoided for a long time -- learning how to parse Excel spreadsheets with Perl.

My problem was that the doc for Spreadsheet::ParseExcel was opaque to me, as are most man pages on UNIX systems. Maybe it's just that I'm dense, or don't have enough patience. Anyway, after much experimentation, I finally "cracked the code" and was able to come up with the following little script to convert an Excel spreadsheet into a delimited text file.

#!/usr/bin/perl
# parse_excel.pl Read an Excel spreadsheet and print its contents to
# a bar delimited file
# Created 10/19/04 by P Lembo

use strict;
use Spreadsheet::ParseExcel;

my $HOME = "/u01/home/ldapcon";
my $xlsFile = "$HOME/data/admin/ArrowOIDRegistry.xls";
my $txtFile = "$HOME/data/admin/oid-registry.txt";

my $wkBook = Spreadsheet::ParseExcel::Workbook->Parse($xlsFile);

open FH, ">$txtFile" or die $!;

my $nuSheets = $wkBook->{SheetCount};
print "There are $nuSheets sheets in this workbook\n";

# Iterate through worksheets
for ( my $iSheet=0; $iSheet < $wkBook->{SheetCount}; $iSheet++ ) {

my $wkSheet = $wkBook->Worksheet($iSheet);
my $wksName = $wkSheet->{Name};

print "Name of worksheet is $wksName\n";

# Now iterate through a worksheet by row
for (my $iRow=0; $iRow < $wkSheet->{MaxRow}; $iRow++ ) {

my $colNu = $wkSheet->{MaxCol};

# Loop through the columns in the row
for (my $iCol=0; $iCol < $wkSheet->{MaxCol}; $iCol++ ) {

# Pull out the cell value
my $wksCell = $wkSheet->{Cells}[$iRow][$iCol];

# Print each value
print FH $wksCell->Value, if ($wksCell);

# Print a delimiter after each value except the last in the row
print FH "\|", if ($iCol < ($colNu - 1));

}

# Print a newline after each row
print FH "\n";

}
}

print FH "\n":

close FH;

__END__;


Mixing Metaphors: LDAP and ADSI at work

I just finished writing a cgi to provide a barebones web based password reset facility for Active Directory as a backup to my groups "approved" COTS solution. As I noted in my personal blog, working with ADSI through Perl's' Win32::OLE module was a challenge. For now the code I'll be posting here will provide a standby capability for production. If I have time later on I'd like to eliminate the Win32::OLE pieces altogether and do everything with Net::LDAP. That won't be possible until SSL is enabled in our Active Directory environment, something that probably won't be entertained by the Windows engineering team until next year.

So, without further ado, here is the code:

#!perl -w
# adpassreset.cgi Reset Active Directory passwords
# 10/1/04 by P Lembo
# Give choice of unlocking, forcing or both (default)
# version 0.01

use strict;
use CGI;
use CGI::Carp qw(fatalsToBrowser);
use URI::URL;
use Net::LDAP;
use Net::LDAP::Entry;
use Win32::OLE;
$Win32::OLE::Warn = 3;

my $q = CGI->new;

require "../etc/ldapapp.conf";

our ($adsTestHost,$adsQualHost,$adsProdHost,$adsTestPath,
$adsQualPath,$adsProdPath,$adsUsr,$adsPass);

my @adshosts = ("$adsTestHost","$adsQualHost","$adsProdHost");
my @adsbases = ("$adsTestPath","$adsQualPath","$adsProdPath");
my $newpass = 'yellow';

my $webhost = "localhost";
my $webport = "80";


if ($q->param('Review')) {

give_status($q);

}

elsif ($q->param('Reset Account')) {

reset_user($q);

}


else {

show_main($q);
}

sub show_main {

print $q->header;
print $q->start_html(-title=>'Active Directory Account Reset',
-style=>{'src'=>'/styles/ldapadmin.css'}
);
print $q->h1("Active Directory Account Reset");

my $action = $q->url;
print $q->start_form(-method=>'POST',
-action=>$action,
);

print $q->h4("Select Server");
print $q->popup_menu(-name=>'adshost',
-values=>\@adshosts
);

print $q->h4("Choose Domain");
print $q->popup_menu(-name=>'adsbase',
-values=>\@adsbases
);

print $q->h4("Enter User ID");
print $q->textfield(-name=>'userid',-size=>5);
print $q->p();
print $q->submit('Review');
print $q->reset("Cancel");

print $q->end_form;
print $q->p();
print $q->a( { -href => "/acctadmin/index.html" },
"Back To Active Directory Tools");
print $q->end_html();


} # main


sub give_status {


print $q->header(-charset=>'UTF-8');
print $q->start_html(-title=>'Active Directory Account Reset',
-style=>{'src'=>'/styles/ldapadmin.css'}
);
print $q->h1("Active Directory Account Reset");
print $q->h3("Confirm User Information");
my $action = $q->url;
print $q->start_form(-method=>'POST',
-action=>$action,
);

my $adshost = $q->param('adshost');
my $adsbase = $q->param('adsbase');
my $userid = $q->param('userid');


print $q->hidden(-name=>"adshost",value=>"$adshost");
print $q->hidden(-name=>"adsbase",value=>"$adsbase");
print $q->hidden(-name=>"userid",value=>"$userid");

print $q->p("Working on $adshost");
print $q->p("$adsbase");

$adsUsr = "$adsUsr,$adsbase";

my $basedn = $adsbase;
my @attrs = qw(displayname cn userprincipalname samaccountname);
my $filter = "(|(samaccountname=$userid)(cn=$userid))";

my $ldap = Net::LDAP->new($adshost, version =>'3');
my $mesg = $ldap->bind($adsUsr, password => $adsPass);

$mesg = $ldap->search(base => $basedn,
scope => 'sub',
filter => $filter,
attrs => \@attrs
);

if ( $mesg->count <1 ) {

print $q->h4("No results returned!");

} # if

elsif ( $mesg->count >1 ) {

print $q->p("Please choose from one of the entries listed and retry");

while (my $entry = $mesg->shift_entry()) {
my $userdn = $entry->dn;
my $cn = $entry->get_value('cn');
my $displayname = $entry->get_value('displayname');
print $q->p("$userdn");
print $q->p("UserID: $userid",$q->br, "Full Name: $displayname");
}
}

else {

my $entry = $mesg->shift_entry();
my $userdn = $entry->dn;
print $q->hidden(-name=>"userdn",value=>"$userdn");
my $cn = $entry->get_value('cn');
print $q->hidden(-name=>"cn",value=>"$cn");
my $displayname = $entry->get_value('displayname');
print $q->hidden(-name=>"displayname",value=>"$displayname");
my $userprincipalname = $entry->get_value('userprincipalname');
print $q->hidden(-name=>"userprincipalname",value=>"$userprincipalname");

print $q->p("$userdn");
print $q->p("UserID: $userid",$q->br, "Full Name: $displayname");


# Bind using ADSI to check flags
my $ldapObj = Win32::OLE->GetObject('LDAP:');
my $usrObj = $ldapObj->OpenDSObject("LDAP://$adshost/$userdn","$adsUsr", "$adsPass", 1);

if ($usrObj->AccountDisabled == 1) {
print $q->p("Account is disabled, contact System Administrator");
}

if ($usrObj->{IsAccountLocked} == 1) {
print $q->p("Account is locked, reset will unlock");
}
else {
print $q->p("Account not locked");
}

print $q->p();
print $q->submit('Reset Account');
print $q->reset("Cancel");

}

$ldap->unbind;

print $q->end_form;

print $q->p();

print $q->a( { -href => '/acctadmin/adacctreset.cgi' }, 'Try Again');
print $q->p();
print $q->a( { -href => "/acctadmin/index.html" }, "Back To Help Desk Tools");
print $q->end_html;

} # status


sub reset_user {


print $q->header(-charset=>'UTF-8');
print $q->start_html(-title=>'Active Directory Account Reset',
-style=>{'src'=>'/styles/ldapadmin.css'}
);
print $q->h1("Active Directory Account Reset");
print $q->h3("Confirming Changes");

my $adshost = $q->param('adshost');
my $adsbase = $q->param('adsbase');
my $userdn = $q->param('userdn');
my $cn = $q->param('cn');
my $displayname = $q->param('displayname');
my $userprincipalname = $q->param('userprincipalname');

print $q->p("Working on $adshost");

print "$userdn

\n";
print "$displayname

\n";

$adsUsr = "$adsUsr,$adsbase";

# Bind using ADSI to reset password
my $ldapObj = Win32::OLE->GetObject('LDAP:');
my $usrObj = $ldapObj->OpenDSObject("LDAP://$adshost/$userdn","$adsUsr", "$adsPass", 1);

# Check for disabled account
if ($usrObj->AccountDisabled == 1) {
print $q->p("Account is still disabled, contact System Administrator");
}

# Unlock account
if ($usrObj->{IsAccountLocked} == 1) {
$usrObj->{IsAccountLocked} = 0;
$usrObj->SetInfo;
print $q->p("Account unlocked");
}
else {
print$q->p( "Account not locked");
}

# Reset password
$usrObj->SetPassword($newpass);
$usrObj->SetInfo;
print $q->p("Password reset to $newpass");

# Forces change password on next logon
$usrObj->Put('pwdLastSet', 0);
$usrObj->SetInfo;
print $q->p("User must change password at next logon");


print $q->p();

print $q->a( { -href => '/acctadmin/adacctreset.cgi' }, 'Try Again');
print $q->p();
print $q->a( { -href => "/acctadmin/index.html" }, "Back To Active Directory Tools");
print $q->end_html;


} # reset



__END__;

Red Hat buying Netscape Directory, Certificate Servers

It's official. Red Hat has posted a press release announcing a deal to acquire Netscape's Directory and Certificate servers from AOL/Netscape. This is real good news for those of us in the LDAP business. The Slashdot post on this from 9/30/04 had over 200 replies, which just goes to show there is indeed intelligent life in the universe.

As a fairly satisfied user of Sun's rebranded version of the Netscape Directory server (which they acquired after the dissolution of the iPlanet partnership), I've kept a careful eye on the Netscape version (which has sometimes been hard to find, buried somewhere among a pile of broken links to technologies Netscape no longer supports). It's been pretty clear for a long time that AOL wasn't even trying to market these Netscape properties, and at least from a "maintaining balance in the universe" standpoint its good to see they will be going to a home where they won't be wasted.

The other good part is that these products will soon be open sourced, and made available for deployment on advanced Linux systems like the upcoming Red Hat Enterprise 4. AOL/Netscape did not support Linux on its latest versions, and Sun has only paid lipservice to the open source O/S (They claim their latest version, rebranded "Java System Enterprise Directory", works on the now antiquated Red Hat Enterprise 2.1. Right.). At work this has been an issue because we're now engineering a probable move of most of our web tier to Linux on Intel. Up until now it looked like the directories would have to stay on Solaris/Sparc.

I'll take a careful look at Red Hat's next release of the Netscape Directory, especially because the schemae and access controls in the Sun and Netscape products are almost identical and as a result the transition in our enterprise would be fairly painless. Interesting contrast here. Some companies. like Oracle, AOL and Novell (remmember WordPerfect? Or UNIXWare? DRDOS?), gobble up others and run them into the ground (beware, PeopleSoft and SuSE customers), others, like Red Hat, give them new life (think Cygwin here).