# ==============================================================================
#
# adtools.pm
#
# Account maint Active Directory functions
#
# $Id: adtools.pm,v 1.12 2008/06/18 21:13:33 dyoung2 Exp $
#
# Darren Young [darren.young@gsb.uchicago.edu]
#
# ==============================================================================
#
# ChangeLog
#
# $Log: adtools.pm,v $
# Revision 1.12  2008/06/18 21:13:33  dyoung2
#   * Ready for testing
#
# Revision 1.11  2008/06/10 19:44:54  dyoung2
#   * First WORKING version to test with.
#
# Revision 1.10  2008/06/10 18:54:27  dyoung2
#   * Attempted at using the groups to enumerate users, didn't work
#     have to use a paging control and go at the memberOf attribute
#
# Revision 1.9  2008/06/10 16:31:44  dyoung2
#   * New updates
#
# Revision 1.8  2008/06/10 15:54:05  dyoung2
#   * Started to add _getADGroupMembers
#
# Revision 1.7  2008/04/04 21:53:16  dyoung2
#   * Changes for registrar in testing.
#
# Revision 1.6  2006/08/09 21:44:58  dyoung2
#   * Added _deleteADUser function
#
# Revision 1.5  2005/10/10 23:18:35  dyoung2
#   * Added _adConnect and setADPassword().
#
# Revision 1.4  2005/09/22 17:57:43  dyoung2
#   * Added _getADDNForUser and _setADPasswordForUSer
#
# Revision 1.3  2005/08/25 22:38:26  dyoung2
#   * Standardized BEGIN headers.
#
# Revision 1.2  2005/06/20 20:57:51  dyoung2
#   * Added _getADGroupsForUser().
#
# Revision 1.1  2005/04/11 23:08:04  dyoung2
#   * Initial version of module.
#   * Added isValidADAccount().
#
# ==============================================================================

my $cvsid = '$Id: adtools.pm,v 1.12 2008/06/18 21:13:33 dyoung2 Exp $';
my @cvsinfo = split( ' ', $cvsid );
our $NAME    = File::Basename::basename($0);
our $VERSION = $cvsinfo[2];


# ------------------------------------------------------------------------------
#                             B E G I N
# ------------------------------------------------------------------------------
BEGIN {

    # Pragmas
    use strict;

    # "Standard" modules we use
    use FindBin;
    use Config::Simple;
    use lib "$FindBin::Bin/../lib";

    # Standard account maint modules to use
    use logging;
    use errcodes;
    use utils;
    
    # Modules that this module uses
    use Net::LDAP;
    use Net::LDAPS;
    use Net::LDAP::Control::Paged;
    use Net::LDAP::Constant qw( LDAP_CONTROL_PAGED );

}


# ------------------------------------------------------------------------------ 
#                             V A R I A B L E S
# ------------------------------------------------------------------------------ 
our $LOGFILE    = "$FindBin::Bin/../log/acctmgr.log" unless($LOGFILE); 
our $CONFIGFILE = "$FindBin::Bin/../etc/acctmgr.cfg" unless($CONFIGFILE); 


# ------------------------------------------------------------------------------
#                             C O N F I G   F I L E
# ------------------------------------------------------------------------------
our $CFG = new Config::Simple( filename => $CONFIGFILE ) or die "$!\n" unless($CFG);


# ==============================================================================
# NAME        : _adConnect
# DESCRIPTION : Establish and bind an AD LDAP session
# ARGUMENTS   : None
# RETURN      : string(reference(ldap))
# NOTES       : None
# ==============================================================================
sub _adConnect {
    my $name = "_adConnect";
    my ( $package, $filename, $line ) = caller; 
    debug("$name: entering with args @_");
    debug("$name: called from package->$package, filename->$filename, line->$line");
    
    my @adhosts = $CFG->param("ad.hosts"); ### list of hosts to use for searches
    my $timeout = $CFG->param("ad.timeout");
    my $ldapver = $CFG->param("ad.version");
    my $ldap;


    # create anonymous hashes for each host we're configured to look at
    # and populate their settings from the config file
    foreach $host (@adhosts) {
        
        $$host{"hostname"} = $CFG->param("$host.hostname");
        $$host{"type"}     = $CFG->param("$host.type");
        $$host{"binddn"}   = $CFG->param("$host.binddn");
        $$host{"bindpw"}   = $CFG->param("$host.bindpw");

    }


    # flip through all the AD servers and connect/bind
    foreach $host (@adhosts) {

        # Attempt to establish a connection to the designated host
        # if we fail, move to the next one
        debug("$name: Attempting to create an $$host{'type'} connection to $host");
        
        # if it's a plain old LDAP server
        if ( $$host{'type'} eq "LDAP" ) {
            if ( $ldap = Net::LDAP->new( $$host{'hostname'}, timeout=>$timeout, version=>$ldapver )) {
                debug("$name: Established $$host{'type'} connection to $$host{'hostname'}");
            } else {
                logmsg("$name: FAILED to establish $$host{'type'} connection to $$host{'hostname'}");
                next;
            }
        
        # if it has SSL enabled
        } elsif ( $$host{'type'} eq "LDAPS" ) {
            if ( $ldap = Net::LDAPS->new( $$host{'hostname'}, timeout=>$timeout, version=>$ldapver )) {
                debug("$name: Established $$host{'type'} connection to $$host{'hostname'}");
            } else {
                logmsg("$name: FAILED to establish $$host{'type'} connection to $$host{'hostname'}");
                next;
            }

        # and there are other ldap types ?
        } else {
            logmsg("$name: Invalid comm type: $$host{'type'} for $host");
            next;
        }


        # if we got a connection, attempt a bind to that sserver
        if ( $ldap->bind( $$host{'binddn'}, password => $$host{'bindpw'} ) ) {
            debug("$name: Successful bind to $host");
            return($ldap);
        } else {
            logmsg("$name: FAILED to bind to LDAP server $host");
            logmsg("$name: LDAP error code is " . $ldap->code);
            logmsg("$name: LDAP error text is " . $ldap->error);
            next;
        }

    } ### end foreach $host loop
    
    # if we made it here, all the connect() and bind() calls failed
    debug("$name: FAILED to contact ANY Active Directory servers");
    return(ERR_CRITICAL);

}


# ==============================================================================
# NAME        : _adClose
# DESCRIPTION : Unbind and disconnect and AD LDAP session
# ARGUMENTS   : string(reference(ldap))
# RETURN      : TRUE or FALSE
# NOTES       : None
# ==============================================================================
sub _adClose {
    my ($ldap) = @_;
    my $name = "_adClose()";
    my ( $package, $filename, $line ) = caller;
    debug("$name: entering with args @_");
    debug("$name: called from package->$package, filename->$filename, line->$line");
    
    # Make sure they gave us a valid Net::LDAP or Net::LDAPS object
    if ( $ldap->isa("Net::LDAP") || $ldap->isa("Net::LDAPS")) {
    
        # unbind from the server
        if ( $ldap->unbind ) {
            debug("$name: unbound from ldap server");
        } else {
            logmsg("$name: FAILED to unbind from ldap server");
            return(0);
        }
    
        # Disconnect from the server
        if ( $ldap->disconnect ) {
            debug("$name: disconnected from ldap server");
        } else {
            logmsg("$name: FAILED to disconnect from ldap server");
            return(0);
        }
    } else {
        logmsg("$name: object passed ($ldap) isn't an ldap object");
        return(0);
    }

    debug("$name: returning with val: $retval");
    return(1);
}


# ==============================================================================
# NAME        : isValidADAccount
# DESCRIPTION : checks to see if an account is valid in AD
# ARGUMENTS   : string(login)
# RETURN      : TRUE or FALSE 
# NOTES       : sends back ERR_CRITICAL on no AD servers available
# ==============================================================================
sub isValidADAccount {
    my ($login) = @_;
    my $name = "isValidADAccount";
    my ( $package, $filename, $line ) = caller; 
    debug("$name: entering with args @_");
    debug("$name: called from package->$package, filename->$filename, line->$line");

    my $filter;
    my $ad;
    my $basedn  = $CFG->param("ad.basedn");
    
    if ( $ad = _adConnect() ) {
        
        $filter = '(sAMAccountName=' . $login . ')';
        debug("$name: search based: " . $basedn );
        debug("$name: search filter: $filter");
        $mesg = $ad->search ( base   => $basedn, scope  => 'sub', filter => $filter );

        if ( $mesg->code ) {
            debug("$name: failed search on $ad->{net_ldap_uri}");
            debug("$name: LDAP error code: " . $mesg->code);
            debug("$name: LDAP error text: " . $mesg->error);
            return(0);
        } else {
            debug("$name: found " . $mesg->count . " entries");
            if ( $mesg->count == 0 ) {
                debug("$name: login not found in AD");
                if ( $ad ) {
                    _adClose($ad);
                    return(0);
                }
            } elsif ( $mesg->count == 1 ) {
                debug("$name: found login in AD");
                if ( $ad ) {
                    _adClose($ad);
                    return(1);
                }
            } else {
                debug("$name: too many entries returned");
                return(0);
            }
        }
    } else {
        logmsg("$name: LDAP search FAILED");
        return(0);
    }
}


# ==============================================================================
# NAME        : _getADGroupsForUser
# DESCRIPTION : Retrieve all AD groups a user is in
# ARGUMENTS   : string(login)
# RETURN      : array(groups)
# NOTES       : Need to rework this to use adConnect(), etc.
# ==============================================================================
sub _getADGroupsForUser {
    my ($login) = @_;
    my $name = "_getADGroupsForUser";
    my ( $package, $filename, $line ) = caller; 
    debug("$name: entering with args @_");
    debug("$name: called from package->$package, filename->$filename, line->$line");      

    my $filter;
    my $found = 0;
    my $retval = 0;
    my @memberGroups;

    my @adhosts = $CFG->param("ad.hosts"); ### list of hosts to use for searches
    my $timeout = $CFG->param("ad.timeout");
    my $ldapver = $CFG->param("ad.version");

    foreach $host (@adhosts) {

        # create anonymous hashes for each host we're configured to look at
        # and populate their settings from the config file
        $$host{"hostname"} = $CFG->param("$host.hostname");
        $$host{"port"}     = $CFG->param("$host.port");
        $$host{"binddn"}   = $CFG->param("$host.binddn");
        $$host{"bindpw"}   = $CFG->param("$host.bindpw");
        $$host{"basedn"}   = $CFG->param("$host.basedn");
    }

    foreach $host (@adhosts) {

        # attempt a connect then bind to the current host in this loop
        debug("$name: attepting to create connection to $$host{'hostname'}");
        $ldap = Net::LDAP->new($$host{'hostname'}, timeout=>$timeout, version=>$ldapver);
        $mesg = $ldap->bind($$host{'binddn'}, password=>$$host{'bindpw'});

        # if we fail the bind, move on to the next server in the array
        if ( $mesg->code ) {
            debug("$name: failed bind to $$host{'hostname'}");
            debug("$name: LDAP error is: " . $mesg->error);
            next; ### move on to next host in the foreach loop
        } else {
            debug("$name: successful bind to $$host{'hostname'}");

            $filter = '(sAMAccountName=' . $login . ')';
            debug("$name: search based: " . $$host{'basedn'});
            debug("$name: search filter: $filter");
            $mesg = $ldap->search ( base   => $$host{'basedn'},
                                    scope  => 'sub',
                                    filter => $filter
                                  );
            if ( $mesg->code ) {
                debug("$name: failed search on $$host{'hostname'}");
                debug("$name: LDAP error is: " . $mesg->error);
                next; ### move on to next host in the foreach loop
            } else {
                debug("$name: found " . $mesg->count . " entries");
                if ( $mesg->count == 0 ) {
                    debug("$name: login not found in AD");
                    if ( $ldap ) {
                        $ldap->unbind;
                        return(0);
                    }
                } elsif ( $mesg->count == 1 ) {
                    debug("$name: found login in AD");
                    my $entry = $mesg->entry(0);
                    my @ldapGroups = $entry->get_value("memberOf"); 
                    foreach my $group (@ldapGroups) {
                        debug("$name: isMemberOf -> $group");
                        push(@memberGroups, $group);
                    }

                    if ( $ldap ) {
                        $ldap->unbind;
                        return(@memberGroups);
                    }
                } else {
                    debug("$name: too many entries returned");
                }
            }

        }  ### end mesg->code check

        if ( $ldap ) {
            $ldap->unbind();
        }
        
    } ### end foreach $host loop

    # if we're still here, something is REALLY wrong with AD
    if ( ! $found ) {
        debug("$name: FAILED to contact ANY Active Directory servers");
        return(0);
    } 

}


# ==============================================================================
# NAME        : _getADDNForUser
# DESCRIPTION : Get the DN for an AD user
# ARGUMENTS   : string(login)
# RETURN      : string(dn)
# NOTES       : None
# ==============================================================================
sub _getADDNForUser {
    my ($login) = @_;
    my $name = "_getADDNForUser";
    my ( $package, $filename, $line ) = caller; 
    debug("$name: entering with args @_");
    debug("$name: called from package->$package, filename->$filename, line->$line");      

    my $filter;
    my $ad;
    my $basedn  = $CFG->param("ad.basedn");
    
    if ( $ad = _adConnect() ) {
        
        $filter = '(sAMAccountName=' . $login . ')';
        debug("$name: search based: " . $basedn );
        debug("$name: search filter: $filter");
        $mesg = $ad->search ( base   => $basedn, scope  => 'sub', filter => $filter );

        if ( $mesg->code ) {
        
        debug("$name: failed search on $ad->{net_ldap_uri}");
            debug("$name: LDAP error code: " . $mesg->code);
            debug("$name: LDAP error text: " . $mesg->error);
            return(0);
        
        } else {
            debug("$name: found " . $mesg->count . " entries");
        
        if ( $mesg->count == 0 ) {
                debug("$name: login not found in AD");
                
                if ( $ad ) {
                    _adClose($ad);
                    return(0);
                }
            
            } elsif ( $mesg->count == 1 ) {
                debug("$name: found login in AD");
                
                # found it, get the dn for the entry
                my $dn = $mesg->entry(0)->dn();                
                
                
                # if we had a good connection, close and return the dn.
                if ( $ad ) {
                    _adClose($ad);
                    return(qq($dn));
                }
            } else {
                debug("$name: too many entries returned");
                return(0);
            }
        }
    } else {
        logmsg("$name: FAILED to create AD/LDAP connection");
        return(0);
    }

}


# ==============================================================================
# NAME        : _setADPasswordForUser
# DESCRIPTION : Set the password for a given AD account
# ARGUMENTS   : string(dn), string(password)
# RETURN      : TRUE or FALSE
# NOTES       : None
# ==============================================================================
sub _setADPasswordForUser {
    my ($dn, $pass) = @_;
    my $name = "_setADPasswordForUser";
    my ( $package, $filename, $line ) = caller; 
    debug("$name: entering with args @_");
    debug("$name: called from package->$package, filename->$filename, line->$line");      

    my $retval = 0;
    my $npass;

    my $ad = _adConnect();

    # add quotes and convert to uniCode
    map { $npass .= "$_\000" } split(//, "\"$pass\"");
    debug("$name: unicodePwd => $npass");
            
    my $rtn = $ad->modify($dn, replace => { "unicodePwd" => $npass });
    
    if ( $rtn->code ) {
        logmsg("$name: FAILED to change password for $login");
        logmsg("$name: LDAP error code is: " . $rtn->code);
        logmsg("$name: LDAP error text is: " . $rtn->error);
        $retval = 0;
    } else {
        $retval = 1;
    }

    if ( _adClose($ad) ) {
        return($retval);
    } else {
        return($retval);
    }

}


# ==============================================================================
# NAME        : 
# DESCRIPTION : 
# ARGUMENTS   : 
# RETURN      : 
# NOTES       : 
# ==============================================================================
sub _deleteADUser {
    my ($login) = @_;
    my $name = "_deleteADUser";
    my ( $package, $filename, $line ) = caller; 
    debug("$name: entering with args @_");
    debug("$name: called from package->$package, filename->$filename, line->$line");      

    my $filter;
    my $ad;
    my $basedn  = $CFG->param("ad.basedn");
    my $retval = 1; # default to true (good)
    
    if ( $ad = _adConnect() ) {
        
        $filter = '(sAMAccountName=' . $login . ')';
        debug("$name: search based: " . $basedn );
        debug("$name: search filter: $filter");
        $mesg = $ad->search ( base   => $basedn, scope  => 'sub', filter => $filter );

        if ( $mesg->code ) {
        
        debug("$name: failed search on $ad->{net_ldap_uri}");
            debug("$name: LDAP error code: " . $mesg->code);
            debug("$name: LDAP error text: " . $mesg->error);
            return(0);
        
        } else {
            debug("$name: found " . $mesg->count . " entries");
        
        if ( $mesg->count == 0 ) {
                debug("$name: login not found in AD");
                
                if ( $ad ) {
                    _adClose($ad);
                    return(0);
                }
            
            } elsif ( $mesg->count == 1 ) {
                debug("$name: found login in AD");
                
                # found it, get the dn for the entry
                my $dn = $mesg->entry(0)->dn();                
                debug("$name: dn => $dn");

                my $r = $ad->delete($dn);

                if ( $r->code ) {
                    logmsg("$name: failed to delete account, code => " . $r->code);
                } else {
                    logmsg("$name: deleted AD account for $login");
                }
                
                # if we had a good connection, close and return the dn.
                if ( $ad ) {
                    _adClose($ad);
                    return(1);
                }
            } else {
                debug("$name: too many entries returned");
                return(0);
            }
        }
    } else {
        logmsg("$name: FAILED to create AD/LDAP connection");
        return(0);
    }

    # fall through to here?
    return($retval);

}


# ==============================================================================
# NAME        : _getADGroupMembers
# DESCRIPTION : Return members of an AD group
# ARGUMENTS   : string(group)
# RETURN      : array(members) or FALSE
# NOTES       : Uses an LDAP paged result set
#             : The group name is the DN of the group
# ==============================================================================
sub _getADGroupMembers {
    my ($group) = @_;
    my $name = "_getADGroupMembers";
    my ( $package, $filename, $line ) = caller; 
    debug("$name: entering with args @_");
    debug("$name: called from package->$package, filename->$filename, line->$line");

    my $filter = "(memberOf=$group)";
    my $ad;
    my $basedn  = $CFG->param("ad.basedn");
    my @members;
    my $page;
    my @args;
    my $cookie;
    my $pagesize = 100;

    $| = 1;

    debug("$name: filter => $filter");

    if ( $ad = _adConnect() ) {

        debug("$name: search based: " . $basedn );
        debug("$name: search filter: $filter");
        debug("$name: page size: $pagesize");

        $page = Net::LDAP::Control::Paged->new( size => $pagesize );

        @args = ( base     => $basedn,
                  scope    => "subtree",
                  filter   => $filter,
                  control  => [ $page ],
                );

        while(1) {
            # Perform search
            debug("Searching next $pagesize...");
            my $mesg = $ad->search( @args );

            # gather the members and shove them in the array
            while (my $entry = $mesg->shift_entry()) {
 
                my $name = $entry->get_value('samAccountName');
                push (@members, $name);

            } # while


            my ($resp) = $mesg->control( LDAP_CONTROL_PAGED ) or last;
            $cookie    = $resp->cookie or last;
            $page->cookie($cookie);

        } # while (1)

        # Close the search if we're out of results
        if ($cookie) {
            $page->cookie($cookie);
            $page->size(0);
            $ad->search( @args );
        }

        # if the connection is still good, close it out
        if ( $ad ) {
            _adClose($ad);
        }

        # Return a sorted array
        debug("$name: found " . scalar(@members) . " group members");
        @members = sort(@members);
        return(@members);

    } else {
        logmsg("$name: FAILED to create AD/LDAP connection");
        return(0);
    }
}

1;
