I’ve been helping a customer along the path towards a proper IAM solution, which has involved a lot of data clean-up, as it so often does. Criteria groups in MIM can encourage data quality as users don’t get the groups they need if their attributes aren’t correct – so I thought, how about getting them used to that idea as soon as possible?
The result was a scheduled PowerShell script which populates groups based on a filter rule written to the group itself. It uses only attributes already in AD and is managed through AD Users and Computers.
To make sure the script only targets the right groups, I’ve asked for them to be moved to a “Managed Groups” OU, and then it’s just a matter of putting a valid PowerShell filter statement in the Notes field. The script is scheduled to run hourly and figures out if any members need to change based on the filter.
I like the way the rule is clearly visible to the people managing AD, and if they’re trying to figure out why a user is not in the group they only have to check the Notes field for exact attribute values, then update the user to match. The data gets cleaner and they get used to the idea of automation – win-win!
Here is the script. Note that the lines which change the groups are initially commented out so this will log only, until you’re ready to un-comment:
Import-Module ActiveDirectory
$LogFile = "Update-Members-{0}.log" -f (get-date).ToString("s").Replace(":","")
"Update-Members.ps1 starting" | out-file $LogFile -Encoding default
$domainController = (Get-ADDomainController).Hostname
$Groups = Get-ADGroup -Server $domainController -SearchBase "OU=Managed Groups,OU=MyOrg,DC=mydomain,DC=com" -Properties "*" #change OU and/or add a filter#
$ADUsers = Get-ADUser -Filter * -Properties *
foreach ($grp in $Groups | sort -Property Name)
{
"`r`n" + $grp.DistinguishedName | Add-Content $LogFile
## Get the filter from the info (Notes) field on the group, and add a rule to select Enabled users only
$filter = '$_.Enabled -and (' + $grp.info + ')'
"Membership filter: $filter" | Add-Content $LogFile
try
{
$scriptFilter = [scriptblock]::create($filter)
$ExpectedMembers = ($ADUsers | where $scriptFilter).DistinguishedName
## Get current members
$AllMembers = Get-ADGroupMember -Identity $grp.DistinguishedName
$CurrentMembers = @(($AllMembers | where {$_.objectClass -eq "user"}).DistinguishedName)
## Compare
$MembersGood = @()
$MembersAdd = @()
$MembersRemove = @()
if ($ExpectedMembers -and $CurrentMembers)
{
$CompUsers = compare-object -ReferenceObject $ExpectedMembers -DifferenceObject $CurrentMembers -IncludeEqual
foreach ($comp in $CompUsers)
{
if ($comp.SideIndicator -eq "==") {$MembersGood += $comp.InputObject}
elseif ($comp.SideIndicator -eq "=>") {$MembersRemove += $comp.InputObject}
elseif ($comp.SideIndicator -eq "<=") {$MembersAdd += $comp.InputObject}
}
}
elseif ($CurrentMembers)
{
$MembersRemove = $CurrentMembers
}
elseif ($ExpectedMembers)
{
$MembersAdd = $ExpectedMembers
}
foreach($m in $MembersGood) {"OK: $m" | Add-Content $LogFile}
if ($MembersAdd -ne $null)
{
foreach($m in $MembersAdd) {"Add: $m" | Add-Content $LogFile}
#Following line commented – logs only. Remove comment to make changes.
#Add-ADGroupMember -Identity $grp.DistinguishedName -Members $MembersAdd -Server $domainController
}
if ($MembersRemove -ne $null)
{
foreach($m in $MembersRemove) {"Remove: $m" | Add-Content $LogFile}
#Following line commented – logs only. Remove comment to make changes.
#Remove-ADGroupMember -Identity $grp.DistinguishedName -Members $MembersRemove -Server $domainController -Confirm:$false
}
}
catch
{
"Error: " + $Error[0].Message | Add-Content $LogFile
}
}
Nice work, Carol! Funny fact : almost exactly the same as the prototype I built 6 years ago for Dynamic Groups in Azure AD 🙂
Clearly an excellent idea then! I did think, while doing it, wouldn’t it be nice if AD groups just worked this way?