Populating users in Active Directory using Perl with OLE

Occasionally, I’ve had the need to populate sample users into an Active Directory for projects.  There are, no doubt, other ways to do this that are simpler/more direct.  For example, it’s likely incredibly easy using PowerShell (but I have never been a PowerShell person) and even in the Perl world, there are Perl modules to interact with LDAP.  So using OLE may not seem like the ideal way, and that’s probably right, but OLE comes with virtually every Windows Perl distribution I’ve seen (as opposed to LDAP) and it isn’t terribly difficult.  In general, I haven’t seen too many examples of using OLE to access this type of data on Windows systems, so I wanted to write a brief post about how it can be used for those that stumble upon it.  It’s also nice that, by virtue of running through OLE, you don’t need anything special for authentication here, as that will be automatically inherited by your active running user.

First things first

In this example, I’ll assume you’ve at least got an Active Directory set up and that it’s generally fairly vanilla.  We need to import the Win32::OLE bits into our script, which is relatively straightforward

use Win32::OLE 'in';

Next, we need to know a bit about the Active Directory that’s been set up.  If you don’t know the structure of it, you can use a variety of tools including the open source LDAP Explorer or Microsoft’s Sysinternals Active Directory Explorer.  You’ll need:

  • The domain to put things in, in LDAP format
  • The base path to put the new users/groups in
  • To be a user that has access to create users (and groups, if you’re looking to do so) at the given base path
  • To create or use some format of e-mail addresses for the new users
  • An LDAP URL to access the system

So in this example, we’ll assume I’ll be putting users into customdomain.sfdemo.mysite.com (LDAP format of “DC=customsubdomain,DC=sfdemo,DC=mysite,DC=com”) and giving them an e-mail address of “firstname.lastname@mysite.com”.  The base path will be “OU=UserGroup,OU=DemoUsers,DC=customsubdomain,DC=sfdemo,DC=mysite,DC=com”

So let’s go define these in Perl

my $domain = 'DC=customsubdomain,DC=sfdemo,DC=mysite,DC=com';
my $newpassword = 'pa55w0rd!';
my $emaildomain = 'mysite.com';
my $basepath = 'OU=UserGroup,OU=DemoUsers,' . $domain;
my $ldaphost = '';
my $ADsPath = 'LDAP://' . $ldaphost . '/' . $basepath;

Once we have this all defined, we can go grab the OU object and use it later

my $ou = Win32::OLE->GetObject($ADsPath) or die "Unable to get $ADsPath\n";

So to start out with, we’ll do something pretty easy: adding a security group.  Let’s say we want to call this group “LegalUsers”.  At its simplest, adding the group is just

  1. Creating the group
  2. Assigning an the “sAMAccountName” property to it for Active Directory
  3. Updating (saving) the object:
my $securitygroup = 'LegalUsers';
my $newgroup;
$newgroup = $ou->Create('group','CN=' . $securitygroup);
$newgroup->{sAMAccountName} = $securitygroup; #add the sAMAccountName property
$newgroup->SetInfo( ); #save the object
$newgroup = Win32::OLE->GetObject('LDAP://' . $securitygroup . ',' . $basepath) or die "Unable to find OU\n"; #retrieve the updated object for later reference

More complex: Adding a user

The only thing that makes adding users more complex is that there are more fields to add.  We need to add the name and set the path of the user in AD and add an e-mail, but may also want to give them a default password, change their account expiration properties, assign them to groups, etc.  Let’s walk through an example of this…

#For this example, we'll show 1 particular username.  The bulk import later will make more sense with this
#Although the bulk import will just take a flat 1-line-per-username format, a CSV could be devised and used instead
my $username = 'shane.connelly';
my $email = $username . "\@$emaildomain"; #our new e-mail, as in shane.connelly@mysite.com
my @namearray = split(/\./, $username);

my $realname = $username;
my $fullname = join(' ', map (ucfirst, @namearray)); #name will now be upper-cased first letters with a space as in "Shane Connelly"
my $initials = uc(join('', map (substr($_,0,1), @namearray))); #initials will now be the first letters of each word, as in "SC"
$initials = substr($initials,1,length($initials) - 2);
my $lastname = ucfirst($namearray[$#namearray]);

print "Adding $fullname\n";

#As with the groups, simply creating a user is fairly easy.
#The only thing that really makes this different than any other LDAP import is that most AD systems have some specific schema as to the field names you use
#The below works with that schema
my $u = $ou->Create("user", 'cn=' . $fullname) or die "Unable to create user\n";
$u->{sAMAccountName} = $username; #as with the groups, we need a sAMAccountName and this needs to be unique
$u->{company} = 'XYZ Corporation';
$u->{initials} = $initials if ($initials);
$u->{displayName} = $fullname;
$u->{name} = $fullname;
$u->{givenName} = ucfirst($namearray[0]);
$u->{sn} = $lastname; #sn = surname
$u->{mail} = $email;
$u->{mobile} = ('(' .(100 + int(rand(800))) . ') ' . (100 + int(rand(800))) . '-' . (1000 + int(rand(8000)))); #create some random phone number for demonstration purposes
$u->{co} = 'United States'; #co = country
$u->{c} = 'US'; #c = country code
$u->SetInfo(  );

#We'll give the user a default password.  For this example, every user will have the same password, which is obviously quite insecure
$u = Win32::OLE->GetObject('LDAP://' . $fullname . ',' . $basepath) or die "Unable to find last added user\n";
$u->SetInfo(  );

#Set some flags for the user
#For this example, we'll say "Password doesn't expire" (0x10000 or 65536 in decimal) and make it a "Normal account" (0x0200 or 512 in decimal)
#Just sum up the flags (0x10000 + 0x0200 = 0x10200 or 65536+512 = 66048 in decimal)
#There are a number of flags you can add to a user account, available at http://support.microsoft.com/kb/305144
$u->{userAccountControl} = '66048';
$u->SetInfo(  );

#perhaps we want to also add the user to the "Remote Desktop Users" group
my $g = Win32::OLE->GetObject('LDAP:// Desktop Users,CN=Builtin,' . $domain) or die "Unable to find last remote desktop group\n";
$g->Add('LDAP://cn=' . $fullname . ',' . $basepath);
$newgroup->Add('LDAP://cn=' . $fullname . ',' . $basepath) if ($newgroup);

Tying it all together

For creating demo users, I’d often simply create a flat file with usernames in it and read through them.  You can also add managers, cities, and addresses for a more “real-looking” scenario.

use strict;
use Win32::OLE 'in';

##### CONFIG #####

my $domain = 'DC=customsubdomain,DC=sfdemo,DC=mysite,DC=com';
my $newpassword = 'pa55w0rd!';
my $emaildomain = 'mysite.com';
my $basepath = 'OU=UserGroup,OU=DemoUsers,' . $domain;
my $securitygroup = 'LegalUsers';
my @cities = ('New York:New York','Los Angeles:California','Houston:Texas','Philadelphia:Pennsylvania','Phoenix:Arizona','San Antonio:Texas',
			  'San Diego:California','Dallas:Texas','San Jose:California','Jacksonville:Florida','Indianapolis:Indiana','San Francisco:California',
			  'Austin:Texas','Columbus:Ohio','Fort Worth:Texas','Charlotte:North Carolina','Detroit:Michigan');
my @streettypes = ('Avenue','Street','Circle');
my @streetnames = ('Second','Third','First','Fourth','Park','Fifth','Main','Sixth','Oak','Pine','Maple','View','Washington','Lake','Hill','Cedar','Stuart','Smith');

my @managers = ('CN=Ally Mcbeal,OU=UserGroup,OU=DemoUsers,DC=customsubdomain,DC=SFDemo,DC=MySite,DC=com',
				'CN=Shane Connelly,OU=UserGroup,OU=DemoUsers,DC=customsubdomain,DC=SFDemo,DC=MySite,DC=com');
##### END CONFIG #####

my $ADsPath = 'LDAP://' . $basepath;
my $ou = Win32::OLE->GetObject($ADsPath) or die "Unable to get $ADsPath\n";

my $newgroup;
if ($securitygroup)
  $newgroup = $ou->Create('group','CN=' . $securitygroup);
  $newgroup->{sAMAccountName} = $securitygroup;
  $newgroup->SetInfo( );
  $newgroup = Win32::OLE->GetObject('LDAP://' . $securitygroup . ',' . $basepath) or die "Unable to find OU\n";

while (<USRS>)
	my $username = $_;
	$username =~ s/\_/\./g;
	my $email = $username . "\@$emaildomain";
	my %altemails = ();
	my @namearray = split(/\./, $username);
	my $realname = $username;
	my $fullname = join(' ', map (ucfirst, @namearray));
	my $initials = uc(join('', map (substr($_,0,1), @namearray)));
	$initials = substr($initials,1,length($initials) - 2);
	my $lastname = ucfirst($namearray[$#namearray]);
	print "Adding $fullname\n";
	my $u = $ou->Create("user", 'cn=' . $fullname) or die "Unable to create user\n";
	$u->{sAMAccountName} = $username;
	$u->{company} = 'XYZ Corporation';
	$u->{initials} = $initials if ($initials);
	$u->{displayName} = $fullname;
	$u->{name} = $fullname;
	$u->{givenName} = ucfirst($namearray[0]);
	$u->{sn} = $lastname;
	$u->{mail} = $email;
	$u->{mobile} = ('(' .(100 + int(rand(800))) . ') ' . (100 + int(rand(800))) . '-' . (1000 + int(rand(8000))));
	$u->{co} = 'United States';
	$u->{c} = 'US';
	my $userlocation = $cities[rand @cities];
	my ($city, $state) = split(":", $userlocation);
	$u->{st} = $state;
	$u->{l} = $city;
	$u->{streetAddress} = int(rand(9999)) . ' ' . $streetnames[rand @streetnames] . ' ' . $streettypes[rand @streettypes];
	$u->{postalCode} = int(rand(99999));
	$u->{userPrincipalName} = $email;
	$u->{manager} = $managers[rand @managers];
	$u->SetInfo(  );
	$u = Win32::OLE->GetObject('LDAP://' . $fullname . ',' . $basepath) or die "Unable to find last added user\n";
	$u->SetInfo(  );
	$u->{userAccountControl} = '66048';
	$u->SetInfo(  );
	my $g = Win32::OLE->GetObject('LDAP:// Desktop Users,CN=Builtin,' . $domain) or die "Unable to find last remote desktop group\n";
	$g->Add('LDAP://cn=' . $fullname . ',' . $basepath);
	$newgroup->Add('LDAP://cn=' . $fullname . ',' . $basepath) if ($newgroup);

Antenna Design Genetic Algorithm


For certain classes of antennas, e.g. Yagi-Uda antennas, the design characteristics have no known “best case” numeric values.  That is, if you want to design a Yagi-Uda antenna for a particular frequency, there is no known numeric solution for the width of the dipoles, number of dipoles, and distance between each dipole in order to achieve the highest gain.  Instead, people rely on experimental evidence: the designs of a number of common frequencies have been tested in the field to produce certain amount of gain, so if you know what frequency you’re looking to design for, you go look up the tables based upon what others have tested for.  If you’re looking for an entirely unique frequency, you have to go experiment yourself.

New Approach

I wrote a MatLab program to use a genetic algorithm to modify the parameters of a antenna and eventually “give birth” to a “best known case” antenna based upon forward gain, etc. Currently, it uses NEC2 (Numerical Electromagnetics Code 2) as the processing engine. It writes out a text file to disk, then calls NEC2 to process that file.  This allows us to try a number of unknown antenna designs and permute possible solutions.  It can be run in a distributed fashion, with each machine “phoning home” to a central database which then redistributes the best-known designs to the worker machines.

The output is something like the following, which shows the forward gain of a given design through a set of frequencies

Output from a forward gain analysis


Genetic Algorithm:

function [beta,stopcode]=ga(funstr,parspace,options,p1,p2,p3,p4,p5,p6,p7,p8,p9)
% Genetic Algorithm for function maximization.
%  beta       = (1 x K) parameter vector maximizing funstr
%  stopcode   = code for terminating condition
%                == 1 if terminated normally
%                == 2 if maximum number of iterations exceeded
%  funstr     = name of function to be maximized (string).
%  parspace   = (2 x K) matrix is [min; max] of parameter space dimensions
%               or, if (3 x K), then bottom row is a good starting value
%  options    = vector of option settings
%  p1,p2,...,p9 are optional parameters to be passed to funstr
% where:
% options(1) = m (size of generation, must be even integer)
% options(2) = eta (crossover rate in (0,1); use Booker's VCO if < 0)
% options(3) = gamma (mutation rate in (0,1))
% options(4) = printcnt (print status once every printcnt iterations)
%                Set printcnt to zero to suppress printout.
% options(5) = maxiter (maximum number of iterations)
% options(6) = stopiter (minimum number of gains < epsln before stop)
% options(7) = epsln (smallest gain worth recognizing)
% options(8) = rplcbest (every rplcbest iterations, insert best-so-far)
% options(9) = 1 if function is vectorized (i.e., if the function
%                can simultaneously evaluate many parameter vectors).
%    Default option settings: [20,-1,0.12,10,20000,2000,1e-4,50,0]
% Note: 
%    The function is maximized with respect to its first parameter,
%    which is expressed as a row vector.
%    Example: 
%      Say we want to maximize function f with respect to vector p,
%      and need also to pass to f data matrices x,y,z.  Then,
%      write the function f so it is called as f(p,x,y,z).  GA will
%      assume that p is a row vector.

months = ['Jan';'Feb';'Mar';'Apr';'May';'Jun';...

if nargin>2
   if isempty(options)
m=options(1); eta=options(2); gam=options(3);
stopiter=options(6); epsln=options(7);

% Use Booker's VCO if eta==-1
vco=(eta<0);  eta=abs(eta);

% Cancel rplcbest if <=0
if rplcbest<=0, rplcbest=maxiter+1; end


% Draw initial Generation
if b0rows>0
  parspace=parspace([1 2],:);

% Initial 'best' holders
bestfun=-Inf; beta=zeros(1,K);

% Score for each of m vectors

% Setup function string for evaluations
evalstr = [funstr,'(G'];
if ~vecfun
        evalstr=[evalstr, '(i,:)'];
if nargin>3, evalstr=[evalstr,paramstr(1:3*(nargin-3))]; end
evalstr = [evalstr, ')'];

% Print header
if printcnt>0
   disp(['Maximization of function ',funstr])
   disp('i      = Current generation')
   disp('best_i = Best function value in generation i')
   disp('best   = Best function value so far')
   disp('miss   = Number of generations since last hit')
   disp('psi    = Proportion of unique genomes in generation')
   disp(sprintf(['\n',blanks(20),'i     best_i        best     miss   psi']))

iter=0;  stopcode=0;
oldpsi=1;  % for VCO option
while stopcode==0
   % Call function for each vector in G
   if vecfun
     for i=1:m
   bf=max([bf0 bestfun]);
   if fgain>epsln
   if fgain>0
   if printcnt>0 & rem(iter,printcnt)==1
        ckhr=int2str(ck(4)+100);  ckday=int2str(ck(3)+100);
        ckmin=int2str(ck(5)+100); cksec=int2str(ck(6)+100);
        timestamp=[ckday(2:3),months(ck(2),:),' ',...
           ckhr(2:3),':',ckmin(2:3),':',cksec(2:3),' '];
        disp([timestamp,sprintf('%6.0f %8.5e %8.5e %5.0f %5.3f',...
                [iter bf0 bestfun inarow psi])])
        save gabest beta timestamp iter funstr
   % Reproduction
   r=rand(1,m); r=sum(r(ones(m,1),:)>pcum(:,ones(1,m)))+1;
   % Crossover
   if vco
        eta=max([0.2 min([1,eta-psi+oldpsi])]);
   if y>0
     % choose crossover point
     for i=1:y
   % Mutation
   % Once every rplcbest iterations, re-insert best beta
   if rem(iter,rplcbest)==0

if printcnt>0
   if stopcode==1
        disp(sprintf('GA: No improvement in %5.0f generations.\n',stopiter))
        disp(sprintf('GA: Maximum number of iterations exceeded.\n'))
% end of GA.M
function [gain_t]=Yagi(p)
% This program is used with a getic optimization code.
% It creates an NEC input file given the parameters for a 3 element YAGI
% antenna.  Then, it runs NEC and reads in the parameters(Gain, Impedance, etc) generated by NEC.
%=========Create NEC input file========================================
D=0.0085; % diamter of elements in wavelengths
Lr=p(1); %perimeter of reflector
Ls=p(2); %perimeter of driven element
Ld=p(3); %perimeter of director
Sr=-p(4); %location of reflector
Sd=p(5);  %location of director
%    Geometry input for NEC

rsl = Lr / 4; %reflector single side length: square loop, one side = permiter / 4
ssl = Ls / 4; %driven single side length: square loop, one side = permiter / 4
dsl = Lr / 4; %director single side length: square loop, one side = permiter / 4
num_segments_per_side = 7;

fprintf(FID_nec,strcat('CM UDA-YAGI SQUARE LOOP ANTENNA','\n'));
fprintf(FID_nec,strcat('CE FIle Generated by MatLab','\n'));
fprintf(FID_nec,'GW %3i %3i %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f\n',1,num_segments_per_side,Sr,0,-Lr/2,Sr,0,Lr/2,R); %Reflector
fprintf(FID_nec,'GW %3i %3i %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f\n',2,num_segments_per_side,0 ,0,-Ls/2, 0,0,Ls/2,R); %Driven Element
fprintf(FID_nec,'GW %3i %3i %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f\n',3,num_segments_per_side,Sd,0,-Ld/2,Sd,0,Ld/2,R); %Director
%    Program Control Commands for NEC
fprintf(FID_nec,'EX %3i %3i %3i %3i %8.4f %8.4f\n',0,2,4,0,1,0);%Exitation Command wire 2 segment 4
fprintf(FID_nec,'FR %3i %3i %3i %3i %8.4f %8.4f\n',0,1,0,0,2400,0);%set freq to 299.8 MHz so wavelength will be 1m
fprintf(FID_nec,'RP %3i %3i %3i %3i %8.4f %8.4f %8.4f %8.4f\n',0,1,1,1000,90,0,0,0);%calculate gain at boresite
%=======Create file to pipe to NEC ===================================
%=======Run NEC======================================================
!NEC2Dx500 < input_CMD >tmp;
%=======Read Data form NEC output file===============================
[freq,Z,gain_t,E_theta,E_phi,n_freq_meas,run_time] = nec_read('NEC.out');