Private/Set-AclConstructor5.ps1
function Set-AclConstructor5 { <# .SYNOPSIS Modifies ACLs on Active Directory objects using a 5-parameter constructor with inheritance control. .DESCRIPTION This function adds or removes access control entries (ACEs) on Active Directory objects using the ActiveDirectoryAccessRule constructor with 5 parameters: - Identity Reference - Active Directory Rights - Access Control Type - Object Type GUID - Active Directory Security Inheritance The function provides granular control over permissions by allowing you to specify precise object types (schema GUIDs) and inheritance settings. It is optimized for large AD environments and supports efficient batch processing through splatting. This constructor is particularly useful when you need to apply permissions with specific inheritance settings, controlling exactly how permissions flow down through the AD hierarchy. .PARAMETER Id Specifies the security principal (user, group, computer) that will receive the permission. This parameter accepts: - String: SamAccountName of the delegated group or user - AD object: Variable containing an AD user or group object - SID: Security Identifier object or string .PARAMETER LDAPPath Specifies the LDAP path (Distinguished Name) of the target Active Directory object on which the permissions will be set. This must be a valid LDAP path in the domain. .PARAMETER AdRight Specifies the Active Directory rights to assign. This parameter accepts multiple values separated by commas. Valid values include: - CreateChild - DeleteChild - ListChildren - Self - ReadProperty - WriteProperty - DeleteTree - ListObject - ExtendedRight - Delete - ReadControl - GenericExecute - GenericWrite - GenericRead - WriteDacl - WriteOwner - GenericAll - Synchronize - AccessSystemSecurity .PARAMETER AccessControlType Specifies whether to Allow or Deny the permission. Valid values are: - Allow - Deny .PARAMETER ObjectType Specifies the object type GUID that defines the type of object the permission applies to. This can be: - Property set GUID - Extended right GUID - Object class GUID Object type GUIDs determine the specific attributes or operations the permission applies to. .PARAMETER AdSecurityInheritance Specifies how the permission is inherited by child objects. Valid values are: - All: The permission applies to this object and all child objects - Children: The permission applies only to child objects - Descendents: The permission applies to all objects within the subtree except this object - None: The permission is not inherited - SelfAndChildren: The permission applies to this object and immediate children only .PARAMETER RemoveRule If specified, the access rule will be removed instead of added. By default, the function adds the specified permission. .EXAMPLE Set-AclConstructor5 -Id "SG_SiteAdmins_XXXX" ` -LDAPPath "OU=Users,OU=XXXX,OU=Sites,DC=EguibarIT,DC=local" ` -AdRight "CreateChild,DeleteChild" ` -AccessControlType "Allow" ` -ObjectType "bf967aba-0de6-11d0-a285-00aa003049e2" ` -AdSecurityInheritance "All" Grants the SG_SiteAdmins_XXXX group permission to create and delete user objects in the specified OU and all its child OUs. .EXAMPLE $Splat = @{ Id = "SG_SiteAdmins_XXXX" LDAPPath = "OU=Users,OU=XXXX,OU=Sites,DC=EguibarIT,DC=local" AdRight = "CreateChild,DeleteChild" AccessControlType = "Allow" ObjectType = "bf967aba-0de6-11d0-a285-00aa003049e2" AdSecurityInheritance = "All" } Set-AclConstructor5 @Splat Uses splatting to grant the same permissions as the previous example. .EXAMPLE $group = Get-AdGroup "SG_SiteAdmins_XXXX" $Splat = @{ Id = $group LDAPPath = "OU=Users,OU=XXXX,OU=Sites,DC=EguibarIT,DC=local" AdRight = "ReadProperty,WriteProperty" AccessControlType = "Allow" ObjectType = "4c164200-20c0-11d0-a768-00aa006e0529" AdSecurityInheritance = "Descendents" } Set-AclConstructor5 @Splat Uses an AD group object to grant read/write permissions to the User Account Restrictions property set for all descendant objects (but not the OU itself). .INPUTS [System.String] [Microsoft.ActiveDirectory.Management.ADGroup] .EXAMPLE $Splat = @{ Id = "SG_SiteAdmins_XXXX" LDAPPath = "OU=Users,OU=XXXX,OU=Sites,DC=EguibarIT,DC=local" AdRight = "GenericAll" AccessControlType = "Allow" ObjectType = $null AdSecurityInheritance = "All" RemoveRule = $true } Set-AclConstructor5 @Splat Removes the previously granted GenericAll (Full Control) permission from the SG_SiteAdmins_XXXX group on the specified OU and its child objects. .INPUTS System.String Microsoft.ActiveDirectory.Management.ADGroup Microsoft.ActiveDirectory.Management.ADUser You can pipe identity values and LDAP paths to this function. .OUTPUTS System.Void This function does not generate any output. It modifies ACLs directly on Active Directory objects. .NOTES Used Functions: Name ║ Module/Namespace ═══════════════════════════════════════════╬══════════════════════════════ Get-ADObject ║ ActiveDirectory Get-Acl ║ Microsoft.PowerShell.Security Set-Acl ║ Microsoft.PowerShell.Security Test-IsValidDN ║ EguibarIT.DelegationPS Get-AdObjectType ║ EguibarIT.DelegationPS Get-FunctionDisplay ║ EguibarIT.DelegationPS Write-Verbose ║ Microsoft.PowerShell.Utility Write-Error ║ Microsoft.PowerShell.Utility Write-Debug ║ Microsoft.PowerShell.Utility .NOTES Version: 4.0 DateModified: 22/May/2025 LastModifiedBy: Vicente Rodriguez Eguibar vicente@eguibar.com Eguibar IT http://www.eguibarit.com .LINK https://github.com/vreguibar/EguibarIT.DelegationPS .LINK https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.activedirectoryaccessrule.-ctor?view=windowsdesktop-9.0#system-directoryservices-activedirectoryaccessrule-ctor(system-security-principal-identityreference-system-directoryservices-activedirectoryrights-system-security-accesscontrol-accesscontroltype-system-guid-system-directoryservices-activedirectorysecurityinheritance) .COMPONENT Active Directory .ROLE Security Administration .FUNCTIONALITY Access Control Management #> [CmdletBinding( SupportsShouldProcess = $true, ConfirmImpact = 'Low', DefaultParameterSetName = 'Default', PositionalBinding = $true )] [OutputType([void])] param ( # PARAM1 STRING for the Delegated Identity # An IdentityReference object that identifies the trustee of the access rule. [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = 'Identity of the Delegated Group', Position = 0)] [ValidateNotNullOrEmpty()] [Alias('IdentityReference', 'Identity', 'Trustee', 'GroupID', 'Group')] $Id, # PARAM2 STRING for the object's LDAP path # The LDAP path to the object where the ACL will be changed [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = 'Distinguished Name of the object', Position = 1)] [ValidateNotNullOrEmpty()] [ValidateScript( { Test-IsValidDN -ObjectDN $_ }, ErrorMessage = 'DistinguishedName provided is not valid! Please Check.' )] [Alias('DN', 'DistinguishedName')] [String] $LDAPpath, # PARAM3 STRING representing AdRight [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = 'Active Directory Right', Position = 2)] [ValidateNotNullOrEmpty()] [ValidateSet([ActiveDirectoryRights])] [Alias('ActiveDirectoryRights')] [String[]] $AdRight, # PARAM4 STRING representing Access Control Type [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = 'Allow or Deny access to the given object', Position = 3)] #[ValidateSet('Allow', 'Deny')] [ValidateSet([AccessControlType])] [String] $AccessControlType, # PARAM5 STRING representing Object GUID [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = 'GUID of the object', Position = 4)] [AllowNull()] $ObjectType, # PARAM6 STRING representing ActiveDirectory Security Inheritance [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = 'Security inheritance of the new right (All, Children, Descendents, None, SelfAndChildren)', Position = 5)] [ValidateSet( [ActiveDirectorySecurityInheritance], ErrorMessage = "Value '{0}' is invalid. Try one of: {1}" )] [Alias('InheritanceType', 'ActiveDirectorySecurityInheritance')] [String] $AdSecurityInheritance, # PARAM7 SWITCH if $false (default) will add the rule. If $true, it will remove the rule [Parameter(Mandatory = $False, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = 'If present, the access rule will be removed.', Position = 6)] [Switch] $RemoveRule ) Begin { Set-StrictMode -Version Latest # Display function header if variables exist if ($null -ne $Variables -and $null -ne $Variables.HeaderDelegation) { $txt = ($Variables.HeaderDelegation -f (Get-Date).ToString('dd/MMM/yyyy'), $MyInvocation.Mycommand, (Get-FunctionDisplay -HashTable $PsBoundParameters -Verbose:$False) ) Write-Verbose -Message $txt } #end if ############################## # Module imports ############################## # Variables Definition [System.Security.Principal.SecurityIdentifier]$GroupSid = $null [System.DirectoryServices.ActiveDirectoryAccessRule]$AccessRule = $null [String]$ObjectPath = $null [Bool]$IsWellKnownSid = $false [HashTable]$AdObjectCache = @{} [int]$RulesRemovedCount = 0 # Convert ObjectType to GUID if it's a string if ($null -ne $PSBoundParameters['ObjectType']) { if ($PSBoundParameters['ObjectType'] -is [System.String]) { try { $ObjectTypeGuid = [Guid]::Parse($PSBoundParameters['ObjectType']) Write-Debug -Message ( 'Successfully parsed ObjectType string to GUID: {0}' -f $ObjectTypeGuid ) } catch { Write-Error -Message ( 'Failed to parse ObjectType as GUID: {0}' -f $PSBoundParameters['ObjectType'] ) throw } #end try-catch } elseif ($PSBoundParameters['ObjectType'] -is [Guid]) { $ObjectTypeGuid = $PSBoundParameters['ObjectType'] Write-Debug -Message ('Using provided ObjectType GUID: {0}' -f $ObjectTypeGuid) } #end if-elseif } #end if } #end Begin Process { try { ############################# # Identify and resolve the trustee ############################# # Check if Identity is a Well-Known SID if ($null -ne $Variables -and $null -ne $Variables.WellKnownSIDs -and $Variables.WellKnownSIDs.Values -contains $Id) { # Find and create SID for well-known identity $TmpSid = ($Variables.WellKnownSIDs.GetEnumerator() | Where-Object { $_.Value -eq $Id }).Name if ($null -ne $TmpSid) { $GroupSid = [System.Security.Principal.SecurityIdentifier]::new($TmpSid) $IsWellKnownSid = $true Write-Debug -Message ('Identity {0} is a Well-Known SID. Retrieved SID: {1}' -f $Id, $GroupSid.Value) } else { Write-Error -Message ('Well-known identity {0} found but unable to resolve SID' -f $Id) return } #end if-else } else { # Get object information for the identity try { $GroupObject = Get-AdObjectType -Identity $Id if ($null -ne $GroupObject -and $null -ne $GroupObject.SID) { $GroupSid = [System.Security.Principal.SecurityIdentifier]::new($GroupObject.SID) Write-Debug -Message ('Resolved identity {0} to SID: {1}' -f $Id, $GroupSid.Value) } else { Write-Error -Message ('Failed to resolve identity {0} to a valid security principal' -f $Id) return } #end if-else } catch { Write-Error -Message ('Error resolving identity {0}: {1}' -f $Id, $_.Exception.Message) throw } #end try-catch } #end if-else ############################# # Get reference to target object ############################# try { # Use caching to avoid redundant queries if ($AdObjectCache.ContainsKey($LDAPPath)) { $Object = $AdObjectCache[$LDAPPath] Write-Debug -Message ('Using cached object for LDAP path: {0}' -f $LDAPPath) } else { # Use server-side filtering for better performance $Object = Get-ADObject -Identity $LDAPPath -Properties nTSecurityDescriptor $AdObjectCache[$LDAPPath] = $Object Write-Debug -Message ('Retrieved object from AD: {0}' -f $Object.DistinguishedName) } #end if-else # Prepare the AD path for Get-Acl $ObjectPath = ('AD:\{0}' -f $Object.DistinguishedName) } catch { Write-Error -Message ('Error retrieving AD object {0}: {1}' -f $LDAPPath, $_.Exception.Message) throw } #end try-catch ############################# # Get current ACL ############################# try { $Acl = Get-Acl -Path $ObjectPath Write-Debug -Message ('Retrieved current DACL for object: {0}' -f $Object.DistinguishedName) } catch { Write-Error -Message ('Error retrieving ACL for {0}: {1}' -f $Object.DistinguishedName, $_.Exception.Message) throw } #end try-catch ############################# # Prepare access rule arguments ############################# # 1. Identity Reference (Trustee) $IdentityRef = [System.Security.Principal.IdentityReference]$GroupSid # 2. Active Directory Rights $ActiveDirectoryRight = [System.DirectoryServices.ActiveDirectoryRights]$PSBoundParameters['AdRight'] # 3. Access Control Type (Allow/Deny) $ACType = [System.Security.AccessControl.AccessControlType]$PSBoundParameters['AccessControlType'] # 4. Object Type (GUID) # Parameter already properly typed as Guid # 5. Security Inheritance $SecurityInheritance = [System.DirectoryServices.ActiveDirectorySecurityInheritance]$PSBoundParameters['AdSecurityInheritance'] # Create Access Rule object $AccessRule = [System.DirectoryServices.ActiveDirectoryAccessRule]::new( $IdentityRef, $ActiveDirectoryRight, $ACType, $ObjectTypeGuid, $SecurityInheritance ) ############################# # Add or Remove the rule ############################# if ($RemoveRule) { # Remove the access rule if ($PSCmdlet.ShouldProcess( $Object.DistinguishedName, ('Remove {0} access rule for {1}' -f $ActiveDirectoryRight, $Id))) { # Find and remove matching rules $RulesToRemove = $Acl.Access | Where-Object { $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value -eq $GroupSid.Value -and $_.ActiveDirectoryRights -eq $ActiveDirectoryRight -and $_.AccessControlType -eq $ACType -and $_.ObjectType -eq $ObjectTypeGuid -and $_.InheritanceType -eq $SecurityInheritance } # Check if any rules were found if ($null -ne $RulesToRemove) { if ($RulesToRemove -is [array]) { foreach ($RuleToRemove in $RulesToRemove) { $null = $Acl.RemoveAccessRule($RuleToRemove) $RulesRemovedCount++ } #end foreach } else { # Single object case $null = $Acl.RemoveAccessRule($RulesToRemove) $RulesRemovedCount = 1 } } #end if Write-Verbose -Message ('Removed {0} access rule(s) from {1} for {2}' -f $RulesRemovedCount, $Object.DistinguishedName, $Id) } #end if } else { # Add the access rule if ($PSCmdlet.ShouldProcess( $Object.DistinguishedName, ('Add {0} access rule for {1}' -f $ActiveDirectoryRight, $Id))) { $null = $Acl.AddAccessRule($AccessRule) Write-Verbose -Message ('Added {0} access rule to {1} for {2}' -f $ActiveDirectoryRight, $Object.DistinguishedName, $Id) } #end if } #end if-else ############################# # Apply the modified ACL ############################# try { if ($PSCmdlet.ShouldProcess($Object.DistinguishedName, 'Apply modified ACL')) { try { # Attempt to set ACL with standard method first Set-Acl -AclObject $Acl -Path $ObjectPath -ErrorAction Stop Write-Verbose -Message ('Applied modified ACL to {0}' -f $Object.DistinguishedName) } catch [System.UnauthorizedAccessException] { # Handle access denied errors by using a different approach Write-Verbose -Message ( 'Access denied using Set-Acl. Attempting alternative method for {0}' -f $Object.DistinguishedName ) # Get the DirectoryEntry object directly $DirectoryEntry = [ADSI]"LDAP://$($Object.DistinguishedName)" # Set the security descriptor $DirectoryEntry.psbase.ObjectSecurity = $Acl # Commit the changes $DirectoryEntry.psbase.CommitChanges() Write-Verbose -Message ( 'Successfully applied ACL to {0} using DirectoryEntry method' -f $Object.DistinguishedName ) } #end try-catch } #end if } catch { Write-Error -Message (' Error applying modified ACL to {0}: {1} ' -f $Object.DistinguishedName, $_.Exception.Message ) throw } #end try-catch } catch { Write-Error -Message ('Error processing {0}: {1}' -f $LDAPPath, $_.Exception.Message) Write-Error -Message $_.ScriptStackTrace throw } #end try-catch } #end Process End { # Display function footer if variables exist if ($null -ne $Variables -and $null -ne $Variables.FooterDelegation) { $txt = ($Variables.FooterDelegation -f $MyInvocation.InvocationName, 'adding access rule with 5 arguments (Private Function).' ) Write-Verbose -Message $txt } #end if } #end END } #end function Set-AclConstructor5 |