openldap wrapper script:

Title:

ldap: wrapper script

Author:

Douglas O’Leary <dkoleary@olearycomputers.com>

Description:

ldap: wrapper script for mundane, repetetive tasks

Disclaimer:

Standard: Use the information that follows at your own risk. If you screw up a system, don’t blame it on me…

Overview:

NOTE: I discovered a minor logic bug in that a simple add user will not set the member attribute in the group to which the user is getting added. This screws up the memberof searches which are enabled via the rfc2307bis schema. That’s workable via another call to the ldap script but shouldn’t be required. That’ll be fixed in a later version.

LDAP Directory Interchange Format (LDIF) is obviously a fact of life for LDAP maintenance. That being said, there’re some activities that are so ubiquitous and mundane that they should be scripted.

So, I did.

The script handles:

  • User and group additions and deletions.

  • Adding (simple) and deleting distinguished names.

  • Reasonably simple searchs. If you’re using a complex filter, use ldapsearch.

  • Password/Account resets, locks, and unlocks.

  • Reasonably simple entry edits. If you’re adding entire branches or need to modify multiple attributes simultaneously, go back to the LDIF. This is good for modifying account attributes, modifying group membership, etc.

It’s primary purpose is to circumvent the need for repetetive LDIF files and save some typing on ldapsearch.

It’s a perl script using the Net::LDAP module which doesn’t come standard but can be installed via rpm. yum -y install perl-LDAP

The Net::LDAP::FAQ is stock full of useful tips/tricks and hints on how to use this wonderful module.

For my purposes, having the password reset and other modification functions all in one script makes sense. I’m a single man shop running ldap as a learning tool. Since the script binds to the ldap server as the admin user, it’s probably not the best idea to have it used as a production password reset that operations handles unless the script is protected via root read-only privileges and sudo access.

As other lessons learned entries attest, I’ve been experimenting with posix and groupofnames style groups and have found, so far at least, that the rfc2307bis implementation of posixgroups is quite a bit more flexible. That means, however, that the group definitions are no longer globally applicable.

Short version: if you’re using rfc2307bis style groups, the script should be good to go. If you’re not, set the bis flag variable to zero (0) on/about line 407. Also, note, if you’re not using rfc2307bis style groups, the memberof searches won’t return anything.

This script is written for openldap. If you’re using a different directory server software, there’s a good chance that you’ll have to translate the various attributes. For instance, openldap tracks when accounts got locked via the pwdAccountLockedTime attribute. FDS and unboundID track when an account will get unlocked via the accountunlocktime attribute. I would think the algorithms used to update the directory entries are still good. I know, for instance, that password resets, simple modifications, and searches work on FDS and unboundID. Anything outside of that, you’ll have to find out on your own.

Configuration:

Bind info:

To migrate this script, the biggest thing that should be updated is the bind dn, password, users and groups dn, the default base, and ldap server (on/about line 348):

our $rootdn  = 'cn=admin,dc=oci,dc=com';
our $rootpw  = 'not_really_my_pwd';
our $base    = 'dc=oci,dc=com';
our $userdn  = "ou=users,$base";
our $groupdn = "ou=groups,$base";
our $lsvr    = 'ldaps://ldapsvr.olearycomputers.com';

Defaults:

  • New users’ passwords default to 1changeme unless otherwise specified. (on/about line 391)

  • New users’ group will default to ldap-users unless otherwise specified. (on/about line 243)

  • New users’ UID will default to 282 or higher unless otherwise specified. (on/about line 265).

  • Automatic UID detection will find and use the first un-used UID starting incrementally at 601.

Usage:

# ldap

Add, delete, enable, lock, modify, reset, or search: Identify an action!
Format:
  ldap [ options ] [ arguments to options ]
     -d $dn                                      # delete a specific dn
     -s $filter                                  # search: eg 'uid=qwer'
     -a ${add_options}                           # add an entry
         -group $group -desc $desc               # add a group
                  OR
         -user $user -gecos $gecos -pwd $pwd \   # add a user
           [ -shell $shell  -min $min -max $max -warn $warn ]
                  OR
     [-l | -e] -user ${user}                     # Lock/enable user account
     -r -user ${user} [-f]                       # reset user pwd
                                                 #    w/optional force arg
     -m -dn ${dn} [add|replace|delete] ${p}=${v} # Modify a dn
  • To search:

    # ldap -search uid=doleary
    ------------------------------------------------------------------------
    dn:uid=doleary,ou=users,dc=oci,dc=com
    
               cn: doleary
            gecos: Doug OLeary
      objectClass: top
                   account
                   posixAccount
                   shadowAccount
        shadowMin: 0
        shadowMax: 90
    shadowWarning: 7
       loginShell: /bin/bash
        uidNumber: 601
        gidNumber: 614
    homeDirectory: /home/doleary
              uid: doleary
         memberOf: cn=infra,ou=groups,dc=oci,dc=com
                   cn=ldap-Administrators,ou=groups,dc=oci,dc=com
                   cn=infosec,ou=groups,dc=oci,dc=com
                   cn=dba,ou=groups,dc=oci,dc=com
     userPassword: [[snipped]]
    

    Additional parameters, pwdAccountLockedTime, pwdReset, pwdChangedTime, and memberof are displayed, if present, in the users entry. Note that memberof values won’t be displayed if you’re not using the rfc2307bis schema.

    Memberof search (only applicable to rfc2307bis schema users):

    # ldap -mof -search uid=doleary
    cn=infra,ou=groups,dc=oci,dc=com
    cn=ldap-Administrators,ou=groups,dc=oci,dc=com
    cn=infosec,ou=groups,dc=oci,dc=com
    cn=dba,ou=groups,dc=oci,dc=com
    

    Other examples:

    # ldap -search uidnumber=621
    ------------------------------------------------------------------------
    dn:uid=aadc,ou=users,dc=oci,dc=com
    
                cn: aadc
             gecos: aadc test user
       objectClass: top
                    account
                    posixAccount
                    shadowAccount
         shadowMin: 0
         shadowMax: 90
     shadowWarning: 7
        loginShell: /bin/bash
         uidNumber: 621
         gidNumber: 614
     homeDirectory: /home/aadc
               uid: aadc
      userPassword: [[snipped]]
    pwdChangedTime: 20140119174946Z
          pwdReset: TRUE
    
    
    # ldap -search cn=ldap-users
    ------------------------------------------------------------------------
    dn:cn=ldap-users,ou=groups,dc=oci,dc=com
    
             cn: ldap-users
    objectClass: top
                 posixGroup
      gidNumber: 614
    description: LDAP Users
    

    NOTE: if the display shows pwdAccountLockedTime, the account is locked.

    # ldap -search uid=aaaa
    ------------------------------------------------------------------------
    dn:uid=aaaa,ou=users,dc=oci,dc=com
    
                      cn: aaaa
                   gecos: aaaa test user
             objectClass: top
                          account
                          posixAccount
                          shadowAccount
               shadowMin: 0
               shadowMax: 90
           shadowWarning: 7
              loginShell: /bin/bash
               uidNumber: 604
               gidNumber: 614
           homeDirectory: /home/aaaa
                     uid: aaaa
            userPassword: [[snip]]
    pwdAccountLockedTime: 20140118203457Z
    
  • To delete an entry:

    # ldap --delete cn=cac,ou=users,dc=oci,dc=com
    # ldap --delete cn=ldap-users,ou=groups,dc=oci,dc=com
    # ldap --search cn=ldap-users
    # ldap --search uid=cac
    
  • To add an entry:

    # ldap -add -group ldap-users -desc 'LDAP Users' -gid 614
    # ldap --search cn=ldap-users
    ------------------------------------------------------------------------
    dn:cn=ldap-users,ou=groups,dc=oci,dc=com
    
             cn: ldap-users
    objectClass: top
                 posixGroup
      gidNumber: 614
    description: LDAP Users
    
    # ldap -add -user cac -gecos 'cac test user'
    # ldap --search uid=cac
    ------------------------------------------------------------------------
    dn:cn=cac,ou=users,dc=oci,dc=com
    
              uid: cac
            gecos: cac test user
      objectClass: top
                   account
                   posixAccount
                   shadowAccount
        shadowMin: 0
        shadowMax: 90
    shadowWarning: 7
       loginShell: /bin/bash
        uidNumber: 621
        gidNumber: 614
    homeDirectory: /home/cac
               cn: cac
     userPassword: [[snipped]]
    
  • To administratively lock an account:

    # ldap -search uid=aaaa | grep -i pwdaccount
    # ldap -l -user aaaa
    alter: add -> pwdAccountLockedTime -> 000001010000Z: done
    # ldap -search uid=aaaa | grep -i pwdaccount
    pwdAccountLockedTime: 000001010000Z
    
  • To Reenable an account:

    # ldap -search uid=aaaa | grep -i pwdaccount
    pwdAccountLockedTime: 20140118203457Z
    # ldap -e -user aaaa
    Account enabled: aaaa
    # ldap -search uid=aaaa | grep -i pwdaccount
    
  • To reset a user’s password:

    # ldap -search uid=aa | grep -i pwdchangedtime
    pwdChangedTime: 20140119174943Z
    # ldap -reset -user aa -pwd 1changeme
    User password reset: aa
    # ldap -search uid=aa | grep -i pwdchangedtime
    pwdChangedTime: 20140119214649Z
    
  • To reset a user’s password and force a password change on next login:

    # ldap -reset -user aa -pwd 1changeme -f
    User password reset w/force option: aa
    # ldap -search uid=aa
    ------------------------------------------------------------------------
    dn:uid=aa,ou=users,dc=oci,dc=com
    
                cn: aa
             gecos: aa test user
       objectClass: top
                    account
                    posixAccount
                    shadowAccount
         shadowMin: 0
         shadowMax: 90
     shadowWarning: 7
        loginShell: /bin/bash
         uidNumber: 602
         gidNumber: 614
     homeDirectory: /home/aa
               uid: aa
      userPassword: [[snipped]]
    pwdChangedTime: 20140119214749Z
          pwdReset: TRUE
    

    The pwdReset parameter is the forced password change flag.

  • The ldap script now supports simple modifications to distinguished names. When I say simple, I mean exactly that. The script will add, replace, or delete individual attributes from an entry. It won’t check to see if what you’re trying to alter makes any sense or even if it’ll blow up your entire directory. As in all things where your login ID has special privileges, be very careful. Some use cases to which I’ve put this functionality in my studies:

    • To remove the pwdReset parameter, if unintentionally applied:

      # ldap -search uid=aa | grep -i pwdreset
            pwdReset: TRUE
      # ldap -modify -dn uid=aa,ou=users,dc=oci,dc=com delete pwdReset=TRUE
      alter: delete -> pwdReset -> TRUE: done
      # ldap -search uid=aa
      ------------------------------------------------------------------------
      dn:uid=aa,ou=users,dc=oci,dc=com
      
                  cn: aa
               gecos: aa test user
         objectClass: top
                      account
                      posixAccount
                      shadowAccount
           shadowMin: 0
           shadowMax: 90
       shadowWarning: 7
          loginShell: /bin/bash
           uidNumber: 602
           gidNumber: 614
       homeDirectory: /home/aa
                 uid: aa
        userPassword: [[snipped]]
      pwdChangedTime: 20140119214749Z
      

      Note: the pwdChangedTime parameter cannot be altered, irritatingly enough.

    • To change an entry’s shadowmax parameter to a week:

      # ldap -modify -dn uid=aa,ou=users,dc=oci,dc=com replace shadowmax=7
      alter: replace -> shadowmax -> 7: done
      # ldap -search uid=aa | grep -i shadowmax
           shadowMax: 7
      
    • To change the default ppolicy pwdmaxage parameter from 90 days to an hour (useful for testing pwd expiration):

      # ldap -search cn=default
      ------------------------------------------------------------------------
      dn:cn=default,ou=policies,dc=oci,dc=com
      
                           cn: default
                  objectClass: top
                               device
                               pwdPolicyChecker
                               pwdPolicy
                 pwdAttribute: userPassword
                 pwdInHistory: 2
                 pwdMinLength: 8
                pwdMaxFailure: 3
      pwdFailureCountInterval: 900
              pwdCheckQuality: 0
                pwdMustChange: TRUE
           pwdGraceAuthNLimit: 0
             pwdExpireWarning: 604800
           pwdLockoutDuration: 1800
                   pwdLockout: TRUE
                    pwdMaxAge: 7776000
      # ldap -modify -dn cn=default,ou=policies,dc=oci,dc=com replace pwdmaxage=3600
      alter: replace -> pwdmaxage -> 3600: done
      # ldap -search cn=default | grep -i pwdmaxage
                    pwdMaxAge: 3600
      
  • To add/delete group membership, use the modify functionality:

    • posixgroup style:

      # ldap -modify -dn cn=infra,ou=groups,dc=oci,dc=com add memberuid=ddddd
      alter: add -> memberuid -> ddddd: done
      # ldap -search cn=infra
      ------------------------------------------------------------------------
      dn:cn=infra,ou=groups,dc=oci,dc=com
      
               cn: infra
      objectClass: top
                   posixGroup
        gidNumber: 635
      description:  System Admins
        memberUid: d
                   dd
                   ddd
                   dddd
                   ddddd
      # ldap -modify -dn cn=infra,ou=groups,dc=oci,dc=com delete memberuid=ddddd
      alter: delete -> memberuid -> ddddd: done
      # ldap -search cn=infra
      ------------------------------------------------------------------------
      dn:cn=infra,ou=groups,dc=oci,dc=com
      
               cn: infra
      objectClass: top
                   posixGroup
        gidNumber: 635
      description:  System Admins
        memberUid: d
                   dd
                   ddd
                   dddd
      
    • groupofnames/rfc2307bis style:

      # ldap -mof -search uid=doleary
      cn=infra,ou=groups,dc=oci,dc=com
      cn=ldap-Administrators,ou=groups,dc=oci,dc=com
      cn=infosec,ou=groups,dc=oci,dc=com
      cn=dba,ou=groups,dc=oci,dc=com
      
      # ldap -mod -dn cn=weblogic,ou=groups,dc=oci,dc=com add \
      member=uid=doleary,ou=users,dc=oci,dc=com
      alter: add -> member -> uid=doleary,ou=users,dc=oci,dc=com: done
      
      # ldap -mof -search uid=doleary
      cn=infra,ou=groups,dc=oci,dc=com
      cn=ldap-Administrators,ou=groups,dc=oci,dc=com
      cn=infosec,ou=groups,dc=oci,dc=com
      cn=dba,ou=groups,dc=oci,dc=com
      cn=weblogic,ou=groups,dc=oci,dc=com
      

Summary:

LDIF is and always will be part of ldap maintenance; however, these mundane, routine, and repetitive should definitely be automated. If you don’t use the script itself, hopefully, it’ll help generate some ideas for your own automation.

The last promised update, providing an option for an alternate bind dn and password probably won’t be fortcomcing any time soon. If you want to take a hack at it, feel free. As it stands, the script does what I need it to do so I can continue with my research without having to play with redundant LDIF overly much.

Script:

The script is available at http://www.olearycomputers.com/ll/ldap/ldap or you can copy/paste the 430 some odd lines below.

#!/usr/bin/perl

#####################################################################################
# ldap:          Generic wrapper script for the more mundane ldapadd/modify/delete
#                requirements.  Adds/deletes users and groups and does searches.
#                Authenticates as the directory admin so some care should be
#                exercised.
# Author:        Doug O'Leary
# Created:       01/09/14
#
#####################################################################################

use Net::LDAP;
use Net::LDAP::Extension::SetPassword;
use Getopt::Long;

#####################################################################################
# Functions
#####################################################################################

sub add_group
{   my ($bis, $gid, $group, $desc) = @_;

    my $ldap = Net::LDAP->new($lsvr) or die "Can't bind to ldap!\n";
    my $def_mem = "cn=admin,dc=oci,dc=com";

    $ldap->bind(
        dn       => $rootdn,
        password => $rootpw,
    );

    if ($bis)
    {   # interesting: order of the attributes appears to be important.  This was failing
        # until I moved member directly below the objectclass statements.
        my $result = $ldap->add(
                        dn   => "cn=$group,$groupdn",
                        attr => [   'cn'          => $group,
                                    'objectclass' => 'top',
                                    'objectclass' => 'groupofNames',
                                    'objectclass' => 'posixgroup',
                                    'member'      => $def_mem,
                                    'description' => $desc,
                                    'gidNumber'   => $gid,
                        ]);
    }
    else
    {   my $result = $ldap->add(
                        dn   => "cn=$group,$groupdn",
                        attr => [   'cn'          => $group,
                                    'objectclass' => 'top',
                                    'objectclass' => 'posixgroup',
                                    'gidNumber'   => $gid,
                                    'description' => $desc,
                        ]);
    }
    if ($result->code)
    {   my $msg = "failed to add entry: " . $result->error;
        usage($msg);
    }
}

sub add_user
{   my ($user, $uid, $gid, $gecos, $pwd, $shell, $min, $max, $warn) = @_;

    my $ldap = Net::LDAP->new($lsvr) or die "Can't bind to ldap!\n";
    $ldap->bind(
        dn       => $rootdn,
        password => $rootpw,
    );

    my $result = $ldap->add(
        dn   => "uid=$user,$userdn",
        attr    => ['cn'               => $user,
                    'gecos'            => $gecos,
                    'objectclass'      => 'top',
                    'objectclass'      => 'account',
                    'objectclass'      => 'posixaccount',
                    'objectclass'      => 'shadowaccount',
                    'shadowmin'        => $min,
                    'shadowmax'        => $max,
                    'shadowwarning'    => $warn,
                    'loginshell'       => $shell,
                    'uidnumber'        => $uid,
                    'gidnumber'        => $gid,
                    'homedirectory'    => "/home/$user",
                ]);

    if ($result->code)
    {   my $msg = "failed to add entry: " . $result->error;
        usage($msg);
    }

    $result = $ldap->set_password(
            user      => "uid=$user,$userdn",
            newpasswd => $pwd,
    );
    if ($result->code)
    {   my $msg = "Password update failed: " . $result->error;
        usage($msg);
    }
}

sub delete_entry
{   my $dn = shift;

    my $ldap = Net::LDAP->new($lsvr) or die "Can't bind to ldap!\n";
    $ldap->bind(
        dn       => $rootdn,
        password => $rootpw,
    );

    my $result = $ldap->delete("$dn");
    if ($result->code)
    {   my $msg = "failed to add entry: " . $result->error;
        usage($msg);
    }
}

sub enable_user
{   my $user = shift;

    my $ldap = Net::LDAP->new($lsvr) or die "Can't bind to ldap!\n";
    $ldap->bind(
        dn       => $rootdn,
        password => $rootpw,
    );

    my $dn = "uid=$user,$userdn";
    my $result = $ldap->modify($dn, delete => ['pwdAccountLockedTime']);
    if ($result->code)
    {   my $msg = "failed to enable user: $user: " . $result->error;
        usage($msg);
    }
    else { printf("Account enabled: %s\n", $user); }
}

sub modify_dn
{   my $dn = shift;
    my @args = @{$_[0]};

    my $action = $args[0];
    # my ($attr, $value) = split(/=/, $args[1]);
    my ($attr, $value) = $args[1] =~ m{([^=]*)=(.*)};

    usage("Action must be 'add', 'delete', or 'replace': $action")
        if ($action !~ /add/ && $action !~ /delete/ && $action !~ /replace/);

    my $ldap = Net::LDAP->new($lsvr) or die "Can't bind to ldap!\n";
    $ldap->bind(
        dn       => $rootdn,
        password => $rootpw,
    );
    my $result = $ldap->modify($dn, $action => {$attr => "$value"});
    if ($result->code)
    {   my $msg = "failed to alter attribute: " . $result->error;
        usage($msg);
    }
    else {print "alter: $action -> $attr -> $value: done\n";    }
}

sub reset_pwd
{   my ($user, $pwd, $force) = @_;
    my $slappasswd = '/usr/sbin/slappasswd';
    my $dn = "uid=$user,$userdn";
    my ($result,$msg);

    my $ldap = Net::LDAP->new($lsvr) or die "Can't bind to ldap!\n";
    $ldap->bind(
        dn       => $rootdn,
        password => $rootpw,
    );

    if (defined($pwd))
    {   $result = $ldap->set_password(
            user      => $dn,
            newpasswd => "$pwd"
        );
    }
    else
    {   $result = $ldap->set_password(
            user      => $dn,
        );
    }
    if ($result->code)
    {   my $msg = "failed to set pwd: " . $result->error;
        usage($msg);
    }
    else
    {   if (! defined($force))
        {   (defined($result->gen_password())) ?
                ($msg = "$user:" . $result->gen_password()) :
                ($msg = "$user");
            print "User password reset: $msg\n";
        }
    }

    if (defined($force))
    {   my $result = $ldap->modify($dn, add => { pwdReset => TRUE });
        if ($result->code)
        {   my $msg = "failed to force pwd change: " . $result->error;
            usage($msg);
        }
        else { print "User password reset w/force option: $user\n";  }
    }
}

sub search
{   my ($mof, $filter) = @_;
    my $mesg;

    my $ldap = Net::LDAP->new($lsvr) or die "Can't bind to ldap!\n";
    $ldap->bind(
        dn       => $rootdn,
        password => $rootpw,
    );


    ($mesg) = $ldap->search(
        base   => $base,
        filter => $filter,
        attrs  => [ '*', 'pwdAccountLockedTime', 'pwdReset', 'pwdChangedTime',
                    'pwdMaxAge', 'pwdminage', 'memberof' ],
    );

    if (! $mof)
    {   foreach $entry ($mesg->all_entries)
        {   $entry->dump;   }
    }
    else
    {   my $entry = $mesg->entry(0);
        my @memof = $entry->get_value('memberof');
        foreach my $mem (@memof)
        {   print "$mem\n"; }
    }
}

sub verify_group
{   my ($gid, $group) = @_;
    my $def_group = 'ldap-users';
    my $group_base = "$groupdn";

    my $ldap = Net::LDAP->new($lsvr) or die "Can't bind to ldap!\n";
    $ldap->bind(
        dn       => $rootdn,
        password => $rootpw,
    );
    # No group or gid supplied so use the default ldap-users
    if (! defined($gid) && ! defined($group))
    {  my ($mesg) = $ldap->search(
            base   => $group_base,
            filter => "cn=$def_group",
            attrs  => ['gidnumber'],
        );
        return ($mesg->entry(0))->get_value('gidnumber');
    }
    # If gid supplied, verify it's a valid group:
    if (defined($gid))
    {   my ($mesg) = $ldap->search(
            base   => $group_base,
            filter => "gidnumber=$gid",
        );
        ($mesg->count == 1) ? (return $gid) : (usage("Invalid gid number: $gid"));
    }
    if (defined($group))
    {   my ($mesg) = $ldap->search(
            base   => $group_base,
            filter => "cn=$group",
            attrs  => ['gidnumber'],
        );
        ($mesg->count == 1) ?  (return ($mesg->entry(0))->get_value('gidnumber')) :
            (usage("Invalid group name: $group"));
    }
    usage("Verify group: we **really** shouldn't have gotten here...");
}

sub verify_user
{   my ($uid, $user) = @_;
    my $uid_min = 601;
    my @uids;

    my $ldap = Net::LDAP->new($lsvr) or die "Can't bind to ldap!\n";
    $ldap->bind(
        dn       => $rootdn,
        password => $rootpw,
    );
## Ensure $user doesn't already exist:
    my ($mesg) = $ldap->search(
        base   => $base,
        filter => "uid=$user",
    );
    usage("User already exists: $user") if ($mesg->count > 0);
### Ensure UID doesn't already exist:
    if (defined($uid))
    {   my ($mesg) = $ldap->search(
            base   => "$userdn",
            filter => "uidnumber=$uid",
        );
        ($mesg->count > 0) ? (usage("UID already exists: $uid")) : (return $uid);
    }
### ID next available UID:
    if (! defined($uid))
    {   my ($mesg) = $ldap->search(
            base   => "$userdn",
            filter => 'cn=*',
            attrs  => ['uidnumber'],
        );
        foreach $entry ($mesg->all_entries)
        {   push (@uids, $entry->get_value('uidnumber'));    }

        my $curr_uid = $uid_min - 1; my $next_uid = $uid_min;
        foreach my $suid (sort {$a <=> $b} @uids)
        {   # printf("Curr: %3d   Next: %3d   Suid: %3d\n", $curr_uid, $next_uid, $suid);
            last if ($suid != $next_uid);
            $curr_uid = $suid; $next_uid += 1;
        }
        return $next_uid;
    }
}

sub usage
{   my $msg = shift;

    print "\n$msg" if (defined($msg));
    print "\nFormat:\n";
    print "  ldap [ options ] [ arguments to options ]\n";
    print "     -d \$dn                                      # delete a specific dn\n";
    print "     -s \$filter                                  # search: eg 'uid=qwer'\n";
    print "     -a \${add_options}                           # add an entry\n";
    print "         -group \$group -desc \$desc               # add a group\n";
    print "                  OR\n";
    print "         -user \$user -gecos \$gecos -pwd \$pwd \\   # add a user\n";
    print "           [ -shell \$shell  -min \$min -max \$max -warn \$warn ] \n";
    print "                  OR\n";
    print "     [-l | -e] -user \${user}                     # Lock/enable user account\n";
    print "     -r -user \${user} [-f]                       # reset user pwd \n";
    print "                                                 #    w/optional force arg\n";
    print "     -m -dn \${dn} [add|replace|delete] \${p}=\${v} # Modify a dn\n";
    exit 1;
}

#####################################################################################
# Main
#####################################################################################

our $rootdn  = 'cn=admin,dc=oci,dc=com';
our $rootpw  = '3Pizda!!';
our $base    = 'dc=oci,dc=com';
our $userdn  = "ou=users,$base";
our $groupdn = "ou=groups,$base";
our $lsvr    = 'ldaps://ldapsvr.olearycomputers.com';

my ($add,   $bis,  $del,  $desc,   $dn,  $enable, $force, $gecos,  $gid);
my ($gruop, $max,  $min,  $modify, $mof, $pwd,    $reset, $search, $shell);
my ($uid,   $user,  $warn);

GetOptions (
        "add"      => \$add,
        "base=s"   => \$base,
        "bis=i"    => \$bis,
        "delete=s" => \$del,
        "desc=s"   => \$desc,
        "dn=s"     => \$dn,
        "enable"   => \$enable,
        "force"    => \$force,
        "gecos=s"  => \$gecos,
        "gid=i"    => \$gid,
        "group=s"  => \$group,
        "lock"     => \$lock,
        "min=i"    => \$min,
        "max=i"    => \$max,
        "modify"   => \$modify,
        "mof"      => \$mof,
        "pwd=s"    => \$pwd,
        "reset"    => \$reset,
        "search=s" => \$search,
        "shell=s"  => \$shell,
        "uid=i"    => \$uid,
        "user=s"   => \$user,
        "warn=i"   => \$warn,
);

usage("Add, delete, enable, lock, modify, reset, or search: Identify an action!")
    unless (defined($add) || defined($del) || defined($search) || defined($lock) ||
        defined($enable) || defined($reset) || defined($modify));

if (defined($add))
{
    switch:
    {   if (defined($user))
        {   usage("User name, gecos, and password must be specified to add a user")
                if (! defined($user) || ! defined($gecos));
            $pwd   = '1changeme' unless (defined($pwd));
            $uid   = verify_user($uid, $user);
            $gid   = verify_group($gid, $group);
            $min   = 0 unless (defined($min));
            $max   = 90 unless (defined($max));
            $shell = "/bin/bash" unless (defined($shell));
            $warn  = 7 unless (defined($warn));
            add_user($user, $uid, $gid, $gecos, $pwd, $shell, $min, $max, $warn);
            last switch;
        }
        if (defined($group))
        {   usage("Group description must be defined if adding a group.")
                if (! defined($desc));
            usage("GID must be defined if adding a group.")
                if (! defined($gid));
            $bis = 1 unless (defined($bis));
            add_group($bis, $gid, $group, $desc);
            last switch;
        }
        usage("User or group, one or the other...");
    }
}

$mof = 0 unless (defined($mof));
usage("Specify a dn to modify!") if (defined($modify) && ! defined($dn));
usage("Specify user to reenable!") if ((defined($enable)) && (!defined($user)));
usage("Specify user to reset!") if ((defined($reset)) && (!defined($user)));
usage("Specify user to lock!") if ((defined($lock)) && (!defined($user)));
# usage("Specify pwd to set!") if ((defined($reset)) && (!defined($pwd)));

modify_dn("uid=$user,$userdn", [ "add", "pwdAccountLockedTime=000001010000Z" ])
    if (defined($lock));
modify_dn($dn, \@ARGV) if (defined($modify));
reset_pwd($user, $pwd, $force) if (defined($reset));
enable_user($user) if (defined($enable));
delete_entry($del) if (defined($del));
search($mof, $search) if (length($search) > 0);

__DATA__