Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

LDAP and AD sync

124 views
Skip to first unread message

Prashanth Sundaram

unread,
Dec 10, 2009, 5:59:20 PM12/10/09
to perl...@perl.org
Folks,

I am a n00b to perl scripting and need help to start building my own. I am
currently working on a project where the LDAP(389-ds) and Active Directory
are always in sync. I have a very minimal set of attributes and conditions
to keep them in sync.

Can anyone share their code, so that I can build around it? Here¹s my
requirement:
* Sync New users from AD to LDAP with attributes: sAMAccountName, sn,
givenName, description, userAccountControl(disable/enable),
* Delete LDAP accounts which are not present in AD and vice versa.
* Generate the next available uidnumber by parsing thru ldap, so new users
can be created
* Check memberOf for 2 groups and if true add them to corresponding groups
in LDAP

If you have any of these modules written already, that would be great help.
I am digging through the archive looking for related code.

Thanks,
Prashanth

Peter Karman

unread,
Dec 10, 2009, 6:48:38 PM12/10/09
to Prashanth Sundaram, perl...@perl.org
Prashanth Sundaram wrote on 12/10/09 4:59 PM:

Net::LDAP::Class should make most if not all of those requirements pretty easy
to implement. It was written to ease keeping a rdbms, LDAP and AD all in sync.


--
Peter Karman . http://peknet.com/ . pe...@peknet.com

Peter Karman

unread,
Dec 10, 2009, 7:18:43 PM12/10/09
to Prashanth Sundaram, perl...@perl.org
Peter Karman wrote on 12/10/09 5:48 PM:

I suppose I should back that up with some actual code.

I would set it up like this. These files:

lib/MyLDAP/User.pm
lib/MyLDAP/Group.pm
lib/MyAD/User.pm
lib/MyAD/Group.pm

Containing code like this. (NOTE the code is *NOT* tested).

lib/MyLDAP/User.pm:

package MyLDAP::User;
use base qw( Net::LDAP::Class::User::POSIX );

__PACKAGE__->metadata->setup(
base_dn => 'dc=yourcompany,dc=com',
attributes => __PACKAGE__->POSIX_attributes,
unique_attributes => __PACKAGE__->POSIX_unique_attributes,
);

sub init_group_class { 'MyLDAP::Group' }

1;

lib/MyLDAP/Group.pm:

package MyLDAP::User;
use base qw( Net::LDAP::Class::Group::POSIX );

__PACKAGE__->metadata->setup(
base_dn => 'dc=yourcompany,dc=com',
attributes => __PACKAGE__->POSIX_attributes,
unique_attributes => __PACKAGE__->POSIX_unique_attributes,
);

sub init_user_class { 'MyLDAP::User' }

1;

lib/MyAD/User.pm:

package MyAD::User;
use base qw( Net::LDAP::Class::User::AD );

__PACKAGE__->metadata->setup(
base_dn => 'dc=yourcompany,dc=com',
attributes => __PACKAGE__->AD_attributes,
unique_attributes => __PACKAGE__->AD_unique_attributes,
);

sub init_group_class { 'MyAD::Group' }

1;

lib/MyAD/Group.pm:

package MyAD::Group;
use base qw( Net::LDAP::Class::Group::AD );

__PACKAGE__->metadata->setup(
base_dn => 'dc=yourcompany,dc=com',
attributes => __PACKAGE__->AD_attributes,
unique_attributes => __PACKAGE__->AD_unique_attributes,
);

sub init_user_class { 'MyAD::User' }

1;

Now you have your .pm class files set up, and you can write a script (or more
likely, multiple scripts, one per action) to use them to handle all your
requirements:

# example to sync between AD and LDAP based on uid/sAMAccountName (username)
use strict;
use warnings;
use lib 'lib';
use Net::LDAP;
use MyLDAP::User;
use MyLDAP::Group;
use MyAD::User;
use MyAD::Group;

# create all the ldap connections and bind
my $ldap_dn = 'your-authenticated-user-dn';
my $ldap_pass = 'your-authenticated-user-password';
my $ldap_host = 'ldap://yourldapserver';
my $ldap = Net::LDAP->new($ldap_host);

my $ad_dn = 'your-authenticated-user-dn';
my $ad_pass = 'your-authenticated-user-password';
my $ad_host = 'ldap://youradserver';
my $ad = Net::LDAP->new($ad_host);

# could use ssl cert here too instead of password
my $mesg = $ldap->bind( $ldap_dn, password => $ldap_pass );
$mesg->code and die Net::LDAP::Class->get_ldap_error($mesg);
$mesg = $ad->bind( $ad_dn, password => $ad_pass );
$mesg->code and die Net::LDAP::Class->get_ldap_error($mesg);

# iterate over all LDAP users, checking if they are in AD
my $num_ldap_checked = MyLDAP::User->act_on_all(
\&check_ldap_users,
{ ldap => $ldap }
);

sub check_ldap_users {
my $ldap_user = shift;
my $ad_user = MyAD::User->new( ldap => $ad, username => "$ldap_user" );
if (!$ad_user->read) {
warn "$ldap_user is not in AD!";

# decide what action should be taken. Delete from ldap?
$ldap_user->delete;

# or create a AD user?
# add them with whatever attributes you need
# see the perldoc for Net::LDAP::Class::User::AD
# ...
# then save to AD
$ad_user->create or die "can't create AD user $ad_user";
}
}

# now do the reverse, checking AD
my $num_ad_checked = MyAD::User->new(
\&check_ad_users,
{ ldap => $ad }
);

sub check_ad_users {
my $ad_user = shift;

# similar to above in check_ldap_users.
}

print "Checked $num_ldap_checked LDAP users and $num_ad_checked AD users\n";
exit(0);

The basic idea is that you can set up .pm classes to handle all the dirty work
of managing user/group relationships, setting attributes, reading/writing from
the server, etc, and then use those classes over and over in your management
scripts.

Darren Young

unread,
Dec 10, 2009, 6:01:34 PM12/10/09
to Prashanth Sundaram, perl...@perl.org
I don't have anything done for that in particular.

However, but for uidnumber I use a object in AD named CN=nextUIDNumber where I store the next uid in the description attribute. Rather than troll through AD and LDAP I just get/set that for the uidnumber attribute.

cou...@linagora.com

unread,
Dec 11, 2009, 3:10:17 AM12/11/09
to Prashanth Sundaram, perl...@perl.org


Hi,

you can maybe have a look at www.lsc-project.org. It is not Perl, but
allows you to create a connector between AD and OpenLDAP easily.

For example, you can follow this tutorial:
http://lsc-project.org/wiki/documentation/tutorials/openldaptoactivedirectory

Cl�ment.

Prashanth Sundaram

unread,
Dec 11, 2009, 5:01:50 PM12/11/09
to Peter Karman, perl...@perl.org
Thanks everyone for the feedback. I am new to scripting and unix world, and
learning my way through.

I want only one way sync from AD --> LDAP(389-ds). The other way has to only
generate a list of users and the AD will approve/reject those. There is no
password sync as it is handled by PAM. I tried the LDAP connector, but it
still needs more development and there are lot of bugs.

I am planning to script this one out and run as a crontab. I was not able to
achieve all my requirements but here it is.

PS: I know this is the worst code you might have ever come across but I am
in the process of breaking down to modules.

Peter: Your idea is great, but I am so new to this that it will take me some
time to implement that.

===========================================================
$user_ldbase = 'ou=people,dc=LDAPDomain,dc=net';
$user_ad="cn=administrator,cn=Users,dc=ADDomain,dc=net";
$user_ld="cn=Directory Manager";
$objectclasses = [ 'person', 'posixAccount', 'top', 'inetorgperson',
'organizationalPerson'];

# attribute defaults
$loginshell='/bin/bash';

# Bind and Initialization
sub init {
system("/bin/stty -echo");
print STDERR "Enter Active Directory password: ";
$passwd_ad=<STDIN>; chop $passwd_ad;
print STDERR "\nEnter Directory Manager password: ";
$passwd_ld=<STDIN>; chop $passwd_ld;
print STDERR "\n";
system("/bin/stty echo");
$ldap_ad = Net::LDAP->new('ADDomain2-svr.ADDomain.net') or die "$@";
$ldap_ld = Net::LDAP->new('centos-lin.LDAPDomain.net', port=>389) or
die "$@";
$ldap_ad->bind( dn => $user_ad, password =>$passwd_ad );
$ldap_ld->bind( dn => $user_ld, password =>$passwd_ld );
$lastuid = 2000;
}

# Get the list of uids to be synced(next available uid for new user)
sub getUid {
my($mesg_ld,$entry, $uid, @uids, $uidnumber_av);
$mesg_ld = $ldap_ld->search ( base => $user_ldbase,filter =>
"(objectclass=posixAccount)",attrs => ['uidNumber'] );
print $mesg_ld->error if $mesg_ld->code;
@entry = $mesg_ld->entries;
for (@entry) {
my $uid = $_->get_value("uidNumber");
if ($uid)
{ $uids[$uid] = 1; }
}
while (1) {
if ($uids[$lastuid])
{ $lastuid++; }
else {
if (($ldap_ld->search(base=>$user_ldbase,filter =>
"(uidNumber=$lastuid)" ))->entry())
{
$uids[$lastuid++] = 1;
}
else
{
$uids[$lastuid] = 1;
$uidnumber_av = $lastuid++;
return $uidnumber_av;
}
}
}
}

# post the AD user to LDAP
sub disableacct {
if (($userAccountControl & 0x0002) != 0x00002)
{ $result = $ldap_ld->modify($entry_pf->dn,changes =>
[delete => ['nsAccountLock' => 'true'],add => ['nsAccountLock' =>
'false'],]);
print "ENABLED account account for $ad_account !\n";
}
else
{ $result = $ldap_ld->modify($entry_pf->dn,changes =>
[delete => ['nsAccountLock' => 'false'],add => ['nsAccountLock' =>
'true'],]);
print "DISABLED account account for $ad_account !\n";
}

if($$result{'resultCode'})
{
print $ad_account, $$result{'errorMessage'},"!\n";
}

print "\n";
}

#Create the AD user with LDAP schema
sub createADUser4LDAP {
my($uidNumber) = @_;
my($entry,$dn,$mesg, $str,$type, $ad_proxyaddress);
$entry = Net::LDAP::Entry->new;
$dn = $ad_account;
$dn = "uid=" . $ad_account . "," . $user_ldbase;
$entry->dn($dn);
$entry->add('objectclass' => $objectclasses);
$entry->add('uid' => $ad_account);
$entry->add('uidNumber' => $uidNumber);
$entry->add('gidNumber' => $uidNumber);
$entry->add('homedirectory' => "/home/$ad_account");
$entry->add('cn' => $ad_name );
$entry->add('gecos' => $ad_account);
$entry->add('sn' => $sn );
$entry->add('givenName' => $givenName);
#$entry->add('description' => $description);
$entry->add('userPassword' => &get_password);
$entry->add('loginShell' => $loginshell);
$entry->add('mail' => $ad_mail);
if ($userAccountControl & 0x0002 != 0x00002)
{ $entry->add('nsAccountLock' => "false"); }
else
{ $entry->add('nsAccountLock' => "true"); }
$mesg = $entry->update( $ldap_ld );
print $mesg->error,"\n" if $mesg->code;
print "create $dn\n";
return $mesg->code;
}

# Perform a search
sub process {
$mesg_ad = $ldap_ad->search ( base =>
"OU=Users,OU=Corporate,DC=ADDomain,DC=net",filter =>
"(objectClass=person)");
$entries = $mesg_ad->count;
if ($entries lt 1)
{
print "entries=0 \n";
exit 1;
}
foreach my $entry ( $mesg_ad->entries )
{
$dn = $entry->dn;
$ad_account= $entry->get_value( "sAMAccountName" );
@ad_proxyAddresses = $entry->get_value( "proxyAddresses" );
$ad_name = $entry->get_value( "name" );
$sn = $entry->get_value( "sn" );
$givenName = $entry->get_value( "givenName" );
$description = $entry->get_value( "description" );
$userAccountControl = $entry->get_value("userAccountControl");
$ad_mail = $entry->get_value( "mail" );
next if(grep(/$ad_account/i, @ignore_users));
$mesg_ld = $ldap_ld->search ( base => $user_ldbase,filter =>
"(&(objectClass=posixAccount)(uid=$ad_account))" );
print "working on user: ",$ad_account,"\n";
if( $mesg_ld->count)
{
print "found user $ad_account on ldap no changes made\n";
$entry_pf = ($mesg_ld->entries)[0];
&disableacct($userAccountControl, $ad_account);
}
elsif ($map_user_list{$ad_account})
{
$mesg_ld = $ldap_ld->search
(base=>$user_ldbase,filter=>"(&(objectClass=posixAccount)(uid=$map_user_list
{$ad_account}))");
if( $mesg_ld->count)
{
$entry_pf = ($mesg_ld->entries)[0];
print "found user to ADD to LDAP
$ad_account/$map_user_list{$ad_account}\n";
&disableacct($userAccountControl, $map_user_list{$ad_account});
}
}
else
{
my($newuidnum);
$newuidnum = &getUid;
print "new userid#: ", $newuidnum, "\n";
&createADUser4LDAP($newuidnum);
}
}
}

#Set password for new user(random pass)
sub get_password {
my ($passname) = @_;
@chars = ('a' .. 'k','m' .. 'z',2..9);
$u{$passname} = "";
for (1 .. 8)
{
$u{$passname} .= $chars[rand @chars];
}
$u{$passname} = enc_passwd($u{$passname});
}


#Encrypted password to md5
sub enc_passwd {
my $password = shift;
my @chars = ( 'A' .. 'Z', 'a' .. 'z', '0' .. '9', '/', '.' );
my $salt = "";
for (1..8)
{
$salt .= $chars[rand @chars];
}
return ("{md5}" . unix_md5_crypt($password,$salt));
}

# Terminate routine
sub termiante {
$ldap_ad->unbind;
$ldap_ld->unbind;
}

&init;
&process;
&termiante;

=================================================

0 new messages