#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 *