Shadow Groups are dynamic groups that include all users in a specified Organizational Unit, but no other users. In this way they shadow the OU. Because Fine Grained Password Policies can only be applied to users and groups, this is the best way to apply a FGPP to all users in an Organizational Unit. This script has been modified to allow you to shadow more than one OU.

There is no automatic mechanism in Active Directory to keep the shadow group membership up to date. A script must be run periodically to update the membership. The script must remove any members of the group that are no longer in the OU (or OUs), and add any users in the OU (or OUs) that are not in the group. This PowerShell Version 2 script is designed to accomplish this task.

A few other scripts have been found for the same purpose, but they have some shortcomings. This script has the following features.

The script exits with an error message under the following conditions.

To run the script in your environment for your purpose, you must assign values to the following variables in the configuration section of the script.

 

PowerShell
Edit|Remove
# ShadowGroup.ps1 
# PowerShell Version 2 script to ensure all users in one or more specified 
# Organizational Units are also members of a corresponding shadow group. 
# Also makes sure users not in the OUs are not members of the group. 
# This script can be used to maintain a shadow group of users in the OU or OUs. 
# A Fine Grained Password Policy can be applied to the shadow group and it will 
# apply to all users in the specified OUs. 
 
# Author: Richard L. Mueller 
# Version 1.0 - October 12, 2015 
# Version 2.0 - February 4, 2017 - Allow more than one OU to be specified. 
# Version 3.0 - June 19, 2017 - Fixed bug when only one user added or removed. 
 
Write-Host "Please Standby..." 
 
###### Start of Configuration Section ###### 
# The values of the variables in this section should be customized for 
# your specific situation. 
 
# Specify the DNS name of a DC. All additions to and removals from the shadow 
# group should be done on the same Domain Controller to avoid replication problems. 
# This must be a DC that supports the Active Directory module cmdlets. 
$Server = "dc0321.mydomain.com" 
 
# Specify log file. 
$LogFile = "c:\PowerShell\Shadow\ShadowGroup.log" 
 
# If $Update is $False, the script only logs what it would do, without actually 
# updating the shadow group. If $Update is $True, the script will update the group. 
$Update = $False 
 
# If $EnabledOnly is $True only enabled users are to be in the group. 
# If $False, all users in the OU (or OUs) will be in the group. 
$EnabledOnly = $True 
 
# Specify the array of one or more OU distinguished names. 
$OUDNs = @("ou=Sales,ou=West,dc=MyDomain,dc=com") 
 
# If $ChildOUs is $True users in child OUs of $OUDNs are included. 
# If $False users in child OUs of $OUDNs are not included. 
$ChildOUs = $False 
 
# Specify the distinguished name of the corresponding shadow group. 
# The group can be empty, but it must exist. 
$GroupDN = "cn=SalesShadow,ou=Sales,ou=West,dc=MyDomain,dc=com" 
 
###### End of Configuration Section ###### 
 
# Script version and date. 
$Version = "Version 3.0 - June 19, 2017" 
 
Try {Import-Module ActiveDirectory -ErrorAction Stop -WarningAction Stop} 
Catch 
{ 
    Write-Host "ActiveDirectory module (or DC with ADWS) not found!!" ` 
        -ForegroundColor Red -BackgroundColor Black 
    Write-Host "Script Aborted." -ForegroundColor Red -BackgroundColor Black 
    # Abort the script. 
    Break 
} 
 
# Assign search scope. 
If ($ChildOUs -eq $False) {$Scope = "OneLevel"Else {$Scope = "SubTree"} 
 
# Check if DNs of OUs are valid. 
$Abort = $False 
$Count = $OUDNs.Count 
For ($k = 0; $k -lt $Count$k = $k + 1) 
{ 
    $OUDN = $OUDNs[$k] 
    $X = [ADSI]"LDAP://$OUDN" 
    If ($X.Name) 
    { 
        # DN of the OU is valid. 
        # Ensure that distinguised name is properly formated. 
        # This will correct situations where the DN provided included 
        # spaces after any commas, such as "OU=West Sales, dc=mydomain, dc=com". 
        $Fix = $($X.distinguishedName) 
        If ($Fix -ne $OUDN) 
        { 
            # Correct the distinguised name in the array. 
            $OUDNs[$k] = $Fix 
            $OUDN = $Fix 
        } 
 
        # Make sure OU is not a container or domain. 
        If ($OUDN.Substring(0, 3) -ne "OU=") 
        { 
            Write-Host "Error: OUDN $OUDN must be an OU, script aborted." ` 
                -foregroundcolor red -backgroundcolor black 
            # Flag to break out of the script, but consider all OUs in the array. 
            $Abort = $True 
        } 
    } 
    Else 
    { 
        Write-Host "Error: OUDN $OUDN invalid, script aborted." ` 
            -foregroundcolor red -backgroundcolor black 
        # Flag to break out of the script, but consider all OUs in the array. 
        $Abort = $True 
    } 
} # End of the For loop. 
If ($Abort) {Break} 
 
# Check if DN of shadow group valid. 
$Y = [ADSI]"LDAP://$GroupDN" 
If ($Y.Name) 
{ 
    If ($Y.objectCategory -NotLike "CN=Group,*") 
    { 
        Write-Host "Error: GroupDN $GroupDN not a group, script aborted." ` 
            -foregroundcolor red -backgroundcolor black 
        Break 
    } 
} 
Else 
{ 
    Write-Host "Error: GroupDN $GroupDN invalid, script aborted." ` 
        -foregroundcolor red -backgroundcolor black 
    Break 
} 
 
# Ensure that distinguished name is properly formated. 
# This will correct situations where the DN provided included 
# spaces after any commas, such as "CN=My Group, OU=East, dc=mydomain, dc=com". 
$GroupDN = $($Y.distinguishedName) 
 
# Check if the designated computer can be contacted. 
$Ping = Test-Connection -ComputerName $Server -Count 1 -Quiet 
If ($Ping -eq $False) 
{ 
    Write-Host "Error: Unable to connect to $Server, script aborted." ` 
        -foregroundcolor red -backgroundcolor black 
    Break 
} 
 
# Check if the computer is a DC that supports the AD modules. 
# Retrieve all direct user members of the shadow group. 
Try 
{ 
    $Members = Get-ADUser -LDAPFilter "(memberOf=$GroupDN)" ` 
        -Server $Server | Select distinguishedName, Enabled 
} 
Catch 
{ 
    Write-Host "Error: $Server does not support the AD modules, script aborted." ` 
        -foregroundcolor red -backgroundcolor black 
    Break 
} 
 
# The text written to the log depends on $Update. 
If ($Update -eq $True) 
{ 
    $AddText = "added" 
    $RemText = "removed" 
} 
Else 
{ 
    $AddText = "would be added" 
    $RemText = "would be removed" 
} 
 
# Add information to the log file. 
Try 
{ 
    Add-Content -Path $LogFile ` 
        -Value "------------------------------------------------" -ErrorAction Stop 
} 
Catch 
{ 
    Write-Host "Error: Logfile $LogFile invalid or protected, script aborted." ` 
        -foregroundcolor red -backgroundcolor black 
    Break 
} 
Add-Content -Path $LogFile -Value "ShadowGroup.ps1 ($Version)" 
Add-Content -Path $LogFile -Value $("Started: " + (Get-Date).ToString()) 
Add-Content -Path $LogFile -Value "Log file: $LogFile" 
Add-Content -Path $LogFile -Value "DNs of the OUs:" 
ForEach ($OU In $OUDNs) 
{ 
    Add-Content -Path $LogFile -Value "    $OU" 
} 
Add-Content -Path $LogFile -Value "DN of the shadow group: $GroupDN" 
Add-Content -Path $LogFile -Value "DC used for updates: $Server" 
Add-Content -Path $LogFile -Value "Only enabled users: $EnabledOnly" 
Add-Content -Path $LogFile -Value "Include users in child OUs: $ChildOUs" 
Add-Content -Path $LogFile -Value "Update the shadow group: $Update" 
Add-Content -Path $LogFile -Value "------------------------------------------------" 
 
# Initialize counters. 
$Removed = 0 
$Added = 0 
 
# Flags if too many users removed from or added to the shadow group. 
# A maximum of 4000 users should be removed or added at a time 
# to avoid excessive network traffic and long running transactions. 
$TooManyRemoved = $False 
$TooManyAdded = $False 
 
# Array of users to be added to the shadow group. 
$UsersToAdd = @() 
 
# Array of users to be removed from the shadow group. 
$UsersToRemove = @() 
 
# Enumerate all direct user members of the shadow group. 
# $Members was retrieved above to test if the DC supports AD modules. 
If ($Members) 
{ 
    ForEach ($Member In $Members) 
    { 
        $DN = $Member.distinguishedName 
        If (($EnabledOnly -eq $True-and ($Member.Enabled -eq $False)) 
        { 
            # Add this disabled member to the array of users 
            # to be removed from the shadow group. 
            $UsersToRemove = $UsersToRemove + $DN 
            $Removed = $Removed + 1 
            Add-Content -Path $LogFile ` 
                -Value "$RemText from group (disabled): $DN" 
        } 
        Else 
        { 
            # Parse the member for the DN of their Parent OU. 
            $Parts = $($DN -split ",OU=") 
            $k = 0 
            ForEach ($Part In $Parts) 
            { 
                Switch ($k) 
                { 
                    0 {$Parent = $Null} 
                    1 {$Parent = "OU=$Part"} 
                    Default {$Parent = "$Parent,OU=$Part"} 
                } 
                $k = $k + 1 
            } 
            # Consider users in an OU. 
            If ($Parent) 
            { 
                If ($ChildOUs -eq $False) 
                { 
                    # Check if the member object is not in any of the OUs. 
                    If ($OUDNs -NotContains $Parent) 
                    { 
                        # Add this member to the array of users 
                        # to be removed from the shadow group. 
                        $UsersToRemove = $UsersToRemove + $DN 
                        $Removed = $Removed + 1 
                        Add-Content -Path $LogFile ` 
                            -Value "$RemText from group (not in OU): $DN" 
                    } 
                } 
                Else 
                { 
                    # Check if the member object is not in any of the OUs or child OUs. 
                    $OK = $False 
                    ForEach ($OUDN In $OUDNs) 
                    { 
                        If ($Parent -Like $("*" + $OUDN)) 
                        { 
                            $OK = $True 
                            # Break out of the ForEach loop. 
                            Break 
                        } 
                    } 
                    If ($OK -eq $False) 
                    { 
                        # Add this member to the array of users 
                        # to be removed from the shadow group. 
                        $UsersToRemove = $UsersToRemove + $DN 
                        $Removed = $Removed + 1 
                        Add-Content -Path $LogFile -Value ` 
                            "$RemText from group (not in OU or child OU): $DN" 
                    } 
                } 
            } 
            Else 
            { 
                # Remove any users from the group that are not in any OU. 
                $UsersToRemove = $UsersToRemove + $DN 
                $Removed = $Removed + 1 
                Add-Content -Path $LogFile ` 
                    -Value "$RemText from group (not in any OU): $DN" 
            } 
        } 
        If ($Removed -gt 3999) 
        { 
            $TooManyRemoved = $True 
            # Break out of the ForEach loop, but not out of the script. 
            # No need to consider other members of the shadow group. 
            Break 
        } 
    } 
} 
 
# Remove the users from the shadow group. 
If (($Update -eq $True-and ($Removed -gt 0)) 
{ 
    Remove-ADGroupMember -Identity $GroupDN -Members $UsersToRemove ` 
        -Server $Server -Confirm:$False 
    # Short pause. 
    Start-Sleep -Seconds 10 
} 
 
# Retrieve all users in the specified OUs that are not members of the shadow group. 
# If $ChildOUs is $True the $Scope is "SubTree" and users in child OUs of $OUDNs 
# are included. 
$Filter = "(!(memberOf=$GroupDN))" 
If ($EnabledOnly -eq $True) 
{ 
    # Only retrieve enabled users. 
    $Filter = "(&" + $Filter + "(!(userAccountControl:1.2.840.113556.1.4.803:=2)))" 
} 
 
$Abort = $False 
# Consider each OU in the array. 
ForEach ($OUDN In $OUDNs) 
{ 
    $UsersInOU = Get-ADUser -SearchBase $OUDN -SearchScope $Scope ` 
        -LDAPFilter $Filter -Server $Server 
 
    # Enumerate the users. These users should be added to the shadow group. 
    If ($UsersInOU) 
    { 
        ForEach ($User in $UsersInOU) 
        { 
            $UserDN = $User.distinguishedName 
            # Add this user to the array of users to be added to the shadow group. 
            $UsersToAdd = $UsersToAdd + $UserDN 
            $Added = $Added + 1 
            Add-Content -Path $LogFile -Value "$AddText to group: $UserDN" 
            If ($Added -gt 3999) 
            { 
                $TooManyAdded = $True 
                # Break out of the inner ForEach loop 
                # and flag to break out of the outer ForEach. 
                $Abort = $True 
                Break 
            } 
        } 
    } 
    if ($Abort) 
    { 
        # Break out of the outer ForEach loop, but not out of the script. 
        Break 
    } 
} 
 
# Add the missing users to the shadow group. 
If (($Update -eq $True-and ($Added -gt 0)) 
{ 
    Add-ADGroupMember -Identity $GroupDN -Members $UsersToAdd -Server $Server 
} 
 
# Update the log file. 
Add-Content -Path $LogFile -Value "------------------------------------------------" 
Add-Content -Path $LogFile -Value $("Finished: " + (Get-Date).ToString()) 
Add-Content -Path $LogFile ` 
    -Value "Number of users $RemText from the group: $('{0:n0}' -f $Removed)" 
Add-Content -Path $LogFile ` 
    -Value "Number of users $AddText to the group: $('{0:n0}' -f $Added)" 
 
If ($TooManyRemoved -eq $True) 
{ 
    Add-Content -Path $LogFile -Value "Caution: 4000 users $RemText from the group." 
    Add-Content -Path $LogFile -Value "This the maximum allowed." 
    Add-Content -Path $LogFile -Value "Run the script again to process more." 
} 
If ($TooManyAdded -eq $True) 
{ 
    Add-Content -Path $LogFile -Value "Caution: 4000 users $AddText to the group." 
    Add-Content -Path $LogFile -Value "This the maximum allowed." 
    Add-Content -Path $LogFile -Value "Run the script again to process more." 
} 
 
Write-Host "Done. See log file: $LogFile" 
If ($TooManyRemoved -eq $True) 
{ 
    Write-Host "Caution: 4000 users $RemText from the group." ` 
        -foregroundcolor yellow -backgroundcolor black 
    Write-Host "         Run the script again to process more." ` 
        -foregroundcolor yellow -backgroundcolor black 
} 
Else {Write-Host "Users $RemText from the group: $Removed"} 
 
If ($TooManyAdded -eq $True) 
{ 
    Write-Host "Caution: 4000 users $AddedText to the group." ` 
        -foregroundcolor yellow -backgroundcolor black 
    Write-Host "         Run the script again to process more." ` 
        -foregroundcolor yellow -backgroundcolor black 
} 
Else {Write-Host "Users $AddText to the group: $Added"}