#!/usr/bin/env perl
#
# BEGIN COPYRIGHT BLOCK
# Copyright (C) 2001 Sun Microsystems, Inc. Used by permission.
# Copyright (C) 2016 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
# See LICENSE for details. 
# END COPYRIGHT BLOCK
#

use lib qw(/usr/lib/dirsrv/perl);
use DSUtil;
use Time::Local;
use POSIX qw(strftime);

DSUtil::libpath_add("");
DSUtil::libpath_add("/usr/lib");
$ENV{'PATH'} = ":/usr/bin";
$ENV{'SHLIB_PATH'} = "$ENV{'LD_LIBRARY_PATH'}";

my $single = 0;
my $role = 0;

###############################
# SUB-ROUTINES
###############################

sub usage
{
    print (STDERR "ns-activate.pl [-Z serverID] [-D rootdn] { -w password | -w - | -j filename } \n");
    print (STDERR "               [-p port] [-h host] [-P protocol] -I DN-to-activate\n\n");
    print (STDERR "May be used to activate a user or a domain of users\n\n");
    print (STDERR "Arguments:\n");
    print (STDERR "        -?                   - Display usage\n");
    print (STDERR "        -D rootdn            - Provide a Directory Manager DN\n");
    print (STDERR "        -w password          - Provide a password for the Directory Manager DN\n");
    print (STDERR "        -w -                 - Prompt for the Directory Manager's password\n");
    print (STDERR "        -Z serverID          - Server instance identifier\n");
    print (STDERR "        -j filename          - Read the Directory Manager's password from file\n");
    print (STDERR "        -p port              - Provide a port\n");
    print (STDERR "        -h host              - Provide a host name'\n");
    print (STDERR "        -P protocol          - STARTTLS, LDAPS, LDAPI, LDAP (default: uses most secure protocol available)\n");
    print (STDERR "        -I DN-to-activate    - Single entry DN or role DN to activate\n");
}

sub debug
{
#    print " ==> @_";
}

sub out
{
    print "@_";
}

# --------------------------
# Check if the entry is part of a locked role:
# i.e.: for each role member (nsroledn) of nsdisabledrole, check if
#     * it is the same as the entry
#     * the entry is member of role (==has nsroledn attributes), compare each of
#        them with the nsroledn of nsdisabledrole
#    * if nsroledn of nsdisabledrole are complex, go through each of them
# argv[0] is the local file handler
# argv[1] is the entry (may be a single entry DN or a role DN)
# argv[2] is the base for the search
# --------------------------

$throughRole="";

sub indirectLock
{
    # For recursivity, file handler must be local
    my $L_filehandle=$_[0];
    $L_filehandle++;

    my $L_entry=$_[1];
    # Remove useless space
    my @L_intern=split /([,])/,$L_entry;
    my $L_result="";
    foreach $L_part (@L_intern){
        $L_part=~s/^ +//;
        $L_part=~ tr/A-Z/a-z/;
        $L_result="$L_result$L_part";
    }
    $L_entry=$L_result;

    my $L_base=$_[2];
    my $L_search;
    my $L_currentrole;
    my $L_retCode;
    my $L_local;

    $info{base} = $L_base;
    $info{filter} = "(|(objectclass=*)(objectclass=ldapsubentry))";
    $info{scope} = "base";
    $info{attrs} = "nsroledn";
    $info{redirect} = ">> /dev/null 2>&1";
    DSUtil::ldapsrch_ext(%info);
    $info{redirect} = "";
    $retCode=$?;
    if ( $retCode != 0 ){
        $retCode=$?>>8;
        return 1;
    }

    # Check if the role is a nested role
    $info{filter} = "(|(objectclass=nsNestedRoleDefinition)(objectclass=ldapsubentry))";
    $info{attrs} = "";
    @L_Nested=DSUtil::ldapsrch(%info);
    # L_isNested == 1 means that we are going through a nested role, so for each member of that
    # nested role, check that the member is below the scope of the nested
    $L_isNested=@L_Nested;

    # Not Direct Lock, Go through roles if any
    $info{attrs} = "nsroledn";
    $info{filter} = "(|(objectclass=*)(objectclass=ldapsubentry))";
    $L_search=DSUtil::ldapsrch(%info);

    debug("\t-->indirectLock: check if $L_entry is part of a locked role from base $L_base\n\n");

    unless (open ($L_filehandle, "$L_search |")){
        out("Can't open file $L_filehandle\n");
        exit;
    }
    while (<$L_filehandle>) {
        s/\n //g;
        if (/^nsroledn: (.*)\n/) {
            $L_currentrole = $1;

            # Remove useless space
            my @L_intern=split /([,])/,$L_currentrole;
            my $L_result="";
            foreach $L_part (@L_intern){
                $L_part=~s/^ +//;
                $L_part=~ tr/A-Z/a-z/;
                $L_result="$L_result$L_part";
            }
            $L_currentrole=$L_result;

            debug("\t-- indirectLock loop: current nsroledn $L_currentrole of base $L_base\n");
            if ( $L_isNested == 1 ){
                if ( checkScope($L_currentrole, $L_base) == 0 ){
                    # Scope problem probably a bad conf, skip the currentrole
                    next;    
                }
            }

            if ( $L_currentrole eq $L_entry ){
                # the entry is a role that is directly locked
                # i.e, nsroledn of nsdisabledrole contains the entry
                $throughRole=$L_base;
                $throughRole=~ tr/A-Z/a-z/;

                # skipDisabled means that we've just found that the entry (which is a role)
                # is locked directly (==its DN is part of nsroledn attributes)
                # we just want to know now, if it is locked through another role
                # at least, one
                if ( $skipDisabled == 1 ){
                    # direct inactivation
                    $directLocked=1;
                    # just go through that test once
                    $skipDisabled=0;
                    next;
                }
                debug("\t-- 1 indirectLock: $L_currentrole locked throughRole == $throughRole\n");
                return 0;
            }

            $L_retCode=memberOf($L_currentrole, $L_entry);
            if ( $L_retCode == 0 && $single == 1 ){
                $throughRole=$L_currentrole;
                $throughRole=~ tr/A-Z/a-z/;
                if ( $skipManaged == 1 ){
                    if ( $L_currentrole eq $nsManagedDisabledRole){
                        # Try next nsroledn
                        $directLocked=1;
                        $skipManaged=0;
                        next;
                    }
                } 
                debug("\t-- 2 indirectLock: $L_currentrole locked throughRole == $throughRole\n");
                return 0;
            }

            # Only for the first iteration
            # the first iteration is with nsdisabledrole as base, other
            # loops are deeper
            $L_local=$skipDisabled;
            $skipDisabled=0;
    
            # the current nsroledn may be a complex role, just go through
            # its won nsroledn
            $L_retCode=indirectLock($L_filehandle,$L_entry, $L_currentrole);

            # Because of recursivity, to keep the initial value for the first level
            $skipDisabled=$L_local;

            if ( $L_retCode == 0 ){
                $throughRole=$L_currentrole;
                $throughRole=~ tr/A-Z/a-z/;
                debug("\t-- 3 indirectLock: $L_entry locked throughRole == $throughRole\n");
                return 0;
            }
        }
    }

    close($L_filehandle);

    debug("\t<--indirectLock: no more nsroledn to process\n");
    return 1;
}

# --------------------------
# Check if nsroledn is part of the entry attributes
# argv[0] is a role DN (nsroledn attribute)
# argv[1] is the entry
# --------------------------
sub memberOf
{
    my $L_nsroledn=$_[0];
    $L_nsroledn=~ tr/A-Z/a-z/;
    my $L_entry=$_[1];
    my $L_search;
    my $L_currentrole;

    $info{base} = $L_entry;
    $info{filter} = "(|(objectclass=*)(objectclass=ldapsubentry))";
    $info{scope} = "base";
    $info{attrs} = "nsrole";
    $L_search = DSUtil::ldapsrch(%info);

    debug("\t\t-->memberOf: $L_search: check if $L_entry has $L_nsroledn as nsroledn attribute\n");

    open (LDAP2, "$L_search |");
    while (<LDAP2>) {
        s/\n //g;
        if (/^nsrole: (.*)\n/) {
            $L_currentrole = $1;
            $L_currentrole=~ tr/A-Z/a-z/;
            if ( $L_currentrole eq $L_nsroledn ){
                # the parm is part of the $L_entry nsroledn
                debug("\t\t<--memberOf: $L_entry locked through $L_nsroledn\n");
                return 0;
            }
        }
    }
    close(LDAP2);

    # the parm is not part of the $L_entry nsroledn
    debug("\t\t<--memberOf: $L_entry not locked through $L_nsroledn\n");
    return 1;
}


# --------------------------
# Remove the rdn of a DN
# argv[0] is a DN
# --------------------------
sub removeRdn
{
    $L_entry=$_[0];

    @L_entryToTest=split /([,])/,$L_entry;
    debug("removeRdn: entry to split: $L_entry**@L_entryToTest\n");

    $newDN="";
    $removeRDN=1;
    foreach $part (@L_entryToTest){
        $part=~ s/^ +//;
        $part=~ tr/A-Z/a-z/;
        if ( $removeRDN <= 2 ){
            $removeRDN=$removeRDN+1;
        } else {
            $newDN="$newDN$part";
        }
    }

    debug("removeRdn: new DN **$newDN**\n");
}

# --------------------------
# Check if L_current is below the scope of 
# L_nestedRole
# argv[0] is a role
# argv[1] is the nested role
# --------------------------
sub checkScope
{
    $L_current=$_[0];
    $L_nestedRole=$_[1];

    debug("checkScope: check if $L_current is below $L_nestedRole\n");

    removeRdn($L_nestedRole);
    $L_nestedRoleSuffix=$newDN;
    debug("checkScope: nested role based:  $L_nestedRoleSuffix\n");

    $cont=1;
    while ( ($cont == 1) && ($L_current ne "") ){
        removeRdn($L_current);
        $currentDn=$newDN;
        debug("checkScope: current DN to check: $currentDn\n");
 
        if ( $currentDn eq $L_nestedRoleSuffix ){
            debug("checkScope: DN match!!!\n");
            $cont = 0;
        } else {
            $L_current=$currentDn;
        }
    }
 
    if ( $cont == 1 ){
        debug("checkScope: $_[0] and $_[1] are not compatible\n");
        return 0;
    } else {
        debug("checkScope: $_[0] and $_[1] are compatible\n");
        return 1;
    }
}

#
# Check if an account is locked by inactivity
# Take the lastlogintime (which is in Generalized Time), and convert it to its
# EPOCH time.  Then compare this to the current time and the inactivity limit
#
sub checkForInactivity
{
    my $gentime_lastlogin = shift;
    my $limit = shift;

    if ($limit == 0){
        return 0;
    }
    my ($year, $mon, $day, $hour, $min, $sec) =
        ($gentime_lastlogin =~ /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/);
    my $lastlogin = timegm($sec, $min, $hour, $day, ($mon-1), $year); # EPOCH time
    my $now = time(); # EPOCH time

    if (($now - $lastlogin) > $limit){
        # Account has be inactive for too long
        return 1;
    }
    # Account is fine and active
    return 0;
}

#
# Return various components of the acct policy
#
sub getAcctPolicy
{
    my %srch = %{$_[0]};
    my $entry = $_[1];

    my $enabled = 0;
    my $stateattr = 0;
    my $altstateattr = 0;
    my $cosspecattr = 0;
    my $limitattr = 0;
    my $limit = 0;
    my $configentry = 0;
    my $templateDN = "";

    $srch{base} = "cn=Account Policy Plugin,cn=plugins,cn=config";
    $srch{filter} = "(&(objectclass=top)(nsslapd-pluginarg0=*))";
    $srch{scope} = "base";
    $srch{attrs} = "nsslapd-pluginEnabled nsslapd-pluginarg0";

    #
    # Get the main plugin entry
    #
    $searchAccPolicy = DSUtil::ldapsrch(%srch);
    open (LDAP1, "$searchAccPolicy |");
    while (<LDAP1>) {
        s/\n //g;
        if( /^nsslapd-pluginenabled: on/i) {
            $enabled = 1;
        } elsif (/^nsslapd-pluginarg0: (.*)/i) {
            $configentry = $1;
        }
    }
    close(LDAP1);

    if ($enabled == 0){
        # Not using acct policy plugin, no reason to continue.
        return (0, 0, 0, 0);
    }

    #
    # Get the plugin config entry
    #
    $srch{base} = $configentry;
    $srch{filter} = "(objectclass=top)";
    $srch{scope} = "base";
    $srch{attrs} = "stateattrname altstateattrname specattrname limitattrname";
    $searchAccPolicy = DSUtil::ldapsrch(%srch);
    open (LDAP1, "$searchAccPolicy |");
    while (<LDAP1>) {
        s/\n //g;
        if( /^stateattrname: (.*)/i) {
            $stateattr = $1;
        } elsif (/^altstateattrname: (.*)/i) {
            $altstateattr = $1;
        } elsif (/^specattrname: (.*)/i) {
            $cosspecattr = $1;
        } elsif (/^limitattrname: (.*)/i) {
            $limitattr = $1;
        }
    }
    close(LDAP1);

    #
    # Now, get the DN for the cos template from the entry
    #
    $srch{base} = $entry;
    $srch{filter} = "(objectclass=*)";
    $srch{scope} = "base";
    $srch{attrs} = "$cosspecattr";
    $searchAccPolicy= DSUtil::ldapsrch(%srch);
    open (LDAP1, "$searchAccPolicy |");
    while (<LDAP1>) {
        s/\n //g;
        if (/^$cosspecattr: (.*)/i){
            $templateDN = $1;
        }
    }
    close(LDAP1);

    #
    # Get the inactivity limit from the template]
    #
    $srch{base} = $templateDN;
    $srch{filter} = "($limitattr=*)";
    $srch{scope} = "base";
    $srch{attrs} = "$limitattr";
    my @result = DSUtil::ldapsrch_ext(%srch);
    if ($#result > 1){
        if ($result[1] =~ /^$limitattr: *([0-9]+)/i){
            $limit = $1;
        }
    }

    return ($enabled, $stateattr, $altstateattr, $limit);
}

###############################
# MAIN ROUTINE
###############################

# Generated variable

my $state="activated";
my $already="already";
my $acct_policy_enabled;
my $stateattr;
my $altstateattr;
my $limit;

# Process the command line arguments
while( $arg = shift)
{
    if($arg eq "-?"){
        usage();
        exit(0);
    } elsif($arg eq "-D"){
        $rootdn = shift @ARGV;
    } elsif($arg eq "-w"){
        $rootpw = shift @ARGV;
    } elsif($arg eq "-j"){
        $pwfile = shift @ARGV;
    } elsif($arg eq "-p"){
        $port = shift @ARGV;
    } elsif($arg eq "-h"){
        $host = shift @ARGV;
    } elsif($arg eq "-I"){
        $entry = shift @ARGV;
    } elsif($arg eq "-Z"){
        $servid = shift @ARGV;
    } elsif ($arg eq "-P") { 
        $protocol = shift @ARGV;
    } else {
        print "ERROR - Unknown option: $ARGV[$i]\n";
        usage();
        exit(1);
    }
}

#
# Gather all our config settings
#
($servid, $confdir) = DSUtil::get_server_id($servid, "/etc/dirsrv");
%info = DSUtil::get_info($confdir, $host, $port, $rootdn);
$info{rootdnpw} = DSUtil::get_password_from_file($rootpw, $pwfile);
$info{protocol} = $protocol;
$info{args} = "-c";
if($entry eq ""){
    usage();
    exit 1;
}

#
# Gather the Account Ppoliy PLugin information(if available)
#
($acct_policy_enabled, $stateattr, $altstateattr, $limit) = getAcctPolicy(\%info, $entry);

#
# Check the actual existence of the entry to inactivate/activate
# and at the same time, validate the various parm: port, host, rootdn, rootpw
#
$info{base} = $entry;
$info{filter} = "(objectclass=*)";
$info{scope} = "base";
$info{attrs} = "dn";
@exist=DSUtil::ldapsrch_ext(%info);
$retCode1=$?;
if ( $retCode1 != 0 ){
    $retCode1=$?>>8;
    exit $retCode1;
}

$info{filter} = "(&(objectclass=LDAPsubentry)(objectclass=nsRoleDefinition))";
@isRole = DSUtil::ldapsrch_ext(%info);
$nbLineRole=@isRole;
$retCode2=$?;
if ( $retCode2 != 0 ){
    $retCode2=$?>>8;
    exit $retCode2;
}

if ( $nbLineRole > 0 ){
    debug("Groups of users\n");
    $role=1;
} else {
    debug("Single user\n");
    $single=1;
}

#
# First of all, check the existence of the nsaccountlock attribute in the entry
#
$isLocked=0;
my $lastlogintime = "";
my $altlogintime  = "";

if ( $single == 1 ){
    $info{filter} = "(objectclass=*)";
    $info{attrs} = "nsaccountlock $stateattr $altstateattr";
    $searchAccountLock= DSUtil::ldapsrch(%info);
    open (LDAP1, "$searchAccountLock |");
    while (<LDAP1>) {
        s/\n //g;
        if (/^nsaccountlock: (.*)\n/i) {
            $L_currentvalue = $1;
            $L_currentvalue=~ tr/A-Z/a-z/;
            if ( $L_currentvalue eq "true"){
                $isLocked=1;
            } elsif ( $L_currentvalue eq "false" ){
                $isLocked=0;
            }
        } elsif (/^$stateattr: (.*)\n/i) {
            $lastlogintime = $1;
        } elsif (/^$altstateattr: (.*)\n/i) {
            $altlogintime = $1;
        }
    }
    close(LDAP1);
}
debug("Is the entry already locked? ==> $isLocked\n");

#
# Get the suffix name of that entry
#

# Remove the space at the beginning (just in case...)
#    -I "uid=jvedder , ou=People , o=sun.com"
@suffix=split /([,])/,$entry;
$result="";
foreach $part (@suffix){
    $part=~s/^ +//;
    $part=~ tr/A-Z/a-z/;
    $result="$result$part";
}
@suffixN=$result;

debug("Entry to activate: #@suffix#\n");
debug("Entry to activate: #@suffixN#\n");

# Get the suffix
$cont=0;
while ($cont == 0){
    # Look if suffix is the suffix of the entry
    #    ldapsearch -s one -b "cn=mapping tree,cn=config" "cn=\"uid=jvedder,ou=People,o=sun.com\""
    #
    debug("\tSuffix from the entry: #@suffixN#\n");
    $info{base} = "cn=mapping tree, cn=config";
    $info{filter} = "cn=\"@suffixN\"";
    $info{scope} = "one";
    $info{attrs} = "cn";
    @mapping = DSUtil::ldapsrch_ext(%info);
    $retCode=$?;
    if ( $retCode != 0 ){
        $retCode=$?>>8;
        exit $retCode;
    }

    # If we get a result, remove the dn:
    #    dn: cn="o=sun.com",cn=mapping tree,cn=config
    #    cn: "o=sun.com"
    #
    shift @mapping;

    foreach $res (@mapping){
        # Break the string cn: "o=sun.com" into pieces
        @cn= split(/ /,$res);

        # And remove the cn: part
        shift @cn;

        # Now compare the suffix we extract from the mapping tree 
        # with the suffix derived from the entry
        debug("\tSuffix from mapping tree: #@cn#\n");
        if ( @cn eq @suffixN ) {
            debug("Found matching suffix\n");
            $cont=1;
        }
    }

    if ( $cont == 0 ){
        # Remove the current rdn to try another suffix
        shift @suffix;

        $result="";
        foreach $part (@suffix){
            $part=~ s/^ +//;
            $part=~ tr/A-Z/a-z/;
            $result="$result$part";
        }
        @suffixN=$result;

        debug("\t\tNothing found => go up one level in rdn #@suffix#\n");
        $len=@suffix;
        if ( $len == 0 ){
            debug("Can not find suffix. Problem\n");
            $cont=2;
        }
    }
}
if ( $cont == 2){
    out("Can not find suffix for entry $entry\n");
    exit 100;
}

$skipManaged=0;
$skipDisabled=0;
$directLocked=0;
$inactiveLocked = 0;
$nsDisabledRole="cn=nsDisabledRole,@suffixN";
$nsDisabledRole=~ tr/A-Z/a-z/;
$nsManagedDisabledRole="cn=nsManagedDisabledRole,@suffixN";
$nsManagedDisabledRole=~ tr/A-Z/a-z/;
$skipManaged=$single;
$skipDisabled=$role;

$ret = indirectLock("LDAP00",$entry, $nsDisabledRole);
if ( $ret == 0 ){
    # indirectly locked
    if ( $throughRole ne $nsDisabledRole && $throughRole ne $nsManagedDisabledRole ){
        out("$entry inactivated through $throughRole. Can not activate it individually.\n");
        exit 100;
    }
    debug("$entry locked individually\n");
} elsif ( $directLocked == 0 ){
    if ( $isLocked != 1 ){
        # The user is not "inactivated", but we need to check for account
        # inactivity before saying the account is actually active
        if($acct_policy_enabled){
            #
            # Now check the Acount Policy Plugin inactivity limits
            #
            my $logintime;
            if ($lastlogintime ne ""){
                $logintime = $lastlogintime;
            }  else {
                $logintime = $altlogintime;
            }
            if(checkForInactivity($logintime, $limit) == 0){
                # The user truly is active
                out("$entry $already $state.\n");
                exit 100;
            }
            $inactiveLocked = 1;
        }
    }
}
# else Locked directly, just unlock it!
debug("$entry locked individually\n");

if($inactiveLocked == 0 and $acct_policy_enabled){
    #
    # Now check the Acount Policy Plugin inactivity limits
    #
    my $logintime;
    if ($lastlogintime ne ""){
        $logintime = $lastlogintime;
    }  else {
        $logintime = $altlogintime;
    }
    if(checkForInactivity($logintime, $limit)){
        $inactiveLocked = 1;
    }
}

#
# Activate the entry
#
if ( $single == 1 ){
    $record = "dn: $entry\n" . "changetype: modify\n";
    if ($isLocked or $directLocked){
        # Remove the role
        $record = $record . "delete: nsRoleDN\n" . "nsRoleDN: cn=nsManagedDisabledRole,@suffixN\n-\n";
    }
    if ($inactiveLocked) {
        # Reset the lastlogintime to active the user
        my $newlogintime = strftime "%Y%m%d%H%M%SZ", gmtime;
        $record = $record . "replace: lastlogintime\n" . "lastlogintime: " . $newlogintime . "\n";
    }
} else {
    $record = "dn: cn=nsDisabledRole,@suffixN\n" . "changetype: modify\n" . "delete: nsRoleDN\n" . "nsRoleDN: $entry\n";
}

$info{args} = "-c";
$info{redirect} = "> /dev/null 2>&1";
DSUtil::ldapmod($record, %info);
if( $? != 0 ){
    debug("delete, $entry\n");
    $retCode=$?>>8;
    if ($retCode == "16") {  # Error 16 (no such attr) - already activated
        out("$entry already $state.\n");
        exit 100;
    } else {
        out("Failed to activate $entry, error $retCode\n");
        exit $retCode;
    }
}

out("$entry $state.\n");
exit 0;
