functions/schema/Invoke-FMSchema.ps1

function Invoke-FMSchema {
    <#
        .SYNOPSIS
            Updates the schema to conform to the desired state.
         
        .DESCRIPTION
            Updates the schema to conform to the desired state.
            Can add new attributes and update existing ones.
 
            Use Register-FMSchema to define the desired state.
            Use the module's configuration settings to govern schema admin credentials.
            The configuration can be read with Get-PSFConfig and updated with Set-PSFConfig.
         
        .PARAMETER InputObject
            Test results provided by the associated test command.
            Only the provided changes will be executed, unless none were specified, in which ALL pending changes will be executed.
         
        .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.
 
        .PARAMETER Confirm
            If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
         
        .PARAMETER WhatIf
            If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
         
        .EXAMPLE
            PS C:\> Invoke-FMSchema
 
            Updates the schema of the current forest according to the configured settings
    #>

    
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')]
    Param (
        [Parameter(ValueFromPipeline = $true)]
        $InputObject,
        
        [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 Schema -Cmdlet $PSCmdlet
        Set-FMDomainContext @parameters

        try { $rootDSE = Get-ADRootDSE @parameters -ErrorAction Stop }
        catch {
            Stop-PSFFunction -String 'Invoke-FMSchema.Connect.Failed' -StringValues $Server -ErrorRecord $_ -EnableException $EnableException -Exception $_.Exception.GetBaseException()
            return
        }
        $forest = Get-ADForest @parameters
        $parameters["Server"] = $forest.SchemaMaster
        $removeParameters = $parameters.Clone()
        
        #region Resolve Credentials
        $cred = $null
        if (Test-SchemaAdminCredential) {
            Invoke-PSFProtectedCommand -ActionString 'Invoke-FMSchema.Schema.Credentials' -Target $forest.SchemaMaster -ScriptBlock {
                [PSCredential]$cred = Get-SchemaAdminCredential @parameters | Write-Output | Select-Object -First 1
                if ($cred) { $parameters['Credential'] = $cred }
            } -EnableException $EnableException -PSCmdlet $PSCmdlet
            if (Test-PSFFunctionInterrupt) { return }
        }
        
        $null = Invoke-PSFProtectedCommand -ActionString 'Invoke-FMSchema.Credentials.Test' -Target $forest.SchemaMaster -ScriptBlock {
            $null = Get-ADDomain @parameters -ErrorAction Stop
        } -EnableException $EnableException -PSCmdlet $PSCmdlet -RetryCount 5 -RetryWait 1
        if (Test-PSFFunctionInterrupt) { return }
        #endregion Resolve Credentials

        # Prepare parameters to use for when discarding the schema credentials
        if ($cred -and ($cred -ne $Credential)) { $removeParameters['SchemaAccountCredential'] = $cred }
    }
    process {
        if (Test-PSFFunctionInterrupt) { return }

        if (-not $InputObject) {
            $InputObject = Test-FMSchema @parameters
        }

        $testResultsSorted = $InputObject | Sort-Object {
            if ($_.Type -eq 'Decommission') { 0 }
            elseif ($_.Type -eq 'Rename') { 2 }
            elseif ($_.Type -eq 'ConfigurationOnly') { 3 }
            else { 1 }
        }
        :main foreach ($testItem in $testResultsSorted) {
            switch ($testItem.Type) {
                #region Create new Schema Attribute
                'Create' {
                    Invoke-PSFProtectedCommand -ActionString 'Invoke-FMSchema.Creating.Attribute' -Target $testItem.Identity -ScriptBlock {
                        New-ADObject @parameters -Type attributeSchema -Name $testItem.Configuration.AdminDisplayName -Path $rootDSE.schemaNamingContext -OtherAttributes (Resolve-SchemaAttribute -Configuration $testItem.Configuration) -ErrorAction Stop
                        Update-Schema @parameters
                    } -EnableException $EnableException.ToBool() -PSCmdlet $PSCmdlet -Continue
                    
                    foreach ($class in  $testItem.Configuration.MayBeContainedIn) {
                        try { $classObject = Get-ADObject @parameters -SearchBase $rootDSE.schemaNamingContext -LDAPFilter "(name=$($class))" -ErrorAction Stop }
                        catch { Stop-PSFFunction -String 'Invoke-FMSchema.Reading.ObjectClass.Failed' -StringValues $class -EnableException $EnableException -Continue -ErrorRecord $_ }
                        if (-not $classObject) { Stop-PSFFunction -String 'Invoke-FMSchema.Reading.ObjectClass.NotFound' -StringValues $class -EnableException $EnableException -Continue }

                        Invoke-PSFProtectedCommand -ActionString 'Invoke-FMSchema.Assigning.Attribute.ToObjectClass' -ActionStringValues $class,$testItem.Identity -Target $testItem.Identity -ScriptBlock {
                            $classObject | Set-ADObject @parameters -Add @{ mayContain = $testItem.Configuration.LdapDisplayName } -ErrorAction Stop
                        } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue -RetryCount 10
                    }

                    foreach ($class in  $testItem.Configuration.MustBeContainedIn) {
                        try { $classObject = Get-ADObject @parameters -SearchBase $rootDSE.schemaNamingContext -LDAPFilter "(name=$($class))" -ErrorAction Stop }
                        catch { Stop-PSFFunction -String 'Invoke-FMSchema.Reading.ObjectClass.Failed' -StringValues $class -EnableException $EnableException -Continue -ErrorRecord $_ }
                        if (-not $classObject) { Stop-PSFFunction -String 'Invoke-FMSchema.Reading.ObjectClass.NotFound' -StringValues $class -EnableException $EnableException -Continue }

                        Invoke-PSFProtectedCommand -ActionString 'Invoke-FMSchema.Assigning.Attribute.ToObjectClass' -ActionStringValues $class,$testItem.Identity -Target $testItem.Identity -ScriptBlock {
                            $classObject | Set-ADObject @parameters -Add @{ mustContain = $testItem.Configuration.LdapDisplayName } -ErrorAction Stop
                        } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue -RetryCount 10
                    }
                }
                #endregion Create new Schema Attribute

                #region Decommission the unwanted Schema Attribute
                'Decommission' {
                    $values = @{
                        IsDefunct = $true
                        # PartialAttributeSet = $false
                    }
                    foreach ($adObject in (Get-ADObject @parameters -SearchBase $rootDSE.schemaNamingContext -LDAPFilter "(mayContain=$($testItem.Configuration.OID))" -Properties ldapDisplayName)) {
                        Invoke-PSFProtectedCommand -ActionString 'Invoke-FMSchema.Decommission.MayContain' -ActionStringValues $testItem.ADObject.LdapDisplayName, $adObject.LdapDisplayName -Target $testItem -ScriptBlock {
                            $adObject | Set-ADObject @parameters -Remove @{ mayContain = $testItem.ADObject.LdapDisplayName } -ErrorAction Stop
                        } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue
                    }

                    foreach ($adObject in (Get-ADObject @parameters -SearchBase $rootDSE.schemaNamingContext -LDAPFilter "(mustContain=$($testItem.Configuration.OID))" -Properties ldapDisplayName)) {
                        Invoke-PSFProtectedCommand -ActionString 'Invoke-FMSchema.Decommission.MustContain' -ActionStringValues $testItem.ADObject.LdapDisplayName, $adObject.LdapDisplayName -Target $testItem -ScriptBlock {
                            $adObject | Set-ADObject @parameters -Remove @{ mustContain = $testItem.ADObject.LdapDisplayName } -ErrorAction Stop
                        } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue
                    }

                    Invoke-PSFProtectedCommand -ActionString 'Invoke-FMSchema.Decommission.Attribute' -ActionStringValues $testItem.ADObject.LdapDisplayName, $testItem.ADObject.AttributeID -Target $testItem -ScriptBlock {
                        $testItem.ADObject | Set-ADObject @parameters -Replace $values -ErrorAction Stop
                    } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue
                    $rootDSE = Get-ADRootDSE @parameters
                }
                #endregion Decommission the unwanted Schema Attribute

                #region Update Schema Attribute
                'Update' {
                    $resolvedAttributes = Resolve-SchemaAttribute -Configuration $testItem.Configuration -ADObject $testItem.ADObject -Changes $testItem.Changed
                    if ($resolvedAttributes.Keys.Count -ge 1) {
                        Invoke-PSFProtectedCommand -ActionString 'Invoke-FMSchema.Updating.Attribute' -ActionStringValues ($resolvedAttributes.Keys -join ', ') -Target $testItem.Identity -ScriptBlock {
                            $testItem.ADObject | Set-ADObject @parameters -Replace $resolvedAttributes -ErrorAction Stop
                        } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue
                    }

                    # Do not process MayContain or MustContain for defunct attributes
                    if ($testItem.Configuration.IsDefunct) { continue }

                    # Only proceed if any Object Class changes are intended
                    $changes = $testItem.Changed | Where-Object Property -in 'MayContain','MustContain'
                    if (-not $changes) { continue }

                    foreach ($change in $changes) {
                        $property = 'mayContain'
                        if ($change.Property -eq 'MustContain') { $property = 'mustContain' }
                        foreach ($class in $change.New | Where-Object { $_ -notin $change.Old }) {
                            if (-not $class) { continue }
                            try { $classObject = Get-ADObject @parameters -SearchBase $rootDSE.schemaNamingContext -LDAPFilter "(name=$($class))" -ErrorAction Stop -Properties $property }
                            catch { Stop-PSFFunction -String 'Invoke-FMSchema.Reading.ObjectClass.Failed' -StringValues $class -EnableException $EnableException -Continue -ErrorRecord $_ }
                            if (-not $classObject) { Stop-PSFFunction -String 'Invoke-FMSchema.Reading.ObjectClass.NotFound' -StringValues $class -EnableException $EnableException -Continue }
        
                            if ($classObject.$property -notcontains $testItem.ADObject.LdapDisplayName) {
                                Invoke-PSFProtectedCommand -ActionString 'Invoke-FMSchema.Assigning.Attribute.ToObjectClass' -ActionStringValues $class, $testItem.Identity -Target $testItem.Identity -ScriptBlock {
                                    $classObject | Set-ADObject @parameters -Add @{ $property = $testItem.ADObject.LdapDisplayName } -ErrorAction Stop
                                } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue
                            }
                        }
    
                        foreach ($class in $change.Old | Where-Object { $_ -notin $change.New }) {
                            if (-not $class) { continue }
                            try { $classObject = Get-ADObject @parameters -SearchBase $rootDSE.schemaNamingContext -LDAPFilter "(name=$($class))" -ErrorAction Stop -Properties $property }
                            catch { Stop-PSFFunction -String 'Invoke-FMSchema.Reading.ObjectClass.Failed' -StringValues $class -EnableException $EnableException -Continue -ErrorRecord $_ }
                            if (-not $classObject) { Stop-PSFFunction -String 'Invoke-FMSchema.Reading.ObjectClass.NotFound' -StringValues $class -EnableException $EnableException -Continue }
        
                            if ($classObject.$property -contains $testItem.ADObject.LdapDisplayName) {
                                Invoke-PSFProtectedCommand -ActionString 'Invoke-FMSchema.Removing.Attribute.FromObjectClass' -ActionStringValues $class, $testItem.Identity -Target $testItem.Identity -ScriptBlock {
                                    $classObject | Set-ADObject @parameters -Remove @{ $property = $testItem.ADObject.LdapDisplayName } -ErrorAction Stop
                                } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue
                            }
                        }
                    }
                }
                #endregion Update Schema Attribute

                #region Rename Schema Attribute
                'Rename' {
                    Invoke-PSFProtectedCommand -ActionString 'Invoke-FMSchema.Rename.Attribute' -ActionStringValues $testItem.ADObject.cn, $testItem.Configuration.Name -Target $testItem -ScriptBlock {
                        $testItem.ADObject | Rename-ADObject @parameters -NewName $testItem.Configuration.Name -ErrorAction Stop
                    } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue
                }
                #endregion Rename Schema Attribute
            }
        }
    }
    end {
        if (Test-PSFFunctionInterrupt) { return }

        if (Test-SchemaAdminCredential) {
            Invoke-PSFProtectedCommand -ActionString 'Invoke-FMSchema.Schema.Credentials.Release' -Target $forest.SchemaMaster -ScriptBlock {
                $null = Remove-SchemaAdminCredential @removeParameters -ErrorAction Stop
            } -EnableException $EnableException -PSCmdlet $PSCmdlet
        }
    }
}