# Author: Steven Manross
# Creation date:  12/2/2010
# Summary: Grant the ManagedBy object of a group access to allow the "Manager can update Membership list" checkbox in Active Directory Users and Computers.
# Version: 1.01
# Last Updated:
#    12/2/2010 Initial revision
#    12/4/2010 revision to handle seperate root and forest domains
#
# Adapted and enhanced from VBScript source found here designed to add permissions necessary to manage the membership of an object:
#  http://www.experts-exchange.com/OS/Microsoft_Operating_Systems/Server/2003_Server/Q_22004020.html

use Win32::OLE qw (in);
use Win32::OLE::Variant;

use constant ADS_RIGHT_DS_WRITE_PROP => 0x20;
use constant ADS_ACEFLAG_DONT_INHERIT_ACE => 0x0;
use constant ADS_ACETYPE_ACCESS_ALLOWED_OBJECT => 0x5;
use constant ADS_FLAG_OBJECT_TYPE_PRESENT => 0x01;
use constant ADS_OBJECT_WRITE_MEMBERS => "{BF9679C0-0DE6-11D0-A285-00AA003049E2}";

foreach $groupname (@ARGV) {
  $rootdse = Win32::OLE->GetObject("LDAP://RootDSE");
  print "DNS = " . $rootdse->Get("defaultNamingContext")."\n";
  query_ldap("<LDAP://" . $rootdse->Get("defaultNamingContext") . ">;(&(objectclass=group)(samaccountname=$groupname));adspath;subtree",$groups);
  add_perms_for_managed_by($groups->Fields("adspath")->{Value});
}

sub add_perms_for_managed_by {
  my $group_path = $_[0];
  
  #stole most of this with pride from Win32::Exchange (is stealing from yourself really stealing?)

  my $group = Win32::OLE->GetObject($group_path);
  if (Win32::OLE->LastError() != 0) {
    print "error querying group object -> ". Win32::OLE->LastError(). "\n";
    exit 0;
  }

  my $mgbyobj = Win32::OLE->GetObject("LDAP://" . $group->{managedBy});
  if ($mgbyobj eq "") {
    print "ManagedBy is not populated.  Nothing to do!\n";
    return 0;
  }
  my $mgbyid = $mgbyobj->{samaccountname};
  
  $rtn = get_netbios_domain_name_for_object($mgbyobj,$mgbydomain);
  if ($rtn = 0) {
    return 0;
  } 
  my $sd = $group->{ntSecurityDescriptor};
    
  if (Win32::OLE->LastError() != 0) {
    print "error querying security descriptor -> ". Win32::OLE->LastError(). "\n";
    exit 0;
  }
  
  my $dacl = $sd->{DiscretionaryAcl};
  if (Win32::OLE->LastError() != 0) {
    print "error querying discretionary acl for group -> ".Win32::OLE->LastError()."\n";
    exit 0;
  }

  my $ace = Win32::OLE->CreateObject("AccessControlEntry");
  if (Win32::OLE->LastError() != 0) {
    print "error creating access control entry for mailbox -> " . Win32::OLE->LastError() . "\n";
    exit 0;
  }
  
  my %properties;

  $properties{'Trustee'} = $mgbydomain."\\".$mgbyid;
  $properties{'AccessMask'} = ADS_RIGHT_DS_WRITE_PROP;
  $properties{'AceFlags'} = ADS_ACEFLAG_DONT_INHERIT_ACE;
  $properties{'AceType'} = ADS_ACETYPE_ACCESS_ALLOWED_OBJECT;
  $properties{'Flags'} = ADS_FLAG_OBJECT_TYPE_PRESENT;
  $properties{'objectType'} = ADS_OBJECT_WRITE_MEMBERS;
  
  print "Group: " . $group->{DisplayName}."\n";
 
  my $ace_exists = 0;
  foreach $old_ace (in $dacl) {
    print "  old ace trustee = " .$old_ace->{Trustee}."\n";
    if (lc($old_ace->{'Trustee'}) eq lc($mgbydomain."\\".$mgbyid)) {
      if ($old_ace->{AccessMask} == $properties{'AccessMask'} &&
          $old_ace->{AceFlags} == $properties{'AceFlags'} &&
          $old_ace->{Type} == $properties{'Type'} &&
          $old_ace->{Flags} == $properties{'Flags'} &&
          $old_ace->{ObjectType} == $properties{'ObjectType'}
         ) {
        print "  ACE already exists!\n";
        $ace_exists = 1;
      }
    }
  }

  if ($ace_exists == 0) {
  
    foreach my $property (keys %properties) {
      print "  " .$property . " -- " . $properties{$property}."\n";
      $ace->{$property} = $properties{$property}; 
      if (Win32::OLE->LastError() != 0) {
        print "error working on ACE while setting $property for group -> " . Win32::OLE->LastError() . "\n";
        exit 0;
      }
    }
  
    $dacl->AddAce($ace);
    if (Win32::OLE->LastError() != 0) {
      print "error adding access control entry to perms list -> " . Win32::OLE->LastError() . "\n";
      exit 0;
    }
    
    $sd->LetProperty("DiscretionaryAcl",$dacl); 
    if (Win32::OLE->LastError() != 0) {
      print "error setting  discretionary acl on security security descriptor -> " . Win32::OLE->LastError() . "\n";
      exit 0;
    }
    $group->Put("ntSecurityDescriptor",[$sd]);
    if (Win32::OLE->LastError() != 0) {
      print "error setting security descriptor on group object -> " . Win32::OLE->LastError() . "\n";
      exit 0;
    }
    $group->SetInfo();
    if (Win32::OLE->LastError() != 0) {
      print "error setting permissions on mailbox -> " . Win32::OLE->LastError() . "\n";
      exit 0;
    }
    print "success setting Security Descriptor access for ".$properties{"Trustee"}." on ". $group->{DisplayName} ."\n";
  } else {
    print "  nothing to do!\n";
  }
  return 1;
}  

sub get_netbios_domain_name_for_object {
  my $userobj = $_[0];
  my $userid = $userobj->{samaccountname};
  my $parent_domain;
  my $root_domain_dn;
  get_parent_domain($userobj,$parent_domain);
  get_root_domain_dn($parent_domain,$root_domain_dn);
  my $query = "<LDAP://CN=Configuration," . $root_domain_dn.">;(nCName=".$parent_domain->{distinguishedname}.");NetBiosName;SubTree";
  $rtn = query_ldap($query,$netbios_names);
  if ($rtn == 0) {
    print "error running LDAP query for NetBios name for parent domain from the Root Domain's Configuration Naming Context\n";
    exit 0;
  }
  if ($netbios_names->{RecordCount} == 1) {
    $_[1] = $netbios_names->Fields("NetBiosName")->{Value};
    return 1;
  } else {
    print "error getting the netbos name of the object...  there wasn't just 1 record!\n";
    return 0;
  }
}

sub get_root_domain_dn {
  my $parent_domain = $_[0];
  my $domain_fqdn;
  convert_domain_dn_to_fqdn($parent_domain->{distinguishedname},$domain_fqdn);

  my $rootdse = Win32::OLE->GetObject("LDAP://" . $domain_fqdn . "/RootDSE");
  if (Win32::OLE->LastError() != 0) {
    print "error getting Domain qualified RootDSE -> " . Win32::OLE->LastError() . "\n";
    exit 0;
  }
  $_[1] = $rootdse->Get("rootDomainNamingContext");
  
}

sub get_parent_domain {
  my $child_object = $_[0];
  my $parent_domain;
  my $parent_object = Win32::OLE->GetObject($child_object->{Parent});
  if ($parent_object->{Class} ne "domainDNS") {
    get_parent_domain($parent_object,$parent_domain);
  } else {
    $parent_domain = $parent_object;
  }
  $_[1] = $parent_domain;
  return 1;
}

sub convert_domain_dn_to_fqdn {
  my $domain = $_[0];
  $domain =~ s/,dc\=/\./gi;
  $domain =~ s/dc\=//gi;
  $_[1] = $domain;
}
sub query_ldap {
  my $ldap_query = $_[0];

  my $error_num;
  my $error_name;
  my $RS;
  my $Conn = Win32::OLE->new("ADODB.Connection");
  if (Win32::OLE->LastError() != 0) {
    print "Failed creating ADODB.Connection object -> " . Win32::OLE->LastError() . "\n";
    return 0;
  }
  $Conn->{'Provider'} = "ADsDSOObject";
  if (Win32::OLE->LastError() != 0) {
      print "Failed setting ADODB.Command Provider information -> " . Win32::OLE->LastError() . "\n";
      return 0;
  }
  $Conn->{Open} = "Perl Active Directory Query";

  my $Cmd = Win32::OLE->new("ADODB.Command");
  if (Win32::OLE->LastError() != 0) {
    print "Failed creating ADODB.Command object -> " . Win32::OLE->LastError() . "\n";
    return 0;
  }
  $Cmd->{CommandText} = $ldap_query;
  $Cmd->{Properties}->{"Page Size"} = 99;
  $Cmd->{ActiveConnection} = $Conn;
  $RS = $Cmd->Execute();
  if (Win32::OLE->LastError() != 0) {
    if ($error_num eq "0x80040e31") {
      print "Failed Due to Command timeout -> The query took too long -> ".Win32::OLE->LastError()."\n";
    } else {
      print "Failed Executing ADODB Command object-> ".Win32::OLE->LastError()."\n";
      print "... while executing ADODB Command -> $ldap_query";
    }
    return 0;
  } else {
    $_[1] = $RS;
    return 1;
  }
}
