# 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.
#
# Copyright (c) 2015-2017 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.
#
# ----------------------------------------------------------------------
# You have a royalty-free right to use, modify, reproduce, and
# distribute this script file in any way you find useful, provided that
# you agree that the copyright owner above has no warranty, obligations,
# or liability for such use.

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"}