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", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] [CmdletBinding()] param ( [PSFComputer] $Server, [PSCredential] $Credential ) begin { #region Utility Functions 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() } } #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 -Properties adminCount if ($adObject.adminCount) { $defaultPermissions = @() $desiredPermissions = Get-AdminSDHolderRules @parameters } else { $defaultPermissions = Get-DMObjectDefaultPermission @parameters -ObjectClass $adObject.ObjectClass $desiredPermissions = $script:accessRules[$key] | Convert-AccessRule @parameters -ADObject $adObject } $delta = Compare-AccessRules @parameters -ADRules ($adAclObject.Access | Convert-AccessRuleIdentity @parameters) -ConfiguredRules $desiredPermissions -DefaultRules $defaultPermissions -ADObject $adObject if ($delta) { New-TestResult @resultDefaults -Type Update -Changed $delta -ADObject $adAclObject continue } } #endregion Process Configured Objects $doParallelize = Get-PSFConfigValue -FullName 'DomainManagement.AccessRules.Parallelize' #region Process Non-Configured AD Objects - Serial if (-not $doParallelize) { $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 -Properties adminCount } $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 } # Protected Objects if ($foundADObject.AdminCount) { $compareParam.DefaultRules = @() $compareParam.ConfiguredRules = Get-AdminSDHolderRules @parameters } $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() return } #endregion Process Non-Configured AD Objects - Serial #region Process Non-Configured AD Objects - Parallel #region Prepare Runspace Environment $variables = @{ resultDefaults = @{ Server = $Server ObjectType = 'AccessRule' } parameters = $parameters adminSDHolderRules = Get-AdminSDHolderRules @parameters schemaDefaultPermissions = $script:schemaObjectDefaultPermission["$Server"] accessRuleConfiguration = @{ accessRules = $script:accessRules accessCategoryRules = $script:accessCategoryRules } objectCategorySettings = $script:objectCategories stringTable = $script:nameReplacementTable } $modules = @( (Get-Module DomainManagement).ModuleBase (Get-Module ADSec).ModuleBase ) $functions = @{ 'New-TestResult' = [ScriptBlock]::Create((Get-Command -Name New-TestResult).Definition) } $begin = { $null = Get-Acl -Path . & (Get-Module DomainManagement) { $script:schemaObjectDefaultPermission["$($global:parameters.Server)"] = $global:schemaDefaultPermissions.Clone() $script:accessRules = $global:accessRuleConfiguration.accessRules.Clone() $script:accessCategoryRules = $global:accessRuleConfiguration.accessCategoryRules.Clone() $script:nameReplacementTable = $global:stringTable.Clone() $script:objectCategories = $global:objectCategorySettings.Clone() foreach ($__category in $script:objectCategories.Values) { if ($__category.TestScript -is [scriptblock]) { $__category.TestScript = ([PsfScriptBlock]$__category.TestScript).ToGlobal() } } } $global:convertCmdName = { Convert-DMSchemaGuid @parameters -OutType Name }.GetSteppablePipeline() $global:convertCmdName.Begin($true) $global:convertCmdGuid = { Convert-DMSchemaGuid @parameters -OutType Guid }.GetSteppablePipeline() $global:convertCmdGuid.Begin($true) $global:cmdCompareAccessRules = & (Get-Module DomainManagement) { Get-Command Compare-AccessRules } $global:cmdConvertAccessRuleIdentity = & (Get-Module DomainManagement) { Get-Command Convert-AccessRuleIdentity } $global:cmdGetCategoryBasedRules = & (Get-Module DomainManagement) { Get-Command Get-CategoryBasedRules } } $process = { $count = 0 do { try { $foundADObject = $_ $adAclObject = Get-AdsAcl @parameters -Path $foundADObject.DistinguishedName $compareParam = @{ ADRules = & $global:cmdConvertAccessRuleIdentity -InputObject $adAclObject.Access @parameters DefaultRules = Get-DMObjectDefaultPermission @parameters -ObjectClass $foundADObject.ObjectClass ConfiguredRules = & $global:cmdGetCategoryBasedRules -ADObject $foundADObject @parameters -ConvertNameCommand $convertCmdName -ConvertGuidCommand $convertCmdGuid ADObject = $foundADObject } # Protected Objects if ($foundADObject.AdminCount) { $compareParam.DefaultRules = @() $compareParam.ConfiguredRules = $adminSDHolderRules } $compareParam += $parameters $delta = & $global:cmdCompareAccessRules @compareParam } catch { $count++ if ($count -lt 10) { continue } $fail = [PSCustomObject]@{ ADObject = $foundADObject Acl = $adAclObject Error = $_ } Write-PSFRunspaceQueue -Name fails -Value $fail break } if ($delta) { New-TestResult @resultDefaults -Type Update -Changed $delta -ADObject $adAclObject -Identity $foundADObject.DistinguishedName } break } while ($true) } $end = { $global:convertCmdName.End() $global:convertCmdGuid.End() } $param = @{ Name = 'AccessRuleProcessor' Count = (Get-PSFConfigValue -FullName 'DomainManagement.AccessRules.Threads' -Fallback 4) InQueue = 'input' OutQueue = 'results' Functions = $functions Modules = $modules Variables = $variables Begin = $begin Process = $process End = $end CloseOutQueue = $true } #endregion Prepare Runspace Environment $workflow = New-PSFRunspaceWorkflow -Name 'DomainManagement.AccessRules' -Force $null = $workflow | Add-PSFRunspaceWorker @param $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 -Properties adminCount } $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 } Write-PSFRunspaceQueue -Name input -InputObject $workflow -Value $foundADObject } $workflow.Queues.input.Closed = $true try { $workflow | Start-PSFRunspaceWorkflow $workflow | Wait-PSFRunspaceWorkflow -WorkerName AccessRuleProcessor -Closed -PassThru | Stop-PSFRunspaceWorkflow $fails = Read-PSFRunspaceQueue -InputObject $workflow -Name fails -All foreach ($fail in $fails) { Write-PSFMessage -Level Warning -String 'Test-DMAccessRule.Parallel.Error' -StringValues $fail.ADObject -ErrorRecord $fail.Error -Target $fail } $results = Read-PSFRunspaceQueue -InputObject $workflow -Name results -All # Fix String Presentation for objects from a background runspace $results | Add-Member -MemberType ScriptMethod -Name ToString -Value { $this.Identity } -Force $results.Changed | Add-Member -MemberType ScriptMethod ToString -Value { '{0}: {1}' -f $this.Type, $this.Identity } -Force $results } finally { $workflow | Remove-PSFRunspaceWorkflow } #endregion Process Non-Configured AD Objects - Parallel } } |