#Manage membership and filters for automated DLs
#v3.5.1 3/27/17
#3.5.1 Added separate field for group mirroring, so no longer necessary to manually enter object GUIDs and use a special format,
# and added corresponding GroupMirror parameter to Set-AutoDLFilter for updating from command line
#3.4.5 Added option of excluding members in GUI and command line, added membership preview window and option to copy preview to clipboard,
# Added syntax checking of the LDAP filter when field loses focus
#3.3.1 Updated to conform with PSScriptAnalyzer recommendations
#3.3.0 Added logging of count of members added and removed, and option to include specific users added and removed
# Changed output of Update-AutoDL to custom object
#3.2.1 Moved Note text to Variables region for easier customization, fixed typo when displaying static membership attribute
#3.2 Added domain restrictions to GUI, fixed hard inclusions not being saved when using GUI, module code reorganized
#region Customizable Variables
$AutoDLCustomAttributeNumber = '14' #Number of the extensionAttribute (custom attribute) to indicate an automated DL
$AutoDLString = 'AutoDL' #The value stored in the above attribute to indicate an automated DL
$hardIncludeAttribute = 'sAMAccountName' #Attribute used to match values for hard inclusion
$noteText = 'NOTE: This group is updated automatically via an agent and should not be updated manually.' #String to be added to the Note property when a group is converted to automated
[array]$mailRecipients = "admin@company.com","anotheradmin@company.com" #One or more addresses to receive admin notifications
$mailSender = 'AutoDLUpdate@company.com' #Sender address of admin notification
$mailSubject = 'Automated DL update alert notification' #Subject of admin notification
$mailServer = 'smtp.company.com' #SMTP server to send admin notification
#Filter that will be applied to every DL regardless of its specific filter. For example, to exclude
#terminated users or require members to be a mail-enabled object so that you don't have to remember to include
#it in the filter for every DL.
$globalFilter = "(|(&(objectcategory=user)(mailnickname=*)(!employeeType=T*)(!employeeType=CT))(objectcategory=group))"
#endregion Customizable Variables
#region Helper Functions
$AutoDLAttribute = 'extensionAttribute' + $AutoDLCustomAttributeNumber
$exAutoDLAttribute = 'CustomAttribute' + $AutoDLCustomAttributeNumber
#requires -Version 3
function Format-LDAPDisplay ($infilter)
{
#Replace pipes with crosshatches to keep them from interfering in regex
$infilter = $infilter.Replace('|','#')
#Iterate through each character in filter and insert CRLF and nesting
$iPos = 0
$iIndentCount = 0
while ($iPos + 1 -lt $infilter.Length)
{
$currCharacter = $infilter.Substring($iPos, 1)
$iWhileIndent = 1
$sIndentation = ''
switch -regex ($currCharacter)
{
#LDAP operators to watch for to modify nesting level.
#NOT operator ignored because only used in one-off attribute value
"[]"
{
#Operator followed by open paren means nesting increase
if ($infilter.Substring($iPos + 1, 1) -eq '(')
{
$iIndentCount ++
#Build nest based on number of indentations
while ($iWhileIndent -le $iIndentCount)
{
#Use unique string as placeholder for nesting with HTML spaces
$sIndentation += '|QZNBSPQZNBSPQZNBSPQZNBSP'
$iWhileIndent ++
}
#Insert new string for formatting
$regx = [regex]$currCharacter
$infilter = $regx.Replace($infilter, "$currCharacter
$sIndentation", 1, $iPos)
#Move current position to next character after inserted string
$iPos += $sIndentation.Length + 5
}
else
{
$iPos ++
}
}
"[\(]"
{
while ($iWhileIndent -le $iIndentCount)
{
$sIndentation += "|QZNBSPQZNBSPQZNBSPQZNBSP"
$iWhileIndent ++
}
if ($sIndentation -ne '')
{
if ($iPos -ne 0)
{
#If open paren follows close paren, insert line break
if ($infilter.Substring($iPos - 1, 1) -eq ')')
{
$sIndentation += '
'
$regx = [regex]'\('
$sNewLDAPFilter = $regx.Replace($infilter, "$sIndentation$currCharacter", 1, $iPos)
$infilter = $infilter.Substring(0, $iPos - 1) + $sNewLDAPFilter
$iPos += $sIndentation.Length + 1
}
else
{
$iPos ++
}
}
else
{
$iPos ++
}
}
else
{
$iPos ++
}
}
"[\)]"
{
#Two consecutive close paren means nesting reduces one level
if ($infilter.Substring($iPos + 1, 1) -eq ')')
{
$iIndentCount --
$bDblClose = $true
}
while ($iWhileIndent -le $iIndentCount)
{
$sIndentation += '|QZNBSPQZNBSPQZNBSPQZNBSP'
$iWhileIndent ++
}
$regx = [regex]'\)'
$infilter = $regx.Replace($infilter, "$currCharacter
$sIndentation", 1, $iPos)
#Adjust position to account for two close paren
If ($bDblClose)
{
$iPos += $sIndentation.Length + 5
$bDblClose = $null
}
Else
{
$iPos += $sIndentation.Length + 6
}
}
default
{
#No paren or operator means move to next character
$iPos ++
}
}
}
#Replace LDAP operators with words
$infilter = $infilter -replace '&', 'AND'
$infilter = $infilter -replace '#', 'OR'
$infilter = $infilter -replace '!', 'NOT '
#'Replace spaceholders with HTML spaces
$infilter = $infilter -replace 'QZNBSP', ' '
$infilter
}
function Test-DistributionGroup($inputParam)
{
try
{
Write-Verbose -Message 'Validating distribution group exists.'
$DL = Get-DistributionGroup -Identity $inputParam -ErrorAction Stop
$DL.DistinguishedName
}
catch
{
Write-Error -Message "The DL $inputParam cannot be found." -ErrorAction Stop -Category ObjectNotFound
}
}
function Get-ADObject($dn)
{
Write-Verbose -Message 'Binding to object in Active Directory.'
New-Object -TypeName System.DirectoryServices.DirectoryEntry -ArgumentList "LDAP://$dn"
}
function ConvertFrom-ExtensionData($data)
{
Write-Verbose -Message 'Decoding contents of extensionData attribute.'
$decodedFilter = [System.Text.UTF8Encoding]::UTF8.GetString($data)
return $decodedFilter.Substring($decodedFilter.IndexOf('') + 8, $decodedFilter.IndexOf('') - 8)
}
function Get-LDAPFilterString($string)
{
Write-Verbose -Message 'Getting LDAP filter from decoded string.'
$string.Substring($string.IndexOf('') + 8,$string.IndexOf('') - $string.IndexOf('') - 8)
}
function Get-HardMembershipString($string)
{
Write-Verbose -Message 'Getting hard inclusions from decoded string.'
$string.Substring($string.IndexOf('') + 9,$string.IndexOf('') - $string.IndexOf('') - 9)
}
function Get-ExcludeMemberString($string)
{
Write-Verbose -Message 'Getting hard exclusions from decoded string.'
try
{
$string.Substring($string.IndexOf('') + 9,$string.IndexOf('') - $string.IndexOf('') - 9)
}
catch
{
return $null
}
}
function Get-DomainRestrictionString($string)
{
Write-Verbose -Message 'Getting domain restrictions from decoded string.'
$string.Substring($string.IndexOf('') + 9,$string.IndexOf('') - $string.IndexOf('') - 9)
}
function Get-SearchBaseString($string)
{
Write-Verbose -Message 'Getting search base from decoded string.'
$string.Substring($string.IndexOf('') + 12,$string.IndexOf('') - $string.IndexOf('') - 12)
}
function Show-IEOutput($body, $displayName)
{
$title = "LDAP Filter for $displayName"
$html = "$title$body"
$html | Out-File -FilePath $env:temp\AutoDLOutput.html
$ie = New-Object -ComObject "InternetExplorer.Application"
$ie.AddressBar = $false
$ie.Menubar = $false
$ie.Toolbar = $false
$ie.Resizable = $true
$ie.Height = 450
$ie.Width = 500
$ie.Visible = $true
$ie.Navigate("$env:temp\AutoDLOutput.html")
do
{
Start-Sleep -Milliseconds 100
}
while ($ie.Busy)
Move-Window
Remove-Item -Path $env:temp\AutoDLOutput.html -ErrorAction SilentlyContinue
}
function Move-Window
{
#Attempt to bring a window to the foreground given the process's window handle
$code = @"
using System;
using System.Runtime.InteropServices;
public class WindowManagement {
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetForegroundWindow(IntPtr hWnd);
}
"@
Add-Type -TypeDefinition $code
$handle = Get-Process -Name iexplore -ErrorAction 'SilentlyContinue' | Where-Object {$_.MainWindowTitle -like 'LDAP Filter for*'} | Select-Object -ExpandProperty MainWindowHandle
[void][WindowManagement]::SetForegroundWindow($handle)
}
function Connect-ADForest
{
Write-Verbose -Message 'Checking if variable indicates shell is connected to AD forest.'
if (-not($ConnectedToADForest))
{
Write-Verbose -Message 'Not connected to AD forest. Connecting...'
$ad = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
$root = "GC://$($ad.RootDomain)"
$global:ADForestSearcher = New-Object -TypeName System.DirectoryServices.DirectorySearcher -ArgumentList $root
$ADForestSearcher.SearchRoot = $root
$ADForestSearcher.PageSize = 1000
$ADForestSearcher.PropertiesToLoad.AddRange(@('distinguishedname','mailnickname','displayname'))
$global:ConnectedToADForest = $true
}
Write-Verbose -Message 'Already connected to AD forest.'
}
function Connect-ADDomain ($domain)
{
Write-Verbose -Message "Connecting to specific domain: $($domain)"
$root = "LDAP://$domain"
$script:ADDomainSearcher = New-Object -TypeName System.DirectoryServices.DirectorySearcher -ArgumentList $root
$ADDomainSearcher.SearchRoot = $root
$ADDomainSearcher.PageSize = 1000
$ADDomainSearcher.PropertiesToLoad.AddRange(@('distinguishedname','mailnickname','displayname'))
}
function Convert-GUIDtoDN($guidString)
{
Write-Verbose -Message "Converting GUID string to DNs: $guidString"
$guidArray = $guidString -split ';'
$dnarray = @()
$ad = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
$root = 'GC://' + $ad.RootDomain
foreach ($guid in $guidArray)
{
$dirObject = New-Object -TypeName System.DirectoryServices.DirectoryEntry -ArgumentList ($root + "/")
if ($dirObject.Path)
{
$dnarray += $dirObject.Properties.distinguishedName
}
}
,$dnarray
}
function Test-Domain ([string[]]$string)
{
Write-Verbose -Message 'Validating that domain is in current forest.'
$ad = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
[array]$domains = $ad.Domains | Select-Object -ExpandProperty Name
foreach ($s in $string)
{
if ($domains -notcontains $s)
{
return $false
}
}
$true
}
function Get-Domain
{
Write-Verbose -Message 'Getting list of domains in the forest.'
$ad = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
[array]$domains = $ad.Domains | Select-Object -ExpandProperty Name
$domains
}
function Get-AutomatedGroup ($domain)
{
$ADSort = New-Object -TypeName System.DirectoryServices.SortOption -ArgumentList ('displayName',[System.DirectoryServices.SortDirection]::Descending)
$filter = "(&(objectCategory=group)($AutoDLAttribute=$AutoDLString))"
if ($domain)
{
Write-Verbose -Message "Getting list of automated groups in domain $domain."
Connect-ADDomain -domain $domain
$ADDomainSearcher.Filter = $filter
$ADDomainSearcher.Sort = $ADSort
$ADDomainSearcher.FindAll()
}
else
{
Write-Verbose -Message 'Getting list of automated groups in the forest.'
Connect-ADForest
$ADForestSearcher.Filter = $filter
$ADForestSearcher.Sort = $ADSort
$ADForestSearcher.FindAll()
}
}
function Test-LDAPFilterSyntax ($filter)
{
Connect-ADForest
$ADForestSearcher.Filter = $filter
try
{
$ADForestSearcher.FindOne() | Out-Null
$true
}
catch
{
if ($_.Exception -like '*search filter is invalid.')
{
$false
}
}
}
function Test-GroupDN ($dnarray)
{
foreach ($dn in $dnarray)
{
$dirObject = Get-ADObject -dn $dn
if (-not($dirObject.objectClass -contains 'group'))
{
return $false
}
}
$true
}
function Build-GroupMembership($filter, $domain)
{
if ($domain)
{
Connect-ADDomain -domain $domain
$ADDomainSearcher.Filter = $filter
$entries = $ADDomainSearcher.FindAll()
}
else
{
Connect-ADForest
$ADForestSearcher.Filter = $filter
$entries = $ADForestSearcher.FindAll()
}
foreach ($entry in $entries)
{
if ($entry.properties.objectclass -contains 'group')
{
Build-GroupMembership -filter "(memberof=$($entry.Properties.distinguishedname))"
}
elseif (!($transMembership.Contains($entry.Properties.distinguishedname)))
{
$script:transMembership += "$($entry.Properties.distinguishedname);"
$script:transMembershipDisplay += "$($entry.Properties.displayname);"
}
}
}
function Update-Group
{
param
(
$dn,
[switch]$Preview
)
$ADS_PROPERTY_CLEAR = 1
$ADS_PROPERTY_UPDATE = 2
$globalFilterStart = "(&" + $globalFilter
$globalFilterEnd = ")"
if (-not($Preview))
{
$outputItem = "" | Select-Object -Property Name,OldCount,NewCount,AddedCount,RemovedCount,AddedMembers,RemovedMembers,Error
$group = Get-ADObject -dn $dn
Write-Verbose -Message ('Updating ' + $group.displayName)
$oldCount = $group.Properties.Member.Count
if ($oldCount -eq 1500) #membership is above ADSI limit for multi-valued attribute listing
{
Connect-ADForest
#invert search to get accurate count
$ADForestSearcher.Filter = "$globalFilterStart(memberof=$dn)$globalFilterEnd"
$searchResult = $ADForestSearcher.FindAll()
$oldCount = $searchResult.Count
$oldMembership = $searchResult | ForEach-Object {$_.Properties.distinguishedname}
}
elseif ($oldCount -gt 0)
{
$oldMembership = $group.Properties.Member
}
else
{
$oldMembership = @()
}
#Decode LDAP filter from extensionData
$eData = ConvertFrom-ExtensionData -data $group.extensionData[0]
$LDAPFilter = Get-LDAPFilterString -string $eData
$includeMember = (Get-HardMembershipString -string $eData) -split ';'
$excludeMember = (Get-ExcludeMemberString -string $eData) -split ';'
$domains = (Get-DomainRestrictionString -string $eData) -split ';'
}
else
{
#If mirroring groups, build filter based on DNs of groups
if ($formRadioButtonMirrorChecked)
{
foreach ($line in $formMirrorGroupsText)
{
$filterGroup = $filterGroup + "(memberof=$line)"
}
$filterGroup = "(|$filterGroup)"
$LDAPFilter = $filterGroup
}
#Else use entered LDAP string
else
{
$LDAPFilter = $formLDAPFilterText
}
if ($formCheckboxHardCode)
{
$includeMember = $formHardMember
}
if ($formCheckBoxExludeMembers)
{
$excludeMember = $formExcludeMember
}
$domains = $formDomainRestrict
}
#Add restriction of excluded members to filter
if ($excludeMember)
{
foreach ($member in $excludeMember)
{
$excludeFilter += "(!" + $hardIncludeAttribute + "=$member)"
}
}
#Build global-adjusted LDAP filter
if ($LDAPFilter.IndexOf('guid:') -eq 0) #DL is mirror of another group
{
$filterDN = Convert-GUIDtoDN -guidString $LDAPFilter.Substring(5)
#If it is a mirror of multiple groups
if($filterDN.count -gt 1) #Build the filter
{
foreach($filter in $filterDN)
{
$filterstring = $filterstring + "(memberof=$filter)"
}
$searchFilter = $globalFilterStart + "(|$filterstring)" + $excludeFilter + $globalFilterEnd
}
else
#Mirror of a single group
{
$searchFilter = $globalFilterStart + "(memberof=$filterDN)" + $excludeFilter + $globalFilterEnd
}
}
else
{
$searchFilter = $globalFilterStart + $LDAPFilter + $excludeFilter + $globalFilterEnd
}
#Search for matching users and add to array
$script:transMembership = ""
if ($domains)
{
foreach ($domain in $domains)
{
Build-GroupMembership -filter $searchFilter -domain $domain
}
}
else
{
Build-GroupMembership -filter $searchFilter
}
#Add hard-coded members
if ($includeMember)
{
foreach ($member in $includeMember)
{
$includeFilter = "$globalFilterStart($hardIncludeAttribute=$member)$globalFilterEnd"
Build-GroupMembership -filter $includeFilter
}
}
if ($transMembership.Length -gt 0)
{
$newMembership = @(($transMembership.Substring(0,$transMembership.LastIndexOf(';'))).Split(';'))
$newCount = $newMembership.Length
}
else
{
$newCount = 0
$newMembership = @()
}
if (($oldCount -gt 0) -and (-not($newCount -gt 0)) -and (-not($Preview)))
{
$script:notifyAdmin = $true
$script:notifyGroups += $group.DisplayName + "
"
}
if (-not($Preview))
{
#Get to-be-added/removed members
$differences = Compare-Object -ReferenceObject $oldMembership -DifferenceObject $newMembership
[array]$pendingAdd = $differences | Where-Object {$_.SideIndicator -eq '=>'} | Select-Object -ExpandProperty InputObject
[array]$pendingRemove = $differences | Where-Object {$_.SideIndicator -eq '<='} | Select-Object -ExpandProperty InputObject
if ($verboseLogging)
{
$outputItem.AddedMembers = $pendingAdd
$outputItem.RemovedMembers = $pendingRemove
}
$outputItem.Name = $group.Properties.DisplayName[0]
$outputItem.OldCount = $oldCount
$outputItem.NewCount = $newCount
$outputItem.AddedCount = $pendingAdd.Count
$outputItem.RemovedCount = $pendingRemove.Count
try
{
#Save new membership
if ($newCount -ge 1)#Overwrite existing membership with new membership
{
$group.PutEx($ADS_PROPERTY_UPDATE,'member',$newMembership)
$group.SetInfo()
}
else
{#Clear old membership if new membership is 0
$group.PutEx($ADS_PROPERTY_CLEAR,'member',$null)
$group.SetInfo()
}
Write-Output -InputObject $outputItem
}
catch
{
$outputItem.Error = $_
$script:notifyAdmin = $true
$script:notifyGroups += $group.DisplayName + "
"
Write-Output -InputObject $outputItem
Write-Error -Message "Error saving new membership:$_"
}
finally
{
[void]$output.Add($outputItem)
}
}
else
{
$newMembershipDisplay = @(($transMembershipDisplay.Substring(0,$transMembershipDisplay.LastIndexOf(';'))).Split(';')) | Sort-Object
if ($newMembershipDisplay.Length -gt 0)
{
@($newMembershipDisplay, $newCount)
}
else
{
@('','-')
}
}
}
#endregion Helper Functions
#region Forms
#region Form Load/Exit Functions
function OnApplicationLoad
{
#Note: This function runs before the form is created
#TODO: Add snapins and custom code to validate the application load
return $true #return true for success or false for failure
}
function OnApplicationExit
{
#Note: This function runs after the form is closed
#TODO: Add custom code to clean up and unload snapins when the application exits
#$script:ExitCode = 0
}
#endregion Form Load/Exit Functions
function Open-AutoDLForm
{
#region Import the Assemblies
[void][reflection.assembly]::Load("mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
[void][reflection.assembly]::Load("System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
[void][reflection.assembly]::Load("System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")
[void][reflection.assembly]::Load("System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
[void][reflection.assembly]::Load("System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
[void][reflection.assembly]::Load("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
[void][reflection.assembly]::Load("System.DirectoryServices, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")
[void][reflection.assembly]::Load("System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
[void][reflection.assembly]::Load("System.ServiceProcess, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")
#endregion Import Assemblies
#region Form Events
$FormEvent_Load =
{
if ($hasExistingFilter = $true)
{
if ($mirrorsGroup)
{
$radiobuttonLDAP.Checked = $false
$LDAPFilterText.Enabled = $false
$LDAPFilterText.BackColor = 'Control'
$LDAPFilterText.TabStop = $false
$radiobuttonMirror.Checked = $true
$mirrorGroupsText.Enabled = $true
$mirrorGroupsText.BackColor = 'Window'
$mirrorGroupsText.Lines = $groupToMirror
}
else
{
$radiobuttonMirror.Checked = $false
$mirrorGroupsText.Enabled = $false
$mirrorGroupsText.BackColor = 'Control'
$mirrorGroupsText.TabStop = $false
$radiobuttonLDAP.Checked = $true
$LDAPFilterText.Enabled = $true
$LDAPFilterText.BackColor = 'Window'
$LDAPFilterText.Text = $currentLDAPFilter
}
if ($currentHardMembership)
{
$checkboxHardCode.Checked = $true
$hardMember.Lines = $currentHardMembership
$hardMember.TabStop = $true
}
if ($currentExcludeMembership)
{
$checkboxExcludeMembers.Checked = $true
$excludeMember.Lines = $currentExcludeMembership
$excludeMember.TabStop = $true
}
if ($currentDomainRestrict)
{
$checkboxDomainRestrict.Checked = $true
$restrictedDomains.BackColor = 'Window'
$restrictedDomains.TabStop = $true
}
else
{
$restrictedDomains.BackColor = 'Control'
}
Enable-OKButton
}
#Populate list box with valid domains
Initialize-DomainListBox
#Check for AD Object picker module and disable button if not loaded
if ($objectPickerLoaded -eq $false)
{
$buttonObjectPicker.Enabled = $false
$labelObjectPicker.Visible = $true
}
}
$checkboxHardCode_CheckedChanged =
{
if ($checkboxHardCode.Checked)
{
$hardMember.Enabled = $true
$hardMember.TabStop = $true
}
else
{
$hardMember.Enabled = $false
$hardMember.TabStop = $false
}
Enable-OKButton
}
$checkboxExcludeMembers_CheckedChanged =
{
if ($checkboxExcludeMembers.Checked)
{
$excludeMember.Enabled = $true
$excludeMember.TabStop = $true
}
else
{
$excludeMember.Enabled = $false
$excludeMember.TabStop = $false
}
Enable-OKButton
}
$buttonCancel_Click=
{
$formSetAutomatedDLFilter.AutoValidate = 'Disable'
}
$hardMemberEnter =
{
if ($hardMember.Text -like "Enter*")
{
$hardMember.Text = $null
}
}
$excludeMemberEnter =
{
if ($excludeMember.Text -like "Enter*")
{
$excludeMember.Text = $null
}
}
$ldapFilterTextValidation =
{
Enable-OKButton
}
$ldapFilterTextLeave =
{
if ($buttonCancel.Focused)
{return}
if (-not($running))
{
$script:running = $true
if (-not(Test-LDAPFilterSyntax -filter $LDAPFilterText.Text))
{
$buttonPreview.Enabled = $false
$buttonOK.Enabled = $false
[System.Windows.Forms.MessageBox]::Show('The search filter syntax is not valid.', 'Invalid Syntax', [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Exclamation)
$LDAPFilterText.Focus()
}
$script:running = $false
}
}
$formStoreValues =
{
Set-Variable -Name formCheckBoxHardCode -Scope 3 -Value $checkboxHardCode.Checked
Set-Variable -Name formCheckBoxExludeMembers -Scope 3 -Value $checkboxExcludeMembers.Checked
Set-Variable -Name formRadioButtonLDAPChecked -Scope 3 -Value $radiobuttonLDAP.Checked
Set-Variable -Name formLDAPFilterText -Scope 3 -Value $LDAPFilterText.Text
Set-Variable -Name formRadioButtonMirrorChecked -Scope 3 -Value $radiobuttonMirror.Checked
Set-Variable -Name formMirrorGroupsText -Scope 3 -Value $mirrorGroupsText.Lines
Set-Variable -Name formHardMember -Scope 3 -Value $hardMember.Lines
Set-Variable -Name formexcludeMember -Scope 3 -Value $excludeMember.Lines
Set-Variable -Name formDomainRestrict -Scope 3 -Value $restrictedDomains.CheckedItems
Set-Variable -Name formCheckBoxDomainRestrict -Scope 3 -Value $checkboxDomainRestrict.Checked
}
$buttonPreview_Click =
{
&$formStoreValues
Open-PreviewForm
}
$buttonOK_Click =
{
&$formStoreValues
$formSetAutomatedDLFilter.Close()
}
$checkboxDomainRestrict_CheckedChanged =
{
if ($checkboxDomainRestrict.Checked)
{
$restrictedDomains.Enabled = $true
$restrictedDomains.BackColor = 'Window'
$restrictedDomains.TabStop = $true
}
else
{
$restrictedDomains.Enabled = $false
$restrictedDomains.BackColor = 'Control'
$restrictedDomains.TabStop = $false
}
Enable-OKButton
}
$hardMemberValidation =
{
Enable-OKButton
}
$excludeMemberValidation =
{
Enable-OKButton
}
$restrictedDomainsValidation =
[System.Windows.Forms.ItemCheckEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.ItemCheckEventArgs]
if ($restrictedDomains.CheckedItems.Count -eq 1 -and $_.NewValue -eq [System.Windows.Forms.CheckState]::Unchecked)
{#The collection is about to be emptied: there's just one item checked, and it's being unchecked at this moment
$buttonOK.Enabled = $false
$buttonPreview.Enabled = $false
}
else
{#The collection will not be empty once this click is handled
Enable-OKButton -domainIsOK $true
}
}
$radiobuttonLDAP_CheckedChanged =
{
if ($radiobuttonLDAP.Checked)
{
$LDAPFilterText.Enabled = $true
$LDAPFilterText.BackColor = 'Window'
$LDAPFilterText.TabStop = $true
$mirrorGroupsText.Enabled = $false
$mirrorGroupsText.BackColor = 'Control'
$mirrorGroupsText.TabStop = $false
$buttonObjectPicker.Enabled = $false
Enable-OKButton
}
}
$radiobuttonMirror_CheckedChanged =
{
if ($radiobuttonMirror.Checked)
{
$mirrorGroupsText.Enabled = $true
$mirrorGroupsText.BackColor = 'Window'
$mirrorGroupsText.TabStop = $true
$LDAPFilterText.Enabled = $false
$LDAPFilterText.BackColor = 'Control'
$LDAPFilterText.TabStop = $false
if ($objectPickerLoaded)
{
$buttonObjectPicker.Enabled = $true
}
Enable-OKButton
}
}
$mirrorGroupsText_Leave =
{
if ($buttonCancel.Focused)
{return}
if (-not($running))
{
$script:running = $true
if (-not($mirrorGroupsText.Text.Length -gt 0 -and (Test-GroupDN -dnarray $mirrorGroupsText.Lines)))
{
$buttonPreview.Enabled = $false
$buttonOK.Enabled = $false
[System.Windows.Forms.MessageBox]::Show('A distinguished name is not valid.', 'Invalid DN', [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Exclamation)
$mirrorGroupsText.Focus()
}
$script:running = $false
}
}
$mirrorGroupsText_TextChanged =
{
Enable-OKButton
}
$mirrorGroupsText_Enter =
{
if ($mirrorGroupsText.Text -like "Enter*")
{
$mirrorGroupsText.Text = $null
}
}
$buttonObjectPicker_Click =
{
&$mirrorGroupsText_Enter
$pickerResult = Open-ADObjectPicker
if ($pickerResult[0] -eq 'OK' -and $pickerResult.Count -gt 1)
{
$newLines = @()
for ($i = 1; $i -le $pickerResult.Count - 1; $i++)
{
$newLines += $pickerResult[$i].FetchedAttributes[0]
}
if ($mirrorGroupsText.TextLength -gt 0)
{
$combinedLines = $mirrorGroupsText.Lines + $newLines
$mirrorGroupsText.Lines = $combinedLines
}
else
{
$mirrorGroupsText.Lines = $newLines
}
}
}
$Form_StateCorrection_Load =
{
#Correct the initial state of the form to prevent the .NET maximized form issue
$formSetAutomatedDLFilter.WindowState = $InitialFormWindowState
}
$Form_Cleanup_FormClosed =
{
#Remove all event handlers from the controls
$buttonObjectPicker.remove_Click($buttonObjectPicker_Click)
$mirrorGroupsText.remove_TextChanged($mirrorGroupsText_TextChanged)
$mirrorGroupsText.remove_Enter($mirrorGroupsText_Enter)
$mirrorGroupsText.remove_Leave($mirrorGroupsText_Leave)
$radiobuttonMirror.remove_CheckedChanged($radiobuttonMirror_CheckedChanged)
$radiobuttonLDAP.remove_CheckedChanged($radiobuttonLDAP_CheckedChanged)
$buttonPreview.remove_Click($buttonPreview_Click)
$excludeMember.remove_TextChanged($excludeMemberValidation)
$excludeMember.remove_Enter($excludeMemberEnter)
$checkboxExcludeMembers.remove_CheckedChanged($checkboxExcludeMembers_CheckedChanged)
$restrictedDomains.remove_ItemCheck($restrictedDomainsValidation)
$checkboxDomainRestrict.remove_CheckedChanged($checkboxDomainRestrict_CheckedChanged)
$checkboxHardCode.remove_CheckedChanged($checkboxHardCode_CheckedChanged)
$buttonCancel.remove_Click($buttonCancel_Click)
$hardMember.remove_Enter($hardMemberEnter)
$LDAPFilterText.remove_TextChanged($ldapFilterTextValidation)
$LDAPFilterText.remove_Leave($ldapFilterTextLeave)
$buttonOK.remove_Click($buttonOK_Click)
$checkboxDomainRestrict.remove_CheckedChanged($checkboxDomainRestrict_CheckedChanged)
$hardMember.remove_TextChanged($hardMemberValidation)
$formSetAutomatedDLFilter.remove_Load($FormEvent_Load)
$formSetAutomatedDLFilter.remove_Load($Form_StateCorrection_Load)
$formSetAutomatedDLFilter.remove_FormClosed($Form_Cleanup_FormClosed)
}
#endregion Form Events
#region Form Helper Functions
function Initialize-DomainListBox
{
$list = Get-Domain
#$restrictedDomains is the name of the checked list box defined in the form
foreach ($element in $list)
{
[void]$restrictedDomains.Items.Add($element)
}
if ($currentDomainRestrict)
{
for ($i = 0; $i -lt $restrictedDomains.Items.Count; $i++)
{
if ($currentDomainRestrict -contains $restrictedDomains.Items[$i])
{
$restrictedDomains.SetItemChecked($i, $true)
}
}
}
}
function Enable-OKButton ($domainIsOK)
{
#Check for LDAP filter or mirrored DNs
if (($radiobuttonLDAP.Checked -and $LDAPFilterText.TextLength -gt 0) -or ($radiobuttonMirror.Checked -and $mirrorGroupsText.TextLength -gt 0))
{$passFilter = $true}
#Check for hard-coded members if enabled
if (($checkboxHardCode.Checked -and $hardMember.TextLength -gt 0 -and $hardMember.Text -notlike 'Enter*') -or (-not($checkboxHardCode.Checked)))
{$passHardMember = $true}
#Check for excluded members if enabled
if (($checkboxExcludeMembers.Checked -and $excludeMember.TextLength -gt 0 -and $excludeMember.Text -notlike 'Enter*') -or (-not($checkboxExcludeMembers.Checked)))
{$passExcludeMember = $true}
#Check for domain restrictions if enabled (If function is called from restricted-domains item-checked event,
#the state of the checkbox hasn't been committed so can produce false failure to pass. Use of this function's parameter
#is to override that failure only when the calling event has determined there will be at least one checked item when done.)
if (($checkboxDomainRestrict.Checked -and $restrictedDomains.CheckedItems) -or (-not($checkboxDomainRestrict.Checked)) -or ($domainIsOK))
{$passDomain = $true}
if ($passFilter -and $passHardMember -and $passDomain -and $passExcludeMember)
{
$buttonOK.Enabled = $true
$buttonPreview.Enabled = $true
}
else
{
$buttonOK.Enabled = $false
$buttonPreview.Enabled = $false
}
}
#endregion Form Helper Functions
#region Form Objects
[System.Windows.Forms.Application]::EnableVisualStyles()
$formSetAutomatedDLFilter = New-Object -TypeName System.Windows.Forms.Form
$labelObjectPicker = New-Object -TypeName System.Windows.Forms.Label
$buttonObjectPicker = New-Object -TypeName System.Windows.Forms.Button
$mirrorGroupsText = New-Object -TypeName System.Windows.Forms.TextBox
$radiobuttonMirror = New-Object -TypeName System.Windows.Forms.RadioButton
$radiobuttonLDAP = New-Object -TypeName System.Windows.Forms.RadioButton
$line1 = New-Object -TypeName System.Windows.Forms.Label
$buttonPreview = New-Object -TypeName System.Windows.Forms.Button
$excludeMember = New-Object -TypeName System.Windows.Forms.TextBox
$checkboxExcludeMembers = New-Object -TypeName System.Windows.Forms.CheckBox
$restrictedDomains = New-Object -TypeName System.Windows.Forms.CheckedListBox
$checkboxDomainRestrict = New-Object -TypeName System.Windows.Forms.CheckBox
$buttonCancel = New-Object -TypeName System.Windows.Forms.Button
$checkboxHardCode = New-Object -TypeName System.Windows.Forms.CheckBox
$hardMember = New-Object -TypeName System.Windows.Forms.TextBox
$LDAPFilterText = New-Object -TypeName System.Windows.Forms.TextBox
$buttonOK = New-Object -TypeName System.Windows.Forms.Button
$InitialFormWindowState = New-Object -TypeName System.Windows.Forms.FormWindowState
# formSetAutomatedDLFilter
$formSetAutomatedDLFilter.Controls.Add($labelObjectPicker)
$formSetAutomatedDLFilter.Controls.Add($buttonObjectPicker)
$formSetAutomatedDLFilter.Controls.Add($mirrorGroupsText)
$formSetAutomatedDLFilter.Controls.Add($radiobuttonMirror)
$formSetAutomatedDLFilter.Controls.Add($radiobuttonLDAP)
$formSetAutomatedDLFilter.Controls.Add($buttonPreview)
$formSetAutomatedDLFilter.Controls.Add($excludeMember)
$formSetAutomatedDLFilter.Controls.Add($checkboxExcludeMembers)
$formSetAutomatedDLFilter.Controls.Add($restrictedDomains)
$formSetAutomatedDLFilter.Controls.Add($checkboxDomainRestrict)
$formSetAutomatedDLFilter.Controls.Add($buttonCancel)
$formSetAutomatedDLFilter.Controls.Add($checkboxHardCode)
$formSetAutomatedDLFilter.Controls.Add($hardMember)
$formSetAutomatedDLFilter.Controls.Add($LDAPFilterText)
$formSetAutomatedDLFilter.Controls.Add($buttonOK)
$formSetAutomatedDLFilter.Controls.Add($line1)
$formSetAutomatedDLFilter.AcceptButton = $buttonOK
$formSetAutomatedDLFilter.AutoScroll = $true
$formSetAutomatedDLFilter.ClientSize = '513, 582'
$formSetAutomatedDLFilter.FormBorderStyle = 'FixedDialog'
$formSetAutomatedDLFilter.MaximizeBox = $False
$formSetAutomatedDLFilter.MinimizeBox = $False
$formSetAutomatedDLFilter.Name = "formSetAutomatedDLFilter"
$formSetAutomatedDLFilter.StartPosition = 'CenterScreen'
$formSetAutomatedDLFilter.Text = "Set Automated DL Filter: $($ADGroup.DisplayName)"
$formSetAutomatedDLFilter.add_Load($FormEvent_Load)
# labelObjectPicker
$labelObjectPicker.Enabled = $false
$labelObjectPicker.ForeColor = 'InactiveCaption'
$labelObjectPicker.Location = '116, 247'
$labelObjectPicker.Name = "labelObjectPicker"
$labelObjectPicker.Size = '218, 23'
$labelObjectPicker.TabIndex = 15
$labelObjectPicker.Text = "(Dependent module could not be loaded)"
$labelObjectPicker.TextAlign = 'MiddleLeft'
$labelObjectPicker.Visible = $False
# buttonObjectPicker
$buttonObjectPicker.Location = '39, 248'
$buttonObjectPicker.Name = "buttonObjectPicker"
$buttonObjectPicker.Size = '75, 23'
$buttonObjectPicker.TabIndex = 14
$buttonObjectPicker.Text = "Find Groups"
$buttonObjectPicker.UseVisualStyleBackColor = $True
$buttonObjectPicker.add_Click($buttonObjectPicker_Click)
# mirrorGroupsText
$mirrorGroupsText.Location = '39, 193'
$mirrorGroupsText.Multiline = $True
$mirrorGroupsText.Name = "mirrorGroupsText"
$mirrorGroupsText.ScrollBars = 'Vertical'
$mirrorGroupsText.Size = '428, 49'
$mirrorGroupsText.TabIndex = 4
$mirrorGroupsText.TabStop = $false
$mirrorGroupsText.Text = "Enter one distinguished name per line or click Find Groups to search."
$mirrorGroupsText.AcceptsReturn = $true
$mirrorGroupsText.WordWrap = $false
$mirrorGroupsText.add_TextChanged($mirrorGroupsText_TextChanged)
$mirrorGroupsText.add_Enter($mirrorGroupsText_Enter)
$mirrorGroupsText.add_Leave($mirrorGroupsText_Leave)
# radiobuttonMirror
$radiobuttonMirror.Font = "Microsoft Sans Serif, 9.75pt"
$radiobuttonMirror.Location = '19, 163'
$radiobuttonMirror.Name = "radiobuttonMirror"
$radiobuttonMirror.Size = '267, 24'
$radiobuttonMirror.TabIndex = 3
$radiobuttonMirror.Text = "Mirror membership of existing groups:"
$radiobuttonMirror.UseVisualStyleBackColor = $True
$radiobuttonMirror.add_CheckedChanged($radiobuttonMirror_CheckedChanged)
# radiobuttonLDAP
$radiobuttonLDAP.Checked = $true
$radiobuttonLDAP.Font = "Microsoft Sans Serif, 9.75pt"
$radiobuttonLDAP.Location = '19, 28'
$radiobuttonLDAP.Name = "radiobuttonLDAP"
$radiobuttonLDAP.Size = '267, 24'
$radiobuttonLDAP.TabIndex = 1
$radiobuttonLDAP.TabStop = $true
$radiobuttonLDAP.Text = "LDAP filter:"
$radiobuttonLDAP.UseVisualStyleBackColor = $True
$radiobuttonLDAP.add_CheckedChanged($radiobuttonLDAP_CheckedChanged)
# buttonPreview
$buttonPreview.Location = '345, 547'
$buttonPreview.Name = "buttonPreview"
$buttonPreview.Size = '75, 23'
$buttonPreview.TabIndex = 12
$buttonPreview.Text = "Preview"
$buttonPreview.Enabled = $false
$buttonPreview.UseVisualStyleBackColor = $True
$buttonPreview.add_Click($buttonPreview_Click)
# excludeMember
$excludeMember.AcceptsReturn = $True
$excludeMember.Enabled = $False
$excludeMember.Location = '39, 444'
$excludeMember.Multiline = $True
$excludeMember.Name = "excludeMember"
$excludeMember.ScrollBars = 'Vertical'
$excludeMember.Size = '191, 79'
$excludeMember.TabIndex = 8
$excludeMember.Text = "Enter one $hardIncludeAttribute per line."
$excludeMember.add_TextChanged($excludeMemberValidation)
$excludeMember.add_Enter($excludeMemberEnter)
# checkboxExcludeMembers
$checkboxExcludeMembers.Location = '39, 418'
$checkboxExcludeMembers.Name = "checkboxExcludeMembers"
$checkboxExcludeMembers.Size = '191, 24'
$checkboxExcludeMembers.TabIndex = 7
$checkboxExcludeMembers.Text = "Exclude members"
$checkboxExcludeMembers.UseVisualStyleBackColor = $True
$checkboxExcludeMembers.add_CheckedChanged($checkboxExcludeMembers_CheckedChanged)
# restrictedDomains
$restrictedDomains.Enabled = $False
$restrictedDomains.CheckOnClick = $True
$restrictedDomains.Sorted = $true
$restrictedDomains.FormattingEnabled = $True
$restrictedDomains.Location = '276, 333'
$restrictedDomains.Name = "restrictedDomains"
$restrictedDomains.Size = '191, 79'
$restrictedDomains.TabIndex = 10
$restrictedDomains.TabStop = $false
$restrictedDomains.add_ItemCheck($restrictedDomainsValidation)
# checkboxDomainRestrict
$checkboxDomainRestrict.Location = '276, 307'
$checkboxDomainRestrict.Name = "checkboxDomainRestrict"
$checkboxDomainRestrict.Size = '188, 24'
$checkboxDomainRestrict.TabIndex = 9
$checkboxDomainRestrict.Text = "Restrict membership to domains"
$checkboxDomainRestrict.UseVisualStyleBackColor = $True
$checkboxDomainRestrict.add_CheckedChanged($checkboxDomainRestrict_CheckedChanged)
# buttonCancel
$buttonCancel.CausesValidation = $false
$buttonCancel.DialogResult = 'Cancel'
$buttonCancel.Location = '264, 547'
$buttonCancel.Name = "buttonCancel"
$buttonCancel.Size = '75, 23'
$buttonCancel.TabIndex = 11
$buttonCancel.Text = "Cancel"
$buttonCancel.UseVisualStyleBackColor = $True
$buttonCancel.add_Click($buttonCancel_Click)
# checkboxHardCode
$checkboxHardCode.Location = '39, 307'
$checkboxHardCode.Name = "checkboxHardCode"
$checkboxHardCode.Size = '182, 24'
$checkboxHardCode.TabIndex = 5
$checkboxHardCode.Text = "Include hard-coded members"
$checkboxHardCode.UseVisualStyleBackColor = $True
$checkboxHardCode.add_CheckedChanged($checkboxHardCode_CheckedChanged)
# hardMember
$hardMember.AcceptsReturn = $True
$hardMember.Enabled = $False
$hardMember.Location = '39, 333'
$hardMember.Multiline = $True
$hardMember.Name = "hardMember"
$hardMember.ScrollBars = 'Vertical'
$hardMember.Size = '191, 79'
$hardMember.TabIndex = 6
$hardMember.Text = "Enter one $hardIncludeAttribute per line."
$hardMember.add_TextChanged($hardMemberValidation)
$hardMember.add_Enter($hardMemberEnter)
# LDAPFilterText
$LDAPFilterText.Location = '39, 58'
$LDAPFilterText.Multiline = $True
$LDAPFilterText.Name = "LDAPFilterText"
$LDAPFilterText.ScrollBars = 'Vertical'
$LDAPFilterText.Size = '428, 90'
$LDAPFilterText.TabIndex = 2
$LDAPFilterText.add_TextChanged($ldapFilterTextValidation)
$LDAPFilterText.add_Leave($ldapFilterTextLeave)
# buttonOK
$buttonOK.Anchor = 'Bottom, Right'
$buttonOK.DialogResult = 'OK'
$buttonOK.Enabled = $False
$buttonOK.Location = '426, 547'
$buttonOK.Name = "buttonOK"
$buttonOK.Size = '75, 23'
$buttonOK.TabIndex = 13
$buttonOK.Text = "OK"
$buttonOK.UseVisualStyleBackColor = $True
$buttonOK.add_Click($buttonOK_Click)
# line1
$line1.BackColor = 'Transparent'
$line1.BorderStyle = 'Fixed3D'
$line1.CausesValidation = $false
$line1.Font = "Microsoft Sans Serif, 12pt"
$line1.Location = '12, 4'
$line1.Name = "line1"
$line1.Size = '489, 280'
$line1.TabIndex = 11
$line1.Text = "Method of determining membership"
#endregion Form Objects
#Save the initial state of the form
$InitialFormWindowState = $formSetAutomatedDLFilter.WindowState
#Init the OnLoad event to correct the initial state of the form
$formSetAutomatedDLFilter.add_Load($Form_StateCorrection_Load)
#Clean up the control events
$formSetAutomatedDLFilter.add_FormClosed($Form_Cleanup_FormClosed)
#Show the Form
return $formSetAutomatedDLFilter.ShowDialog()
}
function Open-PreviewForm
{
#region Preview Form Helper Functions
function Initialize-PreviewListBox ($items)
{
if ($items -is [System.Windows.Forms.ListBox+ObjectCollection])
{
$listBoxMember.Items.AddRange($items)
}
elseif ($items -is [Array])
{
$listBoxMember.BeginUpdate()
foreach($obj in $items)
{
$listBoxMember.Items.Add($obj)
}
$listBoxMember.EndUpdate()
}
else
{
$listBoxMember.Items.Add($items)
}
}
#endregion
#region Form Events
$Form_StateCorrection_Load =
{
#Correct the initial state of the form to prevent the .Net maximized form issue
$PreviewForm.WindowState = $InitialFormWindowState
}
$Form_Cleanup_FormClosed =
{
#Remove all event handlers from the controls
$PreviewForm.remove_Load($Form_StateCorrection_Load)
$PreviewForm.remove_Shown($previewForm_Shown)
$PreviewForm.remove_Click($buttonClose_Click)
$buttonCopyToClipboard.remove_Click($buttonCopy_Click)
$PreviewForm.remove_FormClosed($Form_Cleanup_FormClosed)
$script:transMembershipDisplay = $null
}
$previewForm_Shown =
{
$script:membershipList = Update-Group -Preview
Initialize-PreviewListBox -items $membershipList[0]
$textboxMemCount.Text = $membershipList[1]
if ($membershipList[0].Length -gt 0)
{
$buttonCopyToClipboard.Enabled = $true
}
}
$buttonCopy_Click =
{
$buttonCopyToClipboard.BackColor = 'LawnGreen'
[System.Windows.Forms.Clipboard]::SetText($membershipList[0] -join "`r`n")
Start-Sleep -Milliseconds 500
$buttonCopyToClipboard.BackColor = 'Control'
}
$buttonClose_Click =
{
$PreviewForm.Close()
}
#endregion Form Events
#region Form Objects
[System.Windows.Forms.Application]::EnableVisualStyles()
$PreviewForm = New-Object -TypeName System.Windows.Forms.Form
$buttonCopyToClipboard = New-Object -TypeName System.Windows.Forms.Button
$buttonClose = New-Object -TypeName System.Windows.Forms.Button
$textboxMemCount = New-Object -TypeName System.Windows.Forms.TextBox
$labelCount = New-Object -TypeName System.Windows.Forms.Label
$listboxMember = New-Object -TypeName System.Windows.Forms.ListBox
$InitialFormWindowState = New-Object -TypeName System.Windows.Forms.FormWindowState
# PreviewForm
$PreviewForm.Controls.Add($buttonCopyToClipboard)
$PreviewForm.Controls.Add($buttonClose)
$PreviewForm.Controls.Add($textboxMemCount)
$PreviewForm.Controls.Add($labelCount)
$PreviewForm.Controls.Add($listboxMember)
$PreviewForm.ClientSize = '284, 328'
$PreviewForm.Name = "PreviewForm"
$PreviewForm.StartPosition = 'CenterParent'
$PreviewForm.Text = "Membership Preview"
$PreviewForm.add_Shown($previewForm_Shown)
# buttonCopyToClipboard
$buttonCopyToClipboard.Enabled = $False
$buttonCopyToClipboard.Location = '197, 236'
$buttonCopyToClipboard.Name = "buttonCopyToClipboard"
$buttonCopyToClipboard.Size = '75, 34'
$buttonCopyToClipboard.TabIndex = 1
$buttonCopyToClipboard.Text = "Copy to clipboard"
$buttonCopyToClipboard.UseVisualStyleBackColor = $False
$buttonCopyToClipboard.add_Click($buttonCopy_Click)
# buttonClose
$buttonClose.Font = "Microsoft Sans Serif, 8.25pt"
$buttonClose.Location = '197, 295'
$buttonClose.Name = "buttonClose"
$buttonClose.Size = '75, 23'
$buttonClose.TabIndex = 0
$buttonClose.Text = "Close"
$buttonClose.UseVisualStyleBackColor = $True
$buttonClose.add_Click($buttonClose_Click)
# textboxMemCount
$textboxMemCount.Location = '62, 236'
$textboxMemCount.Name = "textboxMemCount"
$textboxMemCount.Size = '39, 20'
$textboxMemCount.TabIndex = 2
$textboxMemCount.TabStop = $False
$textboxMemCount.ReadOnly = $true
# labelCount
$labelCount.Font = "Microsoft Sans Serif, 11.25pt"
$labelCount.Location = '10, 236'
$labelCount.Name = "labelCount"
$labelCount.Size = '59, 23'
$labelCount.TabIndex = 1
$labelCount.Text = "Count:"
# listboxMember
$listboxMember.FormattingEnabled = $True
$listboxMember.Location = '12, 18'
$listboxMember.Name = "listboxMember"
$listboxMember.SelectionMode = 'None'
$listboxMember.Size = '260, 212'
$listboxMember.TabIndex = 0
$listboxMember.TabStop = $False
#endregion Form Objects
#Save the initial state of the form
$InitialFormWindowState = $PreviewForm.WindowState
#Init the OnLoad event to correct the initial state of the form
$PreviewForm.add_Load($Form_StateCorrection_Load)
#Clean up the control events
$PreviewForm.add_FormClosed($Form_Cleanup_FormClosed)
#Show the Form
return $PreviewForm.ShowDialog()
}
function Open-ADObjectPicker
{
$objectPicker = New-Object -TypeName CubicOrange.Windows.Forms.ActiveDirectory.DirectoryObjectPickerDialog
#Search anywhere in forest by using GC
$objectPicker.AllowedLocations = [CubicOrange.Windows.Forms.ActiveDirectory.Locations]::GlobalCatalog
$objectPicker.DefaultLocations = [CubicOrange.Windows.Forms.ActiveDirectory.Locations]::GlobalCatalog
#Only allow searching for groups
$objectPicker.AllowedObjectTypes = [CubicOrange.Windows.Forms.ActiveDirectory.ObjectTypes]::Groups
$objectPicker.DefaultObjectTypes = [CubicOrange.Windows.Forms.ActiveDirectory.ObjectTypes]::Groups
$objectPicker.ShowAdvancedView = $false
$objectPicker.MultiSelect = $true
$objectPicker.SkipDomainControllerCheck = $false
$objectPicker.Providers = [CubicOrange.Windows.Forms.ActiveDirectory.ADsPathsProviders]::Default
$objectPicker.AttributesToFetch.Add('distinguishedName')
$objectPicker.ShowDialog()
return $objectPicker.Selectedobjects
}
#EndRegion Forms
#region Core Functions
function Get-AutoDLFilter
{
<#
.Synopsis
Get the membership filter for a distribution group. Cmdlet alias is gadl.
.Parameter DistributionGroup
Identity of the distribution group.
#>
[CmdletBinding()]
param (
[Parameter(Position=0,Mandatory=$true)][Alias('DL')][string]$DistributionGroup
)
$groupDN = Test-DistributionGroup -inputParam $DistributionGroup
$ADGroup = Get-ADObject -dn $groupDN
#Decode LDAP filter from extensionData
try
{
$decodedFilter = ConvertFrom-ExtensionData -data $ADGroup.extensionData[0]
}
catch
{
Write-Error -Message "$($ADGroup.displayName) was found, but it does not have any filter information in the extensionData attribute." -ErrorAction Stop -Category InvalidData
}
$LDAPFilter = Get-LDAPFilterString -string $decodedFilter
if ($LDAPFilter -like "guid:*")
{
$mirrorsGroup = $true
$groupToMirror = Convert-GUIDtoDN -guidString $LDAPFilter.Substring(5)
}
$hardMembership = Get-HardMembershipString -string $decodedFilter
$excludeMember = Get-ExcludeMemberString -string $decodedFilter
$domainRestrictions = Get-DomainRestrictionString -string $decodedFilter
#Build document body for output
$hardMembershipOutput = $hardMembership -replace ';','
'
$excludeMemberOutput = $excludeMember -replace ';','
'
if (-not($hardMembershipOutput))
{
[string]$hardMembershipOutput = 'None'
}
if (-not($excludeMemberOutput))
{
[string]$excludeMemberOutput = 'None'
}
if (-not($domainRestrictions))
{
[string]$domainRestrictions = 'All'
}
if ($mirrorsGroup)
{
$htmlbodyPrefix = 'Mirrors Groups:
' + $groupToMirror
}
else
{
$htmlbodyPrefix = 'Display-formatted Filter:
' + (Format-LDAPDisplay $LDAPFilter) +
'Raw Filter:
' + $LDAPFilter
}
$htmlbody = $htmlbodyPrefix + 'Manual inclusions:
' + $hardMembershipOutput +
'Manual exclusions:
' + $excludeMemberOutput +
'Membership Domains:
'+$domainRestrictions
#Send body to browser output
Show-IEOutput -body $htmlbody -displayName $ADGroup.displayName
}
function Set-AutoDLFilter
{
<#
.Synopsis
Save a membership filter in a distribution group.
.Parameter DistributionGroup
Identity of the distribution group to modify
.Parameter LDAPFilter
LDAP filter to set for group membership
.Parameter MirrorGroup
Distinguished name of one or more groups (comma-separated list or array object) to mirror its memembership
.Parameter AlwaysInclude
Comma-separated list (or array object) of users to always include (Default attribute is SamAccountName)
.Parameter AlwaysExclude
Comma-separated list (or array object) of users to always exclude (Uses the same attribute as AlwaysInclude)
.Parameter MembershipDomains
Comma-separated list (or array) of AD domains, in FQDN format, to restrict membership
(Default membership is forest-wide)
.Parameter AutoProvision
Switch to suppress confirmation to enable standard DL for automatic updating
#>
[CmdletBinding(DefaultParameterSetName='set1',SupportsShouldProcess=$true,ConfirmImpact='Medium')]
param (
[parameter(Position=0,Mandatory=$true,ParameterSetName='set1')]
[parameter(Position=0,Mandatory=$true,ParameterSetName='set2')]
[parameter(Position=0,Mandatory=$true,ParameterSetName='set3')][Alias('DL')][string]$DistributionGroup,
[parameter(Mandatory=$true,ParameterSetName='set2')][ValidateScript({Test-LDAPFilterSyntax -filter $_})][string]$LDAPFilter,
[parameter(Mandatory=$true,ParameterSetName='set3')][ValidateScript({Test-GroupDN -dnarray $_})][string[]]$MirrorGroup,
[parameter(Mandatory=$false,ParameterSetName='set2')]
[parameter(Mandatory=$false,ParameterSetName='set3')][string[]]$AlwaysInclude,
[parameter(Mandatory=$false,ParameterSetName='set2')]
[parameter(Mandatory=$false,ParameterSetName='set3')][string[]]$AlwaysExclude,
[parameter(Mandatory=$false,ParameterSetName='set2')]
[parameter(Mandatory=$false,ParameterSetName='set3')][ValidateScript({Test-Domain -string $_})][string[]]$MembershipDomains,
[parameter(Mandatory=$false)][switch]$AutoProvision
)
$groupDN = Test-DistributionGroup -inputParam $DistributionGroup
$ADGroup = Get-ADObject -dn $groupDN
#Check if DL is configured for automation
if (-not($ADGroup.$AutoDLAttribute.IndexOf($AutoDLString) -ge 0))
{
if ($AutoProvision -eq $false)
{
Write-Warning -Message ($ADGroup.displayName[0] + ' was found, but is not enabled for automatic updating.')
$answer = Read-Host -Prompt 'Do you want to enable the DL for automation and set a filter? (y/n)'
if (-not($answer -like 'y*'))
{
Write-Error -Message 'Operation canceled.' -ErrorAction Stop -Category OperationStopped
}
}
if ($AutoProvision -or $answer -like 'y*')
{
#Enable DL for automatic updates
Invoke-Command -ScriptBlock ([ScriptBlock]::Create("Set-DistributionGroup `"$groupDN`" -$exAutoDLAttribute $AutoDLString"))
$currentNote = (Get-Group -Identity $groupDN).Notes
if ($currentNote.Length -gt 0)
{$newNotePrefix = "$currentNote`r`n`r`n"}
else
{$newNotePrefix = ''}
$newNote = $newNotePrefix + $noteText
Set-Group -Identity $groupDN -Notes $newNote
$newlyProvisioned = $true
}
}
if ($PSCmdlet.ParameterSetName -eq 'set1') #Display form
{
if (-not($newlyProvisioned))
{
#Get existing filter to populate in form
try
{
$decodedFilter = ConvertFrom-ExtensionData -data $ADGroup.extensionData[0]
Write-Verbose -Message "Decoded extension data: $decodedFilter"
$currentLDAPFilter = Get-LDAPFilterString -string $decodedFilter
if ($currentLDAPFilter -like "guid:*")
{
$mirrorsGroup = $true
$groupToMirror = Convert-GUIDtoDN -guidString $currentLDAPFilter.Substring(5)
}
$currentHardMembership = (Get-HardMembershipString -string $decodedFilter) -split ';' | Sort-Object
$currentExcludeMember = (Get-ExcludeMemberString -string $decodedFilter) -split ';' | Sort-Object
$currentDomainRestrict = (Get-DomainRestrictionString -string $decodedFilter) -split ';'
}
catch
{
Write-Error -Message 'An error occurred parsing the group''s filter.' -Category ParserError
}
}
if ($decodedFilter)
{
$hasExistingFilter = $true
}
#Load and open the form
$formResult = Open-AutoDLForm
#Perform cleanup
OnApplicationExit
}
else #Command-line filter specified
{
$formLDAPFilterText = $LDAPFilter
if ($MirrorGroup)
{
$formMirrorGroupsText = $MirrorGroup
$formRadioButtonMirrorChecked = $true
}
if ($AlwaysInclude)
{
$formCheckBoxHardCode = $true
$formHardMember = $AlwaysInclude
}
if ($AlwaysExclude)
{
$formCheckBoxExcludeMembers = $true
$formExcludeMember = $AlwaysExclude
}
if ($MembershipDomains)
{
$formCheckBoxDomainRestrict = $true
$formDomainRestrict = $MembershipDomains
}
}
if ($formResult -eq 'OK' -or $PSCmdlet.ParameterSetName -ne 'set1')
{
if ($formRadioButtonMirrorChecked)
{
#Convert DNs of mirrored groups to GUIDs
$guidArray = @()
foreach ($line in $formMirrorGroupsText)
{
$dirObject = Get-ADObject -dn $line
$guidArray += (New-Object -Typename System.Guid -ArgumentList (,$dirObject.Properties.ObjectGUID[0])).Guid
}
$filter = 'guid:' + ($guidArray -join ';')
}
else
{
$filter = $formLDAPFilterText
}
if ($formCheckBoxHardCode) #Hard include checked
{
$formHardMember = ($formHardMember | Sort-Object) -join ';'
}
else
{
$formHardMember = $null
}
if ($formCheckBoxExcludeMembers) #Hard exclusions checked
{
$formExcludeMember = ($formExcludeMember | Sort-Object) -join ';'
}
else
{
$formExcludeMember = $null
}
if ($formCheckBoxDomainRestrict) #Membership restricted to domains checked
{
$memberDomains = $formDomainRestrict -join ';'
}
else
{
$memberDomains = $null
}
$rawFilter = '' + $filter + '' + $formHardMember + '' +
$formExcludeMember + '' + $memberDomains + ''
Write-Verbose -Message ('Filter to be encoded and stored: ' + $rawFilter)
#Encode string to byte array
$encodedFilter = [System.Text.UTF8Encoding]::UTF8.GetBytes($rawFilter)
try
{
if ($pscmdlet.ShouldProcess($ADGroup.DisplayName))
{
$ADGroup.Put('extensionData',$encodedFilter)
$ADGroup.SetInfo()
}
}
catch
{
Write-Error -Message 'Error saving filter to group object.'
}
}
else
{
Write-Warning -Message 'Action canceled - Filter not updated.'
}
}
function Update-AutoDL
{
<#
.Synopsis
Update the membership of one or all automated DLs
.Parameter DistributionGroup
Identity of the distribution group to update (Default is all DLs in the forest)
.Parameter UpdateDomain
FQDN of an AD domain with DLs to update (Default is all domains in the forest)
.Parameter DoNotLog
Switch to indicate that a log file should not be created
.Parameter VerboseMembershipChanges
Switch to include in output which users were added to and removed from a DL
#>
[CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='High',DefaultParameterSetName='set1')]
param (
[Parameter(Position=0,ParameterSetName='set1')][Alias("DL")][ValidateScript({Test-DistributionGroup $_})][string]$DistributionGroup,
[Parameter(ParameterSetName='set2')][ValidateScript({Test-Domain $_})][string]$UpdateDomain,
[switch]$DoNotLog,
[switch]$VerboseMembershipChanges
)
$currentPath = (Get-Location).Path
$output = New-Object -TypeName System.Collections.ArrayList
$script:notifyAdmin = $false
$script:notifyGroups = ''
[string]$executionTime = Get-Date -f "yyyyMMdd-HHmm"
if ($VerboseMembershipChanges)
{
$verboseLogging = $true
}
#Get all DLs enabled for automated membership
if ($UpdateDomain)
{
$autoDLs = Get-AutomatedGroup -domain $UpdateDomain
}
else
{
$autoDLs = Get-AutomatedGroup
}
Write-Verbose -Message ('Number of automated groups: ' + $autoDLs.Count)
$shortnames = @{}
for ($i = 0;$i -lt $autoDLs.Count;$i++)
{
$shortnames.Add([string]$autoDLs[$i].Properties.mailnickname,[string]$autoDLs[$i].Properties.distinguishedname)
}
#Check for single group update versus all
if ($DistributionGroup)
{
$singleUpdate = $DistributionGroup
if ($shortnames.ContainsKey($singleUpdate))
{
Update-Group -dn $shortnames.Item($singleUpdate)
}
else
{
Write-Warning -Message "$singleUpdate is valid, but is not an automated DL ($exAutoDLAttribute is not set to `'$AutoDLString`')."
}
}
else
{
if ($pscmdlet.ShouldProcess('All automated DLs'))
{
foreach ($DL in $autoDLs)
{
Update-Group -dn $DL.Properties.distinguishedname
}
}
}
if ($DoNotLog -eq $false)
{
$output | Select-Object -Property Name,OldCount,NewCount,AddedCount,RemovedCount,@{n='AddedMembers';e={$_.AddedMembers -join ';'}},@{n='RemovedMembers';e={$_.RemovedMembers -join ';'}},Error | Export-Csv -Path "$currentPath\AutoDLResults-$executionTime.csv" -NoTypeInformation
}
$script:output = $null
#Email notification if error occurred
if ($notifyAdmin)
{
$mailBody = "Either an error occurred or a DL has 0 members that previously did not. The following DLs had an issue:$notifyGroups
"
Send-MailMessage -Body $mailBody -BodyAsHtml -From $mailSender -Priority High `
-SmtpServer $mailServer -Subject $mailSubject -To $mailRecipients
}
}
#endregion Core Functions
#region Load AD Object Picker DLL
#From https://gallery.technet.microsoft.com/scriptcenter/Active-Directory-Object-a832f7bd
try
{
Add-Type -Path "$PSScriptRoot\CubicOrange.Windows.Forms.ActiveDirectory.dll" -ErrorAction Stop
$objectPickerLoaded = $true
}
catch
{
$objectPickerLoaded = $false
}
#endregion
Set-Alias -Name gadl -Value Get-AutoDLFilter
Set-Alias -Name sadl -Value Set-AutoDLFilter
Export-ModuleMember -Function Get-AutoDLFilter,Set-AutoDLFilter,Update-AutoDL
Export-ModuleMember -Alias *