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