functions/schemadefaultpermissions/Test-FMSchemaDefaultPermission.ps1

function Test-FMSchemaDefaultPermission
{
<#
    .SYNOPSIS
        Validates, whether the target forest has the defined default permissions applied in its schema.
     
    .DESCRIPTION
        Validates, whether the target forest has the defined default permissions applied in its schema.
        Returns a list of all actions that would be taken by the associated Invoke-* command.
     
    .PARAMETER Server
        The server / domain to work with.
         
    .PARAMETER Credential
        The credentials to use for this operation.
 
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> Test-FMSchemaDefaultPermission -Server contoso.com
     
        Validates, whether the contoso.com forest has the defined default permissions applied in its schema.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")]
    [CmdletBinding()]
    param (
        [PSFComputer]
        $Server,
        
        [PSCredential]
        $Credential,
        
        [switch]
        $EnableException
    )
    
    begin
    {
        $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential
        $parameters['Debug'] = $false
        Assert-ADConnection @parameters -Cmdlet $PSCmdlet
        Invoke-Callback @parameters -Cmdlet $PSCmdlet
        Assert-Configuration -Type SchemaDefaultPermissions -Cmdlet $PSCmdlet
        Set-FMDomainContext @parameters
        try { $rootDSE = Get-ADRootDSE @parameters -ErrorAction Stop }
        catch
        {
            Stop-PSFFunction -String 'Test-FMSchemaDefaultPermission.Connect.Failed' -StringValues $Server -ErrorRecord $_ -EnableException $EnableException -Exception $_.Exception.GetBaseException()
            return
        }
        $forest = Get-ADForest @parameters
        $parameters["Server"] = $forest.SchemaMaster
        Invoke-PSFProtectedCommand -ActionString 'Test-FMSchemaDefaultPermission.WinRM.Connect' -Target $forest.SchemaMaster -ScriptBlock {
            $psParameters = $parameters.Clone()
            $psParameters.Remove('Server')
            $psParameters.ComputerName = $forest.SchemaMaster
            $session = New-PSSession @psParameters -ErrorAction Stop
        } -EnableException $EnableException -PSCmdlet $PSCmdlet -WhatIf:$false -Confirm:$false
        
        #region Default Permissions Scriptblock
        $defaultPermissionScriptblock = {
            param (
                $ClassName,
                
                $SchemaNC
            )
            $object = Get-ADObject -LDAPFilter "(name=$ClassName)" -SearchBase $SchemaNC -Server localhost -Properties defaultSecurityDescriptor
            if (-not $object) { throw "Object class '$ClassName' not found!" }
            $acl = New-Object System.DirectoryServices.ActiveDirectorySecurity
            $acl.SetSecurityDescriptorSddlForm($object.defaultSecurityDescriptor)
            $acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])
        }
        #endregion Default Permissions Scriptblock
        
        #region Utility Functions
        function Convert-ConfiguredAccessRule
        {
            [CmdletBinding()]
            param (
                [Parameter(ValueFromPipeline = $true)]
                $AccessRule,
                
                [System.Collections.Hashtable]
                $Parameters
            )
            process
            {
                $basicHash = $AccessRule | ConvertTo-PSFHashtable
                $basicHash.IdentityResolved = $true
                $basicHash.Error = $null
                $basicHash.ObjectTypeGuid = Convert-DMSchemaGuid @Parameters -Name $basicHash.ObjectType -OutType GuidString
                $basicHash.ObjectTypeName = Convert-DMSchemaGuid @Parameters -Name $basicHash.ObjectType -OutType Name
                $basicHash.InheritedObjectTypeGuid = Convert-DMSchemaGuid @Parameters -Name $basicHash.InheritedObjectType -OutType GuidString
                $basicHash.InheritedObjectTypeName = Convert-DMSchemaGuid @Parameters -Name $basicHash.InheritedObjectType -OutType Name
                
                # Namensauflösung
                $basicHash.ResolvedIdentity = $AccessRule.Identity | Resolve-String -Mode Lax -ArgumentList $Parameters
                
                # Principal Auflösung
                try { $basicHash.Principal = [string]($basicHash.ResolvedIdentity | Resolve-Principal -OutputType SID @Parameters -ErrorAction Stop) }
                catch
                {
                    Write-PSFMessage -Level Warning -String 'Test-FMSchemaDefaultPermission.Principal.ResolutionError' -StringValues $AccessRule.Identity, $basicHash.ResolvedIdentity -Target $AccessRule
                    $basicHash.IdentityResolved = $false
                    $basicHash.Error = $_
                }
                
                [pscustomobject]$basicHash
            }
        }
        
        function Compare-AccessRule
        {
            [CmdletBinding()]
            param (
                $Configuration,
                
                $Applied,
                
                [PSFComputer]
                $Server,
                
                [Hashtable]
                $Parameters
            )
            
            #region Utility Functions
            function New-Change
            {
                [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
                [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")]
                [CmdletBinding()]
                param (
                    [Parameter(Mandatory = $true)]
                    [ValidateSet('Add', 'Remove')]
                    [string]
                    $Type,
                    
                    $Configuration,
                    
                    $ADObject,
                    
                    [string]
                    $ClassName,
                    
                    [hashtable]
                    $Parameters
                )
                
                $object = [pscustomobject]@{
                    PSTypeName = 'ForestManagement.SchemaDefaultPermission.Change'
                    Type       = $Type
                    Identity   = $Configuration.ResolvedIdentity
                    Privilege  = $Configuration.ActiveDirectoryRights
                    Access       = $Configuration.AccessControlType -as [string]
                    Configuration = $Configuration
                    ADObject   = $ADObject
                    ClassName  = $ClassName
                }
                if ($ADObject)
                {
                    $object.Identity = $ADObject.IdentityReference
                    if (($ADObject.IdentityReference -as [System.Security.Principal.SecurityIdentifier]).AccountDomainSid)
                    {
                        try { $object.Identity = $ADObject.IdentityReference | Resolve-Principal @Parameters -ErrorAction Stop -OutputType NTAccount }
                        catch { } # No Action Needed
                    }
                    $object.Privilege = $ADObject.ActiveDirectoryRights
                    $object.Access = $ADObject.AccessControlType -as [string]
                }
                
                Add-Member -InputObject $object -MemberType ScriptMethod -Name ToString -Force -Value {
                    '{0}: {1}>{2}({3})' -f $this.Type, $this.Identity, $this.Privilege, $this.Access.SubString(0, 1)
                } -PassThru
            }
            #endregion Utility Functions
            
            #region Process ProcessingMode
            $processingMode = 'Additive'
            if ($Configuration.Mode -contains 'Defined') { $processingMode = 'Defined' }
            if ($Configuration.Mode -contains 'Constrained') { $processingMode = 'Constrained' }
            
            if ($processingMode -eq 'Constrained' -and ($Configuration | Where-Object IdentityResolved -EQ $false))
            {
                Write-PSFMessage -Level Warning -String 'Test-FMSchemaDefaultPermission.Class.IdentityUncertain' -StringValues $Configuration[0].ClassName -Target $Configuration[0].ClassName -Data @{
                    Configured = $Configuration
                    Applied    = $Applied
                }
                New-TestResult -ObjectType SchemaDefaultPermission -Type IdentityError -Identity $Configuration[0].ClassName -Server $Server -Configuration $Configuration -ADObject $Applied
                return
            }
            #endregion Process ProcessingMode
            
            $changes = @()
            $matchedRules = @()
            #region Check configured rules against applied rules
            :outer foreach ($ruleDefinition in $Configuration)
            {
                if (-not $ruleDefinition.IdentityResolved) { continue }
                foreach ($appliedRule in $Applied)
                {
                    if ($ruleDefinition.ActiveDirectoryRights -ne $appliedRule.ActiveDirectoryRights) { continue }
                    if ($ruleDefinition.InheritanceType -ne $appliedRule.InheritanceType) { continue }
                    if ($ruleDefinition.ObjectTypeGuid -ne $appliedRule.ObjectType) { continue }
                    if ($ruleDefinition.InheritedObjectTypeGuid -ne $appliedRule.InheritedObjectType) { continue }
                    if ($ruleDefinition.AccessControlType -ne $appliedRule.AccessControlType) { continue }
                    if ($ruleDefinition.Principal -ne $appliedRule.IdentityReference) { continue }
                    
                    # Existing rule is a match
                    $matchedRules += $appliedRule
                    continue outer
                }
                
                $changes += New-Change -Type Add -Configuration $ruleDefinition -ClassName $Configuration[0].ClassName -Parameters $Parameters
            }
            #endregion Check configured rules against applied rules
            
            #region Check applied rules against configured rules
            foreach ($appliedRule in $Applied)
            {
                if ($processingMode -eq 'Additive') { break }
                if ($appliedRule -in $matchedRules) { continue }
                if ($processingMode -eq 'Defined' -and $Configuration.Principal -notcontains $Applied.Identity) { continue }
                
                $changes += New-Change -Type Remove -ADObject $appliedRule -ClassName $Configuration[0].ClassName -Parameters $Parameters
            }
            #endregion Check applied rules against configured rules
            
            if ($changes)
            {
                New-TestResult -ObjectType SchemaDefaultPermission -Type Update -Identity $Configuration[0].ClassName -Server $Server -Configuration $Configuration -ADObject $Applied -Changed $changes
            }
        }
        #endregion Utility Functions
    }
    process
    {
        if (Test-PSFFunctionInterrupt) { return }
        
        foreach ($className in $script:schemaDefaultPermissions.Keys)
        {
            $definedAccessRules = $script:schemaDefaultPermissions[$className].Values | Convert-ConfiguredAccessRule -Parameters $parameters
            
            try { $actualAccessRules = Invoke-Command -Session $session -ScriptBlock $defaultPermissionScriptblock -ArgumentList $className, $rootDSE.schemaNamingContext }
            catch
            {
                Write-PSFMessage -Level Warning -String 'Test-FMSchemaDefaultPermission.Class.NotFound' -StringValues $className -Target $className
                New-TestResult -ObjectType SchemaDefaultPermission -Type NotFound -Identity $className -Server $Server -Configuration $definedAccessRules
                continue
            }
            
            Compare-AccessRule -Configuration $definedAccessRules -Applied $actualAccessRules -Server $Server -Parameters $parameters
        }
    }
    end
    {
        if ($session) { Remove-PSSession -Session $session -ErrorAction Ignore -WhatIf:$false -Confirm:$false }
    }
}