Create Exact Copies of Active Directory Security Groups in a New Forest

Create Exact Copies of Active Directory Security Groups in a New Forest

  • Comments 3
  • Likes

 

Summary: Guest Blogger Oliver Lipkau shows us how to create exact copies of Active Directory security groups in a new forest.

 

Microsoft Scripting Guy Ed Wilson here. We have a guest blogger today: Oliver Lipkau. Here is Oliver’s scripting biography.

I have been scripting for more than 10 years. I started with batch at the age of 13 by modifying the school’s computer. Today, my main scripting language is Windows PowerShell. It allows me to automate the majority of my routine management of servers and clients. My specializations are ADSI and WMI. I also code in vbs, ahk, html, js, batch, php, and asp. But Windows PowerShell is my favorite. My blog is http://oliver.lipkau.net/blog.

 

Do you know the kinds of tasks that look simple but turn out to be much trickier than you thought? Recently I was confronted with such a task. I was asked to write a script that would create exact copies of Active Directory security groups in a new forest.

My first thought was that it would take me all of five minutes to finish the script. Because I had already written functions to query Active Directory for groups and users, as well as for manipulating group membership. How wrong I was! As it turns out, adding a user to a domain local security group in a trusted forest is not as easy as I had thought.

The first stepfinding all the groups I need to re-createis easy. To do this I query Active Directory like this:

$name = "Test_Group"

$root = New-Object System.DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$searcher = New-Object System.DirectoryServices.DirectorySearcher

$searcher.filter = "(&(objectcategory=group)(|(sAMAccountName=$Name)(cn=$Name)(name=$Name)))"

$searcher.FindAll() | Foreach-Object {$_.GetDirectoryEntry()}

Because this is something I might use more than once, why not make a nice function that can be used again? Also, being able to choose the search root might be a good idea because I will be working in two different domains (and forests). With this in mind, I refactored the above code into this function:

function Get-Group
{
   
param(
       
[string]$name = "*",
       
[string]$SearchRoot
   
)

   
$root = New-Object System.DirectoryServices.DirectoryEntry("LDAP://RootDSE")
   
$searcher = New-Object System.DirectoryServices.DirectorySearcher
    
   
if (!($SearchRoot))
       
{$SearchRoot=$root.defaultNamingContext}
   
$searcher.SearchRoot = "LDAP://$SearchRoot"

   
$searcher.filter = "(&(objectcategory=group)(|(sAMAccountName=$Name)(cn=$Name)(name=$Name)))"

   
$searcher.FindAll() | Foreach-Object {$_.GetDirectoryEntry()}
}
Get-Group "Test_Group"
Image of output of function
 

There we go! Now I need to create the equivalent group in the new forest. To do this I will use this function:

Function New-Group
{
   
param(
       
[Parameter(mandatory=$true)]
       
[string]$Name,
        
       
[Parameter(mandatory=$true)]
       
[string]$ParentContainer,
        
       
[ValidateSet("Universal","Global","DomainLocal")]
       
[string]$GroupScope = "Global",
        
       
[ValidateSet("Security","Distribution")]
       
[string]$GroupType = "Security",
        
       
[string]$Description 
   
) 

   
switch ($GroupScope)
   
{
        "Global"
           
{$GroupTypeAttr = 2}
        "DomainLocal"
           
{$GroupTypeAttr = 4}
        "Universal"
           
{$GroupTypeAttr = 8}
   
}

   
# modify group type attribute if the group is security enabled
   
if ($GroupType -eq 'Security')
       
{$GroupTypeAttr = $GroupTypeAttr -bor 0x80000000}

   
$Parent = [adsi]"LDAP://$ParentContainer"
   
$group = $Parent.Create("group","CN=$Name")
   
$null = $group.put("sAMAccountname",$Name)
   
$null = $group.put("grouptype",$GroupTypeAttr)
    
   
if ($Description)
       
{$null = $group.put("description",$Description)} 
    
   
$null = $group.SetInfo()   
}

Combining this function with the Get-Group, I can easily create the new group:

Get-Group "Test_Group" -SearchRoot "ou=MyTest,dc=om,dc=net" |% { New-Group -Name $_.cn -ParentContainer "ou=MyTest,dc=tww832,dc=omtest1,dc=net" -GroupScope DomainLocal -GroupType Security -Description $_.description}

To make my life easier in the next steps, I will save this new group in a variable:

$newGroup = Get-Group "Test_Group" -SearchRoot "ou=MyTest,dc=tww832,dc=omtest1,dc=net"

The next step will be to enumerate all the members in the original group. The members of a group are listed in the member property of the LDAP object. Knowing this means that I can show the list of members with this line:

(Get-Group "Test_Group" -SearchRoot "ou=MyTest,dc=om,dc=net").member


Image of showing list of members

The last thing I need to figure out is how to add the users in this list to the new group. Normally, the easiest way to add a user to a group is in this manner:

$user = [adsi]"LDAP://cn=Oliver,ou=MyTest,dc=om,dc=net"
$group = $newgroup
$Group.add($user.Path)

However, this will not work in this case because users from trusted domains are represented as ForeignSecurityPrincipals (FSPs). FSP objects only contain the user’s SID and are converted to a user object in the domain where the object resides. Executing the code above returns this error message:

Image of error message

One way to work around this problem is to add the users to the new group by their SID. To do this I will first have to get the object's SID. I do this by using another function:

function Get-ObjectSID
{
   
param(
       
[Parameter(mandatory=$true)]
       
[adsi]$object
   
)

   
$objectSid = [byte[]]$Object.objectSid.value
   
$sid = new-object System.Security.Principal.SecurityIdentifier $objectSid,0
   
$sid.value
}

When I have the SID, I can add the object to the new group. I must be careful because the syntax is slightly different when referencing the object by its SID:

$Group.add("LDAP://<SID=$SID>")

So the line in the script to add the users looks like this:

(Get-Group "Test_Group" -SearchRoot "ou=MyTest,dc=om,dc=net").member | Foreach-Object {$SID = Get-ObjectSID ([adsi]"LDAP://$_");$newGroup.add("LDAP://<SID=$SID>")}

After running the script, we can take a look at both groups and see that both have the same members.

Image of groups having same members

 

Image of groups having same members

 

Image of groups having same members

 

The only thing left to do is to pull together all of these steps and allow the script to process multiple groups at once. This is shown here:

function Get-Group
{
   
param(
       
[string]$name = "*",
       
[string]$SearchRoot
   
)

   
$root = New-Object System.DirectoryServices.DirectoryEntry("LDAP://RootDSE")
   
$searcher = New-Object System.DirectoryServices.DirectorySearcher
    
   
if (!($SearchRoot))
       
{$SearchRoot=$root.defaultNamingContext}
   
$searcher.SearchRoot = "LDAP://$SearchRoot"

   
$searcher.filter = "(&(objectcategory=group)(|(sAMAccountName=$Name)(cn=$Name)(name=$Name)))"

   
$searcher.FindAll() | Foreach-Object {$_.GetDirectoryEntry()}
}

Function New-Group
{
   
param(
       
[Parameter(mandatory=$true)]
       
[string]$Name,
        
       
[Parameter(mandatory=$true)]
       
[string]$ParentContainer,
        
       
[ValidateSet("Universal","Global","DomainLocal")]
       
[string]$GroupScope = "Global",
        
       
[ValidateSet("Security","Distribution")]
       
[string]$GroupType = "Security",
        
       
[string]$Description
   
) 

   
switch ($GroupScope)
   
{
        "Global"
           
{$GroupTypeAttr = 2}
        "DomainLocal"
           
{$GroupTypeAttr = 4}
        "Universal"
           
{$GroupTypeAttr = 8}
   
}

   
# modify group type attribute if the group is security enabled
   
if ($GroupType -eq 'Security')
       
{$GroupTypeAttr = $GroupTypeAttr -bor 0x80000000}

   
$Parent = [adsi]"LDAP://$ParentContainer"
   
$group = $Parent.Create("group","CN=$Name")
   
$null = $group.put("sAMAccountname",$Name)
   
$null = $group.put("grouptype",$GroupTypeAttr)
    
   
if ($Description)
       
{$null = $group.put("description",$Description)} 
    
   
$null = $group.SetInfo()   
}

function Get-ObjectSID
{
   
param(
       
[Parameter(mandatory=$true)]
       
[adsi]$object
   
)

   
$objectSid = [byte[]]$Object.objectSid.value
   
$sid = new-object System.Security.Principal.SecurityIdentifier $objectSid,0
   
$sid.value
}

$sourceOU = "ou=groups,dc=olddomain,dc=oldforest,dc=com"
$destinationOU = "ou=groups,dc=newdomain,dc=newforest,dc=com"

foreach ($group in Get-Group * -SearchRoot $sourceOU)
{
    "Processing: `t"
+ $group.cn 
   
New-Group -Name $group.cn -ParentContainer $destinationOU `
   
-GroupScope DomainLocal -GroupType Security -Description $group.description
        
   
$newGroup = Get-Group $group.cn -SearchRoot "ou=groups,dc=newdomain,dc=newforest,dc=com"

   
foreach ($member in $group.member)
   
{
       
$SID = Get-ObjectSID ([adsi]LDAP://$member)
       
$newGroup.add("LDAP://<SID=$SID>")
   
}
}

 That is all for today. Many thanks to Oliver for sharing his script and knowledge. I hope you enjoyed it as much as I did. Tomorrow we will start a week of Splatting with James Brundage.

We invite you to follow us on Twitter and Facebook. If you have any questions, send email to us at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

 

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • Oliver é o mágico da ciencia da computacao! Hahaha Meu deus!

  • Hey Ed.

    I found a type:

    $SID = Get-ObjectSID ([adsi]"LDAP://$_")

    should be $member, since it's not piped.

    Could you please fix it?

    Thanks :-)

  • As per Oliver's comment the code in the script has been corrected.