AddGPLink.psm1
<#
.SYNOPSIS Links a new GPO to all OUs where a given GPO is already linked. Optionally removes the given GPO. Does not process GPOs linked to sites or the domain itself. .DESCRIPTION Sometimes, new GPOs need to be deployed everywhere a given GPO is already in use. Or a given GPO needs to be replaced globally after testing. The Append-GPLink function takes a reference GPO and a new GPO (both must already exist). Then it enumerates the OUs where the reference GPO is linked. It then links the new GPO to these OUs (bottom most link order by default). Link order and link properties can be modified. DYNAMIC PARAMETERS -ReferenceGPO <String> The GPO that serves as a reference. The OUs this GPO is linked to are enumerated and updated. Tab completion searches the list of GPOs in TargetDomain. Required? true Position? 2 Default value False Accept pipeline input? false Accept wildcard characters? false -NewGPO <String> The GPO that will be linked to the OUs where ReferenceGPO is linked. This parameter is required if -RemoveLink is not specified. Tab completion searches the list of GPOs in TargetDomain. Required? true Position? 3 Default value False Accept pipeline input? false Accept wildcard characters? false .PARAMETER TargetDomain The domain where the actions should be performed. Defaults to the domain of the currently logged on user. .PARAMETER OUFilter By default, all OUs are processed where the ReferenceGPO is linked. Use this parameter to restrict the OUs to process. The filter is evaluated as a regular expression match against the distinguished name of the OUs where the ReferenceGPO is linked. Filtering would be smarter if done via LDAPFilter, but there's no possibility to escape LDAP filters like it can be done with [Regex]::Escape for regular expressions. .PARAMETER SearchBase Use this distinguished name to limit the search for OUs where ReferenceGPO is linked to a specific searchbase. Since the domain is already defined, omit the domain from the searchbase (do not include the DC=... parts) .PARAMETER RegexEscape By default, the OUFilter will be used literally in a regex match. This means if you want to search for special characters like \ or *, you must escape them properly. Use this switch to let the cmdlet escape your filter string. .PARAMETER RelativeLinkPos By default, NewGPO will be appended at the bottom of the linked GPOs. With RelativeLinkPos, you can specify whether NewGPO should be inserted directly above or below ReferenceGPO. Valid options are "before" and "after". .PARAMETER ReplaceLink Specify this switch if you want to remove the link to ReferenceGPO, leaving only the NewGPO link active. NewGPO will be linked at the position where ReferenceGPO was linked. .PARAMETER LinkOrder By default, NewGPO is linked at the last position (bottom) or near ReferenceGPO. Specify a different LinkOrder to link it e.g. at the top (Linkorder 1) or anywhere in between. .PARAMETER Enforced Specify this parameter to select an enforcement state for the GPO link. The default is "unspecified" which effectively means "not enforced". Valid options are "unspecified" (0), "no" (1) and "yes" (2). .PARAMETER LinkEnabled Specify this parameter to select an enablement state for the GPO link. The default is "unspecified" which effectively means "enabled". Valid options are "unspecified" (0), "no" (1) and "yes" (2). .PARAMETER RemoveLink Specify this switch to only remove the link to ReferenceGPO. .INPUTS This cmdlet does not take pipeline input. .OUTPUTS This cmdlet does not return pipeline output. .EXAMPLE Append-GPLink -ReferenceGPO 'Server Default Policy' -NewGPO 'Server Addon Policy' Searches all OUs where the GPO named 'Server Default Policy' is linked, and links the GPO named 'Server Addon Policy' to these OUs. The link will be disabled and enforced. .EXAMPLE Append-GPLink -ReferenceGPO 'Server Default Policy' -NewGPO 'Server New Default Policy' -OUFilter 'OU=Servers' -Replace -LinkOrder 1 Searches all OUs matching 'OU=Servers' where the GPO named 'Server Default Policy' is linked, and links the GPO named 'Server New Default Policy' to these OUs at position 1 . Then it removes the existing link to 'Server Default Policy'. .EXAMPLE Append-GPLink -ReferenceGPO 'Server Default Policy' -Remove Searches all OUs where the GPO named 'Server Default Policy' is linked, and removes the existing link. .NOTES Because such mass operations are usually not required for sites or for the domain itself, it does not process these SOM types. Only OUs are searched. #> Function Add-GPLink { [CmdletBinding( SupportsShouldProcess = $True, DefaultParameterSetName = 'Add' )] [Alias()] Param( [Parameter( Position = 0 )] [ValidateScript( { Get-ADDomain $_ } )] # verify TargetDomain is reachable [String] $TargetDomain = $env:USERDNSDOMAIN, [Parameter()] [ValidateScript( { $_ -match '^(?:OU=[^,]+,?)+$' } )] # match any number of OU=xxx,OU=yyy... [String] $SearchBase, [Parameter()] [String] $OUFilter, [Parameter()] [Switch] $RegexEscape, [Parameter( ParameterSetName = 'Add' )] [ValidateSet('before','after')] [String] $RelativeLinkPos, [Parameter( ParameterSetName = 'Replace' )] [Switch] $ReplaceLink, [Parameter( ParameterSetName = 'AddWithOrder' )] [ValidateRange( 1, 999 )] # maximum number of linked GPOs is 1000 due to size limitation of the GPLink attribute... [Int32] $LinkOrder, [Parameter( ParameterSetName = 'Add' )] [Parameter( ParameterSetName = 'Replace' )] [Parameter( ParameterSetName = 'AddWithOrder' )] [Microsoft.GroupPolicy.EnforceLink] $Enforced = [Microsoft.GroupPolicy.EnforceLink]::Unspecified, [Parameter( ParameterSetName = 'Add' )] [Parameter( ParameterSetName = 'Replace' )] [Parameter( ParameterSetName = 'AddWithOrder' )] [Microsoft.GroupPolicy.EnableLink] $LinkEnabled = [Microsoft.GroupPolicy.EnableLink]::Unspecified, [Parameter( ParameterSetName = 'Remove' )] [Switch] $RemoveLink ) DynamicParam { # To enable tab expansion for ReferenceGPO and NewGPO, create a hash of all GPO names in TargetDomain and add this # as a ValidateSet to both parameters. # GPOHash contains the GPO names for the ValidateSet. # GuidHash is used to resolve linked GPOs from the GPLink attribute. Usually one would search these with a # Where clause in the GPOHash. But in domains wit a large number of GPOs, that's way too slow. The GuidHash # allows direct access to all GPOs by Guid. $GPOHash = @{} $GuidHash = @{} $GPOs = Get-GPO -All -Domain $TargetDomain | Sort-Object -Property ModificationTime Foreach ( $GPO in $GPOs ) { $GPOHash[ $GPO.DisplayName ] = $GPO $GuidHash[ $GPO.Id.Guid ] = $GPO } # Create the DynamicParam Array. Each array member is a hashtable containing the parmeter definition. # ParameterAttributes is an embedded Array of hashtables containing the attributes for each ParameterSet. # The comments for the ReferenceGPO parameter definition show some commonly used attributes. # Parameter references: # https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters # https://docs.microsoft.com/en-us/powershell/scripting/developer/cmdlet/validating-parameter-input # https://docs.microsoft.com/en-us/powershell/scripting/developer/cmdlet/parameter-attribute-declaration $DynamicParameters = @( @{ Name = 'ReferenceGPO' # ValidateCount = @( [int]Min, [int]Max ) # ValidateLenght = @( [int]Min, [int]Max ) # ValidateRange = @( [int]Min, [int]Max ) # ValidateSet = @( 'a', 'b', 'c' ) ValidateSet = $GPOHash.Keys ParameterAttributes = @( @{ # ParameterSetName = 'a' # Mandatory = $True # ValueFromPipeline = $True # ValueFromPipelineByPropertyName = $True Mandatory = $True } ) }, @{ Name = 'NewGPO' ValidateSet = $GPOHash.Keys ParameterAttributes = @( @{ ParameterSetName = 'Add' Mandatory = $True }, @{ ParameterSetName = 'Replace' Mandatory = $True }, @{ ParameterSetName = 'AddWithOrder' Mandatory = $True } ) } ) # Create and populate the parameter dictionary $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary Foreach( $DynamicParameter in $DynamicParameters ) { $RuntimeParameter = New-DynamicParameter @DynamicParameter $RuntimeParameterDictionary.Add( $DynamicParameter.Name, $RuntimeParameter ) } Return $RuntimeParameterDictionary } Begin { Foreach ( $BoundParam in $PSBoundParameters.GetEnumerator() ) { New-Variable -Name $BoundParam.Key -Value $BoundParam.Value -ErrorAction 'SilentlyContinue' -Whatif:$False } $SourceGPO = $GPOHash[ $ReferenceGPO ] If ( $NewGPO ) { $TargetGPO = $GPOHash[ $NewGPO ] } $Domain = Get-ADDomain -Identity $TargetDomain $DomainDN = $Domain.DistinguishedName $DomainDNS = $Domain.DNSRoot $PDC = $Domain.PDCEmulator # default parameters for GP Cmdlets $GPParms = @{ Server = $PDC Domain = $DomainDNS } } Process { Write-Progress -Activity 'Enumerating organizational units.' -Id 0 -PercentComplete 0 $LDAPSearchBase = $DomainDN If ( $SearchBase ) { $LDAPSearchBase = "$SearchBase,$DomainDN" } If ( $RegexEscape ) { $OUFilter = [Regex]::Escape( $OUFilter ) } $LDAPParms = @{ LDAPFilter = "(GPLink=*$($SourceGPO.Id.Guid)*)" Properties = 'GPLink' SearchBase = $LDAPSearchBase SearchScope = 'SubTree' Server = $PDC } $OrganizationalUnits = Get-ADOrganizationalUnit @LDAPParms | Where-Object -FilterScript { $_.DistinguishedName -match $OUFilter } | Select-Object -Property * $Counter = 0 Foreach ( $OU in $OrganizationalUnits ) { $Counter += 1 $ActivityParms = @{ Activity = 'Processing organizational units ({0}/{1})' -f $Counter, $OrganizationalUnits.Count Status = $OU.DistinguishedName PercentComplete = $Counter * 100 / $OrganizationalUnits.Count } Write-Progress @ActivityParms -Id 0 If ( -not $RemoveLink ) { # First, get the current GPO links as a hashtable with the GPO id as key and the link properties as a custom object $GPLinks = Resolve-GPLinksFromHashtable -OU $OU -GuidHash $GuidHash $UpdateLink = $False # Need current LinkOrder of both GPOs if we want to link before/after or replace. $SourceGPOLink = $GPLinks[ $SourceGPO.Id.Guid ] $SourceGPOLinkOrder = $SourceGPOLink.Order $TargetGPOLink = $GPLinks[ $TargetGPO.Id.Guid ] # might be empty if not already linked $TargetGPOLinkOrder = $TargetGPOLink.Order If ( $ReplaceLink -Or $RelativeLinkPos ) { # link order of new gpo defaults to same as old gpo (will be inserted above) $TargetGPONewLinkOrder = $SourceGPOLinkOrder # Need to fix LinkOrder if NewGPO is already linked above ReferenceGPO. Removing moves ReferenceGPO one position to top... If ( $TargetGPOLinkOrder -and $TargetGPOLinkOrder -lt $SourceGPOLinkOrder ) { $TargetGPONewLinkOrder-- } # If NewGPO should be linked below ReferenceGPO, add 1 position. If ( $RelativeLinkPos -match 'after' ) { $TargetGPONewLinkOrder++ } } ElseIf ( $LinkOrder ) { # Static link order, make sure $LinkOrder does not exceed the number of currently linked GPOs... $TargetGPONewLinkOrder = [Math]::Min( $LinkOrder, $GPLinks.Count + 1 ) } Else { If ( $TargetGPOLinkOrder ) { # Already linked, keep current link order $TargetGPONewLinkOrder = $TargetGPOLinkOrder } Else { # Not already linked and no order specified through parameters? Then append at the bottom. $TargetGPONewLinkOrder = $GPLinks.Count + 1 } } # verify if the existing link must be updated If ( ( $TargetGPOLinkOrder -ne $TargetGPONewLinkOrder ) -or ( $LinkEnabled -ne [Microsoft.GroupPolicy.EnableLink]::Unspecified -and $TargetGPOLink.Enabled -ne $LinkEnabled ) -or ( $Enforced -ne [Microsoft.GroupPolicy.EnforceLink]::Unspecified -and $TargetGPOLink.Enforced -ne $Enforced ) ) { $UpdateLink = $True } $LinkParms = @{ Guid = $TargetGPO.Id Target = $OU.DistinguishedName Order = $TargetGPONewLinkOrder LinkEnabled = $LinkEnabled Enforced = $Enforced ErrorAction = 'Stop' } If ( $TargetGPOLinkOrder ) { # NewGPO is already linked, so simply update Link if required If ( $UpdateLink ) { Set-GPLink @LinkParms @GPParms } } Else { # NewGPO is currently not linked - create new link New-GPLink @LinkParms @GPParms } } If ( $ReplaceLink -Or $RemoveLink ) { Remove-GPLink -Guid $SourceGPO.Id -Target $OU.DistinguishedName @GPParms -ErrorAction 'Stop' } } } End { Write-Progress -Activity 'Processing organizational units' -Id 0 -PercentComplete 100 -Completed } } Function New-DynamicParameter { # based on the work of adamtheautomator # https://github.com/adbertram/Random-PowerShell-Work/blob/master/PowerShell%20Internals/New-DynamicParam.ps1 [CmdletBinding()] [OutputType('System.Management.Automation.RuntimeDefinedParameter')] param ( [Parameter(Mandatory)][ValidateNotNullOrEmpty()] [String] $Name, [Parameter()][ValidateNotNullOrEmpty()] [Type] $Type = [String], [Parameter()][ValidateNotNullOrEmpty()][ValidateCount( 2, 2 )] [Int[]] $ValidateCount, [Parameter()][ValidateNotNullOrEmpty()][ValidateCount( 2, 2 )] [Int[]] $ValidateLength, [Parameter()][ValidateNotNullOrEmpty()] [String] $ValidatePattern, [Parameter()][ValidateNotNullOrEmpty()][ValidateCount( 2, 2 )] [Int[]] $ValidateRange, [Parameter()][ValidateNotNullOrEmpty()] [Scriptblock] $ValidateScript, [Parameter()][ValidateNotNullOrEmpty()] [Array] $ValidateSet, [Parameter()][ValidateNotNullOrEmpty()] [Switch] $ValidateNotNullOrEmpty, [Parameter()][ValidateNotNullOrEmpty()] [Array] $ParameterAttributes ) $AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute] Foreach ( $ParameterAttribute in $ParameterAttributes ) { $ParamAttrib = New-Object System.Management.Automation.ParameterAttribute # Get all settable properties of the $ParamAttrib object $AttribNames = ( Get-Member -InputObject $ParamAttrib -MemberType Property | Where-Object -FilterScript { $_.Definition -match '{.*set;.*}$' } ).Name # Loop through settable properties and assign value if present in $ParameterAttribute Foreach ( $AttribName in $AttribNames ){ If ( $ParameterAttribute.$AttribName ) { $ParamAttrib.$AttribName = $ParameterAttribute.$AttribName } } $AttribColl.Add( $ParamAttrib ) } $ValidationAttributes = @( 'Count', 'Length', 'Pattern', 'Range', 'Script', 'Set' ) # create all validation attributes Foreach ( $ValidationAttribute in $ValidationAttributes ){ If ( $PSBoundParameters.ContainsKey( "Validate$ValidationAttribute" )) { $TypeName = 'System.Management.Automation.Validate' + $ValidationAttribute + 'Attribute' $AttribColl.Add(( New-Object $TypeName -ArgumentList ( Get-Variable -Name "Validate$ValidationAttribute" ).Value )) } } # need to handle this one separately - it does not take parameters in its constructor If ( $ValidateNotNullOrEmpty.IsPresent ) { $AttribColl.Add(( New-Object System.Management.Automation.ValidateNotNullOrEmptyAttribute )) } $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter( $Name, $Type, $AttribColl ) Return $RuntimeParam } Function Resolve-GPLinksFromHashtable{ # based on the work of Thomas Bouchereau # https://gallery.technet.microsoft.com/scriptcenter/Get-GPlink-Function-V13-b31253b4 [CmdletBinding()] Param( [Parameter(Mandatory)][Microsoft.ActiveDirectory.Management.ADOrganizationalUnit] $OU, [Parameter(Mandatory)][System.Collections.HashTable] $GuidHash ) $o = 0 # GPLink has a weird format - [GPO-DN;LinkFlags][GPO-DN;LinkFlags][...] # remove leading [ and trailing ], then split on ][ $GPLinks = $OU.GPLink.Substring( 1, $OU.GPLink.Length - 2 ) -split '\]\[' $Target = $OU.DistinguishedName $Return = @{} # we need to do reverse to get the proper link order - last GPO in GPLink is link order 1 for ( $s = $GPLinks.Count - 1; $s -ge 0; $s-- ) { $o++ $Order = $o $null = $GPLinks[$s] -match '{(?<GpoGuid>.*)}.*;(?<Flags>\d)$' $GpoGuid = $Matches.GpoGuid $Flags = $Matches.Flags # Retrieve current GPO from GuidHash - much faster than using Where... $MyGpo = $GuidHash[ $GPOGuid ] If ( $MyGpo ) { $GpoName = $MyGPO.DisplayName $GpoDomain = $MyGPO.DomainName } Else { $GpoName = 'Orphaned GPLink' $GpoDomain = '<undefined>' } # The GroupPolicy Link enums have the following values: # 0 - unspecified (impossible for existing links) # 1 - No (link is disabled or not enforced ) # 2 - Yes (link is enabled or enforced) [Microsoft.GroupPolicy.EnableLink]$Enabled = ( ( $Flags -band 1 ) -eq 0 ) + 1 # $Flags bit 0 unset means "Link enabled" [Microsoft.GroupPolicy.EnforceLink]$Enforced = ( ( $Flags -band 2 ) -eq 2 ) + 1 # $Flags bit 1 set means "Link enforced" # Create an object for each GPOs, its link status and order $Return[ $GpoGuid ] = [PSCustomObject]@{ GPOID = $GpoGuid DisplayName = $GpoName Domain = $GpoDomain Target = $Target Enabled = $Enabled Enforced = $Enforced Order = $Order } } Return $Return } |