functions/AccessRule/Test-DMAccessRule.ps1
function Test-DMAccessRule { <# .SYNOPSIS Validates the targeted domain's Access Rule configuration. .DESCRIPTION Validates the targeted domain's Access Rule configuration. This is done by comparing each relevant object's non-inherited permissions with the Schema-given default permissions for its object type. Then the remaining explicit permissions that are not part of the schema default are compared with the configured desired state. The desired state can be defined using Register-DMAccessRule. Basically, two kinds of rules are supported: - Path based access rules - point at a DN and tell the system what permissions should be applied. - Rule based access rules - All objects matching defined conditions will be affected by the defined rules. To define rules - also known as Object Categories - use Register-DMObjectCategory. Example rules could be "All Domain Controllers" or "All Service Connection Points with the name 'Virtual Machine'" This command will test all objects that ... - Have at least one path based rule. - Are considered as "under management", as defined using Set-DMContentMode It uses a definitive approach - any access rule not defined will be flagged for deletion! .PARAMETER Server The server / domain to work with. .PARAMETER Credential The credentials to use for this operation. .EXAMPLE PS C:\> Test-DMAccessRule -Server fabrikam.com Tests, whether the fabrikam.com domain conforms to the configured, desired state. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")] [CmdletBinding()] param ( [PSFComputer] $Server, [PSCredential] $Credential ) begin { #region Utility Functions function Compare-AccessRules { [CmdletBinding()] param ( $ADRules, $ConfiguredRules, $DefaultRules, $ADObject, [PSFComputer] $Server, [PSCredential] $Credential ) $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential # Resolve the mode under which it will be evaluated. Either 'Additive' or 'Constrained' $processingMode = Resolve-DMAccessRuleMode @parameters -ADObject $adObject function Write-Result { [CmdletBinding()] param ( [ValidateSet('Create', 'Delete', 'FixConfig', 'Restore')] [Parameter(Mandatory = $true)] $Type, $Identity, [AllowNull()] $ADObject, [AllowNull()] $Configuration, [string] $DistinguishedName ) $item = [PSCustomObject]@{ Type = $Type Identity = $Identity ADObject = $ADObject Configuration = $Configuration DistinguishedName = $DistinguishedName } Add-Member -InputObject $item -MemberType ScriptMethod ToString -Value { '{0}: {1}' -f $this.Type, $this.Identity } -Force -PassThru } $defaultRulesPresent = [System.Collections.ArrayList]::new() $relevantADRules = :outer foreach ($adRule in $ADRules) { if ($adRule.OriginalRule.IsInherited) { continue } #region Skip OUs' "Protect from Accidential Deletion" ACE if (($adRule.AccessControlType -eq 'Deny') -and ($ADObject.ObjectClass -eq 'organizationalUnit')) { if ($adRule.IdentityReference -eq 'everyone') { continue } $eSid = [System.Security.Principal.SecurityIdentifier]'S-1-1-0' $eName = $eSid.Translate([System.Security.Principal.NTAccount]) if ($adRule.IdentityReference -eq $eName) { continue } if ($adRule.IdentityReference -eq $eSid) { continue } } #endregion Skip OUs' "Protect from Accidential Deletion" ACE foreach ($defaultRule in $DefaultRules) { if (Test-AccessRuleEquality -Parameters $parameters -Rule1 $adRule -Rule2 $defaultRule) { $null = $defaultRulesPresent.Add($defaultRule) continue outer } } $adRule } #region Foreach non-default AD Rule: Check whether configured and delete if not so :outer foreach ($relevantADRule in $relevantADRules) { foreach ($configuredRule in $ConfiguredRules) { if (Test-AccessRuleEquality -Parameters $parameters -Rule1 $relevantADRule -Rule2 $configuredRule) { # If explicitly defined for deletion, do so if ('False' -eq $configuredRule.Present) { Write-Result -Type Delete -Identity $relevantADRule.IdentityReference -ADObject $relevantADRule -DistinguishedName $ADObject } continue outer } } # Don't generate delete changes if ($processingMode -eq 'Additive') { continue } # Don't generate delete changes, unless we have configured a permission level for the affected identity if ($processingMode -eq 'Defined') { if (-not ($relevantADRule.IdentityReference | Compare-Identity -Parameters $parameters -ReferenceIdentity $ConfiguredRules.IdentityReference -IncludeEqual -ExcludeDifferent)) { continue } } Write-Result -Type Delete -Identity $relevantADRule.IdentityReference -ADObject $relevantADRule -DistinguishedName $ADObject } #endregion Foreach non-default AD Rule: Check whether configured and delete if not so #region Foreach configured rule: Check whether it exists as defined or make it so :outer foreach ($configuredRule in $ConfiguredRules) { foreach ($defaultRule in $DefaultRules) { if ('True' -ne $configuredRule.Present) { break } if ($configuredRule.NoFixConfig) { break } if (Test-AccessRuleEquality -Parameters $parameters -Rule1 $defaultRule -Rule2 $configuredRule) { Write-Result -Type FixConfig -Identity $defaultRule.IdentityReference -ADObject $defaultRule -Configuration $configuredRule -DistinguishedName $ADObject continue outer } } foreach ($relevantADRule in $relevantADRules) { if (Test-AccessRuleEquality -Parameters $parameters -Rule1 $relevantADRule -Rule2 $configuredRule) { continue outer } } # Do not generate Create rules for any rule not configured for creation if ('True' -ne $configuredRule.Present) { continue } Write-Result -Type Create -Identity $configuredRule.IdentityReference -Configuration $configuredRule -DistinguishedName $ADObject } #endregion Foreach configured rule: Check whether it exists as defined or make it so #region Foreach non-existent default rule: Create unless configured otherwise $domainControllersOUFilter = '*{0}' -f ('OU=Domain Controllers,%DomainDN%' | Resolve-String) :outer foreach ($defaultRule in $DefaultRules | Where-Object { $_ -notin $defaultRulesPresent.ToArray()}) { # Do not apply restore to Domain Controllers OU, as it is already deployed intentionally diverging from the OU defaults if ($ADObject -like $domainControllersOUFilter) { break } foreach ($configuredRule in $ConfiguredRules) { if (Test-AccessRuleEquality -Parameters $parameters -Rule1 $defaultRule -Rule2 $configuredRule) { # If we explicitly don't want the rule: Skip and do NOT create a restoration action if ('True' -ne $configuredRule.Present) { continue outer } } } Write-Result -Type Restore -Identity $defaultRule.IdentityReference -Configuration $defaultRule -DistinguishedName $ADObject } #endregion Foreach non-existent default rule: Create unless configured otherwise } function Convert-AccessRule { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] $Rule, [Parameter(Mandatory = $true)] $ADObject, [PSFComputer] $Server, [PSCredential] $Credential ) begin { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential $convertCmdName = { Convert-DMSchemaGuid @parameters -OutType Name }.GetSteppablePipeline() $convertCmdName.Begin($true) $convertCmdGuid = { Convert-DMSchemaGuid @parameters -OutType Guid }.GetSteppablePipeline() $convertCmdGuid.Begin($true) } process { foreach ($ruleObject in $Rule) { $objectTypeGuid = $convertCmdGuid.Process($ruleObject.ObjectType)[0] $objectTypeName = $convertCmdName.Process($ruleObject.ObjectType)[0] $inheritedObjectTypeGuid = $convertCmdGuid.Process($ruleObject.InheritedObjectType)[0] $inheritedObjectTypeName = $convertCmdName.Process($ruleObject.InheritedObjectType)[0] try { $identity = Resolve-Identity @parameters -IdentityReference $ruleObject.IdentityReference -ADObject $ADObject } catch { if ('True' -ne $ruleObject.Present) { continue } Stop-PSFFunction -String 'Convert-AccessRule.Identity.ResolutionError' -Target $ruleObject -ErrorRecord $_ -Continue } [PSCustomObject]@{ PSTypeName = 'DomainManagement.AccessRule.Converted' IdentityReference = $identity AccessControlType = $ruleObject.AccessControlType ActiveDirectoryRights = $ruleObject.ActiveDirectoryRights InheritanceFlags = $ruleObject.InheritanceFlags InheritanceType = $ruleObject.InheritanceType InheritedObjectType = $inheritedObjectTypeGuid InheritedObjectTypeName = $inheritedObjectTypeName ObjectFlags = $ruleObject.ObjectFlags ObjectType = $objectTypeGuid ObjectTypeName = $objectTypeName PropagationFlags = $ruleObject.PropagationFlags Present = $ruleObject.Present } } } end { #region Inject Category-Based rules Get-CategoryBasedRules -ADObject $ADObject @parameters -ConvertNameCommand $convertCmdName -ConvertGuidCommand $convertCmdGuid #endregion Inject Category-Based rules $convertCmdName.End() $convertCmdGuid.End() } } function Convert-AccessRuleIdentity { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '')] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] [System.DirectoryServices.ActiveDirectoryAccessRule[]] $InputObject, [PSFComputer] $Server, [PSCredential] $Credential ) begin { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential $domainObject = Get-Domain2 @parameters } process { :main foreach ($accessRule in $InputObject) { if ($accessRule.IdentityReference -is [System.Security.Principal.NTAccount]) { Add-Member -InputObject $accessRule -MemberType NoteProperty -Name OriginalRule -Value $accessRule -PassThru continue main } if (-not $accessRule.IdentityReference.AccountDomainSid) { try { $identity = Get-Principal @parameters -Sid $accessRule.IdentityReference -Domain $domainObject.DNSRoot -OutputType NTAccount } catch { # Empty Catch is OK here, warning happens in command } } else { try { $identity = Get-Principal @parameters -Sid $accessRule.IdentityReference -Domain $accessRule.IdentityReference -OutputType NTAccount } catch { # Empty Catch is OK here, warning happens in command } } if (-not $identity) { $identity = $accessRule.IdentityReference } $newRule = [System.DirectoryServices.ActiveDirectoryAccessRule]::new($identity, $accessRule.ActiveDirectoryRights, $accessRule.AccessControlType, $accessRule.ObjectType, $accessRule.InheritanceType, $accessRule.InheritedObjectType) # Include original object as property in order to facilitate removal if needed. Add-Member -InputObject $newRule -MemberType NoteProperty -Name OriginalRule -Value $accessRule -PassThru } } } function Resolve-Identity { [CmdletBinding()] param ( [string] $IdentityReference, $ADObject, [PSFComputer] $Server, [PSCredential] $Credential ) #region Parent Resolution if ($IdentityReference -eq '<Parent>') { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential $domainObject = Get-Domain2 @parameters $parentPath = ($ADObject.DistinguishedName -split ",",2)[1] $parentObject = Get-ADObject @parameters -Identity $parentPath -Properties SamAccountName, Name, ObjectSID if (-not $parentObject.ObjectSID) { Stop-PSFFunction -String 'Resolve-Identity.ParentObject.NoSecurityPrincipal' -StringValues $ADObject, $parentObject.Name, $parentObject.ObjectClass -EnableException $true -Cmdlet $PSCmdlet } if ($parentObject.SamAccountName) { return [System.Security.Principal.NTAccount]('{0}\{1}' -f $domainObject.Name, $parentObject.SamAccountName) } else { return [System.Security.Principal.NTAccount]('{0}\{1}' -f $domainObject.Name, $parentObject.Name) } } #endregion Parent Resolution #region Default Resolution $identity = Resolve-String -Text $IdentityReference if ($identity -as [System.Security.Principal.SecurityIdentifier]) { $identity = $identity -as [System.Security.Principal.SecurityIdentifier] } else { $identity = $identity -as [System.Security.Principal.NTAccount] } if ($null -eq $identity) { $identity = (Resolve-String -Text $IdentityReference) -as [System.Security.Principal.NTAccount] } $identity #endregion Default Resolution } function Get-CategoryBasedRules { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $ADObject, [PSFComputer] $Server, [PSCredential] $Credential, $ConvertNameCommand, $ConvertGuidCommand ) $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include ADObject, Server, Credential $resolvedCategories = Resolve-DMObjectCategory @parameters foreach ($resolvedCategory in $resolvedCategories) { foreach ($ruleObject in $script:accessCategoryRules[$resolvedCategory.Name]) { $objectTypeGuid = $ConvertGuidCommand.Process($ruleObject.ObjectType)[0] $objectTypeName = $ConvertNameCommand.Process($ruleObject.ObjectType)[0] $inheritedObjectTypeGuid = $ConvertGuidCommand.Process($ruleObject.InheritedObjectType)[0] $inheritedObjectTypeName = $ConvertNameCommand.Process($ruleObject.InheritedObjectType)[0] try { $identity = Resolve-Identity @parameters -IdentityReference $ruleObject.IdentityReference } catch { Stop-PSFFunction -String 'Convert-AccessRule.Identity.ResolutionError' -Target $ruleObject -ErrorRecord $_ -Continue } [PSCustomObject]@{ PSTypeName = 'DomainManagement.AccessRule.Converted' IdentityReference = $identity AccessControlType = $ruleObject.AccessControlType ActiveDirectoryRights = $ruleObject.ActiveDirectoryRights InheritanceFlags = $ruleObject.InheritanceFlags InheritanceType = $ruleObject.InheritanceType InheritedObjectType = $inheritedObjectTypeGuid InheritedObjectTypeName = $inheritedObjectTypeName ObjectFlags = $ruleObject.ObjectFlags ObjectType = $objectTypeGuid ObjectTypeName = $objectTypeName PropagationFlags = $ruleObject.PropagationFlags Present = $ruleObject.Present } } } } #endregion Utility Functions $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential $parameters['Debug'] = $false Assert-ADConnection @parameters -Cmdlet $PSCmdlet Invoke-Callback @parameters -Cmdlet $PSCmdlet Assert-Configuration -Type accessRules -Cmdlet $PSCmdlet Set-DMDomainContext @parameters try { $null = Get-DMObjectDefaultPermission -ObjectClass top @parameters } catch { Stop-PSFFunction -String 'Test-DMAccessRule.DefaultPermission.Failed' -StringValues $Server -Target $Server -EnableException $false -ErrorRecord $_ return } } process { if (Test-PSFFunctionInterrupt) { return } #region Process Configured Objects foreach ($key in $script:accessRules.Keys) { $resolvedPath = Resolve-String -Text $key $resultDefaults = @{ Server = $Server ObjectType = 'AccessRule' Identity = $resolvedPath Configuration = $script:accessRules[$key] } if (-not (Test-ADObject @parameters -Identity $resolvedPath)) { if ($script:accessRules[$key].Optional -notcontains $false) { continue } New-TestResult @resultDefaults -Type 'MissingADObject' continue } try { $adAclObject = Get-AdsAcl @parameters -Path $resolvedPath -EnableException } catch { if ($script:accessRules[$key].Optional -notcontains $false) { continue } Write-PSFMessage -String 'Test-DMAccessRule.NoAccess' -StringValues $resolvedPath -Tag 'panic','failed' -Target $script:accessRules[$key] -ErrorRecord $_ New-TestResult @resultDefaults -Type 'NoAccess' Continue } $adObject = Get-ADObject @parameters -Identity $resolvedPath $defaultPermissions = Get-DMObjectDefaultPermission @parameters -ObjectClass $adObject.ObjectClass $delta = Compare-AccessRules @parameters -ADRules ($adAclObject.Access | Convert-AccessRuleIdentity @parameters) -ConfiguredRules ($script:accessRules[$key] | Convert-AccessRule @parameters -ADObject $adObject) -DefaultRules $defaultPermissions -ADObject $adObject if ($delta) { New-TestResult @resultDefaults -Type Update -Changed $delta -ADObject $adAclObject continue } } #endregion Process Configured Objects #region Process Non-Configured AD Objects $resolvedConfiguredObjects = $script:accessRules.Keys | Resolve-String $foundADObjects = foreach ($searchBase in (Resolve-ContentSearchBase @parameters -NoContainer)) { Get-ADObject @parameters -LDAPFilter '(objectCategory=*)' -SearchBase $searchBase.SearchBase -SearchScope $searchBase.SearchScope } $resultDefaults = @{ Server = $Server ObjectType = 'AccessRule' } $convertCmdName = { Convert-DMSchemaGuid @parameters -OutType Name }.GetSteppablePipeline() $convertCmdName.Begin($true) $convertCmdGuid = { Convert-DMSchemaGuid @parameters -OutType Guid }.GetSteppablePipeline() $convertCmdGuid.Begin($true) $processed = @{ } foreach ($foundADObject in $foundADObjects) { # Prevent duplicate processing if ($processed[$foundADObject.DistinguishedName]) { continue } $processed[$foundADObject.DistinguishedName] = $true # Skip items that were defined in configuration, they were already processed if ($foundADObject.DistinguishedName -in $resolvedConfiguredObjects) { continue } $adAclObject = Get-AdsAcl @parameters -Path $foundADObject.DistinguishedName $compareParam = @{ ADRules = $adAclObject.Access | Convert-AccessRuleIdentity @parameters DefaultRules = Get-DMObjectDefaultPermission @parameters -ObjectClass $foundADObject.ObjectClass ConfiguredRules = Get-CategoryBasedRules -ADObject $foundADObject @parameters -ConvertNameCommand $convertCmdName -ConvertGuidCommand $convertCmdGuid ADObject = $foundADObject } $compareParam += $parameters $delta = Compare-AccessRules @compareParam if ($delta) { New-TestResult @resultDefaults -Type Update -Changed $delta -ADObject $adAclObject -Identity $foundADObject.DistinguishedName continue } } $convertCmdName.End() $convertCmdGuid.End() #endregion Process Non-Configured AD Objects } } |