Thursday, December 17, 2009

Remove a user in domainA from a group in domainB

I'm writing a script in Posh (that seems to be the popular acronym for PowerShell--so I'll go with it as well) that finds all of the inactive users in the domain and moves them to an OU to be disabled one month later if the manager of the user doesn't respond to the email that the script sends.  I finished that part and and now I'm at the point that I disable user accounts where there was no response.

The moving and disabling the users is rather straight forward in Posh. The problem I encounterd was removing the group memberships of the users (a business requirement when we disable the user account).  Now removing the group memberships of groups in the same domain as the user account is also very straight forward, but what happens when you try to remove the user account from a group that is located in a sibling domain in the same forest.
Well here's what happens:

Remove-ADGroupMember : Cannot find an object with identity: 'CN=Smith\, John,OU=Users,OU=location,DC=DomainA,DC=example,DC=com' under: 'DC=DomainB,DC=example,DC=com'.
At line:1 char:21
+ Remove-ADGroupMember <<<<  -Identity $group.ObjectGUID -Members $group.member[12] -Credential $myAdminCreds.DomainB -server DomainB.example.com
    + CategoryInfo          : ObjectNotFound: (CN=Smith\, Joh...,DC=example,DC=com:ADPrincipal) [Remove-ADGroupMember], ADIdentityNotFoundException
    + FullyQualifiedErrorId : SetADGroupMember.ValidateMembersParameter,Microsoft.ActiveDirectory.Management.Commands.RemoveADGroupMember

 All in nice bright red wording.

Here is a script similar to the one that generated that error:

Import-Module ActiveDirectory

$adminCreds = @{domainA = Get-Credential "domA\admin";domainB = Get-Credential "domB\admin"}

$user = Get-ADUser johndoe -Properties memberof -Credential $adminCreds.domainA
$groups = $user.memberof | where{$_ -match "dc=domainB"}

foreach($group in $groups){
    Remove-ADGroupMember -Identity $group -Members $user.distinguishedName -Server domainB.example.com -Credential $adminCreds.domainB
}

Had I chosen groups from domainA, the script would have run without error.

So I posted my problem on every forum I could find including 'The Official Scripting Guys Forum!' as well on #powershell irc chat on freenode.net.  To date no one has responded.  So I delved into a work-a-round.

At first I tried straight up ADSI:

$group = [adsi]"LDAP://$($group.distinguishedName)"
$group.putEx(4,"member",@($user.distinguishedName))
$group.setinfo()

However, because my account is not an administrator in the sibling domain I got an access denied error.  I did a bunch of research again and low-and-behold the best I got was that there is no specific way to provide credentials to a foreign domain with [adsi] (albeit a sibling domain in the same forest).  I even mapped a drive to the domain controller in domainB using my admin credentials in that domain--nadda!

So after a ton of unfruitful research I developed my own work-around. If one of you Posh gurus wants to post an easier way I'd be delighted.  I use WinRM 2.0 to accomplish this (I read a ton of help pages):


Import-Module ActiveDirectory

$adminCreds = @{domainA = Get-Credential "domA\admin";domainB = Get-Credential "domB\admin"}

#Get the user object
$user = Get-ADUser johndoe -Properties memberof -Credential $adminCreds.domainA
#Find groups with distinguishedNames that are in domainB
$groups = $user.memberof | where{$_ -match "dc=domainB"}

#Create a remote session on domainB's domain controller
$s = New-PSSession -ComputerName domb-dc1 -Credential $adminCreds.domainB

<#Unfortunately you can't just reference variables in your local ps session while in the remote session so some setup 
work is needed.  Cycling through the collection of group DNs you first create a hash table (I love hash tables) with 
the parameters you will need to work with in the remote session.#>
foreach($group in $groups){
    $LHparams = @{userDN = $user.distinguishedName;groupDN = $group} #Local Host parameters.
    <#We need to send those parameters to the remote session.  The variable '$multivars' is an arbitrary name. 
    You can use what ever you makes sense in that spot. 'RHparams' Remote Host parameters will be set in the remote pssession.
    $multivars becomes $LHparams and can be used to set a variable on the remote host.  
    
    You can send multiple arguments and must have multiple parameters (param($a, $b))to hold those arguments.
    So this would work:#>
    
    Invoke-Command -Session $s -ScriptBlock {param($rhuserDN, $rhgroupDN) $userDN = $rhuserDN;$groupDN = $rhgroupDN}` 
                                                            -ArgumentList $user.distinguishedName,$group
    
    #However, I like hash tables better-well actually I love hash tables (best thing since sliced bread). so:
    
    Invoke-Command -Session $s -ScriptBlock {param($multivars) $RHparams = $multivars} -ArgumentList $LHparams
    
    <#Now you may be tempted to enter the session in your script--but don't.  My previous post had the script doing that but it
    doesn't work.  The commands issued in the script after entering a PSSession don't do anything.  So do this 
    (check example 5 of Help invoke-command -examples):   #>
    $command = {
        $group = [adsi]"LDAP://$($RHparams.groupDN)"
        $group.putEx(4,"member",@($RHparams.userDN))
        $group.setinfo()
    }
    <#Note that when using the hash table in the remote session the porperties ARE case sensitive.  
    GroupDN gets you nothing groupDN has the data.#>
    
    <#Of course we want as few commands as possible so let's not run the invoke-command twice
    instead let's do this:  #>
    
    $LHparams = @{userDN = $user.distinguishedName;groupDN = $group}
    $command = {
        param($multivars)
        $RHparams = $multivars
        $group = [adsi]"LDAP://$($RHparams.groupDN)"
        $group.putEx(4,"member",@($RHparams.userDN))
        $group.setinfo()
    }
    
    #Now send the commands to the remote session just once.
    Invoke-Command -Session $s -ScriptBlock $command -ArgumentList $LHparams
}

"***The script looks like this without all the extraneous comments***"

#Provided for easier reading of the code.

Import-Module ActiveDirectory

$adminCreds = @{domainA = Get-Credential "domA\admin";domainB = Get-Credential "domB\admin"} #Hash table woohoo!!!
 
$user = Get-ADUser johndoe -Properties memberof -Credential $adminCreds.domainA
$groups = $user.memberof | where{$_ -match "dc=domainB"}

$s = New-PSSession -ComputerName domB-dc1 -Credential $adminCreds.domainB

foreach($group in $groups){
    $LHparams = @{userDN = $user.distinguishedName;groupDN = $group} #another hash table woohoo woohoo!!!
    $command = {
        param($multivars)
        $RHparams = $multivars
        $group = [adsi]"LDAP://$($RHparams.groupDN)"
        $group.putEx(4,"member",@($RHparams.userDN))
        $group.setinfo()
    } 
    Invoke-Command -Session $s -ScriptBlock $command -ArgumentList $LHparams
}

3 comments:

  1. Great post - thank you. I'm running into the same problem you describe. I don't suppose you've worked on this problem again in the past two years?

    ReplyDelete
  2. hey great script!
    when I run '$user = Get-ADUser johndoe -Properties memberof -Credential $adminCreds.domainA' I only get groups of User's domain..

    ReplyDelete