Public/Miscellaneous/Set-AdAclPromoteDomain.ps1

function Set-AdAclPromoteDomain {
    <#
        .SYNOPSIS
            Delegates permissions to promote and demote Domain Controllers.

        .DESCRIPTION
            This function delegates all necessary permissions to a specified group to allow them to promote
            and demote Domain Controllers in the domain. It configures permissions for:

            - Directory replication rights
            - Site management
            - Domain Controller management
            - DNS configuration
            - BitLocker/TPM management

            The function is idempotent and supports both adding and removing delegations.

        .PARAMETER Group
            Security group that will receive the delegation rights. Must be a valid AD group.
            Accepts pipeline input and name or Distinguished Name format.

        .PARAMETER StagingOU
            Distinguished Name of the Staging OU where new DC computer objects will be created.
            This OU must exist and be accessible before running the function.
            Server objects must be present in this OU before starting the promotion process.

        .PARAMETER RemoveRule
            If specified, removes the delegated permissions instead of adding them.
            Use with caution as this affects DC promotion capabilities.

        .EXAMPLE
            Set-AdAclPromoteDomain -Group "SG_SiteAdmins_XXXX" -StagingOU "OU=InfraStaging,OU=Infra,OU=Admin,DC=EguibarIT,DC=local"

            Delegates DC promotion rights to the specified group using the specified staging OU.

        .EXAMPLE
            Set-AdAclPromoteDomain -Group "SG_SiteAdmins_XXXX" -StagingOU "OU=InfraStaging,OU=Infra,OU=Admin,DC=EguibarIT,DC=local" -RemoveRule

            Removes DC promotion delegation from the specified group.

        .EXAMPLE
            "SG_DCAdmins" | Set-AdAclPromoteDomain -StagingOU "OU=Staging,DC=EguibarIT,DC=local"

            Delegates DC promotion rights using pipeline input for the group name.

        .OUTPUTS
            [void]

        .NOTES
            Used Functions:
                Name ║ Module
                ═════════════════════════════════════╬══════════════════════════════
                Set-AclConstructor4 ║ EguibarIT.DelegationPS
                Set-AclConstructor5 ║ EguibarIT.DelegationPS
                Get-AttributeSchemaHashTable ║ EguibarIT.DelegationPS
                Get-ExtendedRightHashTable ║ EguibarIT.DelegationPS
                Get-AdObjectType ║ EguibarIT.DelegationPS
                Set-AdDirectoryReplication ║ EguibarIT.DelegationPS
                Set-AdAclCreateDeleteSite ║ EguibarIT.DelegationPS
                Set-AdAclChangeSite ║ EguibarIT.DelegationPS
                Get-ADOrganizationalUnit ║ ActiveDirectory
                Write-Verbose ║ Microsoft.PowerShell.Utility
                Write-Error ║ Microsoft.PowerShell.Utility

        .NOTES
            Version: 1.3
            DateModified: 24/Mar/2025
            LasModifiedBy: Vicente Rodriguez Eguibar
                vicente@eguibar.com
                Eguibar IT
                http://www.eguibarit.com

        .LINK
            https://github.com/vreguibar/EguibarIT.DelegationPS/blob/main/Public/Miscellaneous/Set-AdAclPromoteDomain.ps1

        .LINK
            https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/deploy/install-active-directory-domain-services

        .COMPONENT
            ActiveDirectory

        .ROLE
            Security Administration

    #>

    [CmdletBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact = 'High'
    )]
    [OutputType([void])]

    param (
        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'Identity of the group getting the delegation',
            Position = 0)]
        [ValidateNotNullOrEmpty()]
        [Alias('IdentityReference', 'Identity', 'Trustee', 'GroupID')]
        $Group,

        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'DistinguishedName of the Staging OU. OU must exist and Server must be present here before starting the Promotion process.',
            Position = 1)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript(
            { Test-IsValidDN -ObjectDN $_ },
            ErrorMessage = 'DistinguishedName provided is not valid! Please Check.'
        )]
        [Alias('DN', 'DistinguishedName')]
        [String]
        $StagingOU,

        # PARAM2 SWITCH If present, the access rule will be removed.
        [Parameter(Mandatory = $false,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'If present, the access rule will be removed.',
            Position = 2)]
        [Switch]
        $RemoveRule,

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            HelpMessage = 'If present, the function will not ask for confirmation when performing actions.',
            Position = 3)]
        [Switch]
        $Force
    )

    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
        [Hashtable]$Splat = [hashtable]::New([StringComparer]::OrdinalIgnoreCase)

        try {
            Write-Debug -Message 'Checking variable $Variables.GuidMap. In case is empty a function is called to fill it up.'
            Get-AttributeSchemaHashTable

            Write-Debug -Message 'Checking variable $Variables.ExtendedRightsMap. In case is empty a function is called to fill it up.'
            Get-ExtendedRightHashTable

            # Verify Group exist and return it as Microsoft.ActiveDirectory.Management.AdGroup
            $CurrentGroup = Get-AdObjectType -Identity $PSBoundParameters['Group']

            if (-not $CurrentGroup) {
                throw ('Group {0} not found or not accessible' -f $PSBoundParameters['Group'])
            }

        } catch {

            Write-Error -Message ('Initialization failed: {0}' -f $_.Exception.Message)
            return

        } #end Try-Catch

    } #end Begin

    Process {

        # Each defined "Naming Context" must have these permissions
        # Variable $Variables.namingContexts contains all available naming contexts
        # the CMDLet "Set-AdDirectoryReplication" does grants all requiered rights in all NC
        # except for the "Add/Remove Replica In Domain"

        Set-AdDirectoryReplication -Group $PSBoundParameters['Group']

        Foreach ($Context in $Variables.namingContexts) {

            ####################
            # Add/Remove Replica In Domain
            $Splat = @{
                Id                = $CurrentGroup
                LDAPPath          = $Context
                AdRight           = 'ExtendedRight'
                AccessControlType = 'Allow'
                ObjectType        = $Variables.ExtendedRightsMap['Add/Remove Replica In Domain']
            }
            # Check if RemoveRule switch is present.
            If ($PSBoundParameters['RemoveRule']) {

                # Add the parameter to remove the rule
                $Splat.Add('RemoveRule', $true)

            } #end If

            If ($Force -or
                $PSCmdlet.ShouldProcess($PSBoundParameters['Group'], 'Delegate the permissions for Add/Remove Replica In Domain?')) {

                Set-AclConstructor4 @Splat




                ####################
                # Configure msDS-NC-RO-Replica-Locations on all NC
                # Not sure if ACENumber 1 is needed (ReadProperty, GenericExecute).
                # Included in DirRepl CMDlet
                <#
                ACENumber : 1
                Id : EguibarIT\XXX
                LDAPpath : CN=EguibarIT,CN=Partitions,CN=Configuration,DC=EguibarIT,DC=local
                AdRight : ReadProperty, GenericExecute
                AccessControlType : Allow
                ObjectType : All [GuidNULL]
                AdSecurityInheritance : None
                InheritedObjectType : All [GuidNULL]
                IsInherited : False
            #>



                <#
                ACENumber : 2
                IdentityReference : EguibarIT\XXX
                LDAPpath : CN=EguibarIT,CN=Partitions,CN=Configuration,DC=EguibarIT,DC=local
                AdRight : WriteProperty
                AccessControlType : Allow
                ObjectType : msDS-NC-RO-Replica-Locations [attributeSchema]
                AdSecurityInheritance : None
                InheritedObjectType : All [GuidNULL]
                IsInherited : False
            #>


            } #end ForEach


            # Needed permissions to create/Manage site
            # All these permissions are already on the following CMDlets

            Set-AdAclCreateDeleteSite -Group $CurrentGroup
            Set-AdAclChangeSite -Group $CurrentGroup

            ####################
            # Grant permissions on Sites
            <#
            ACENumber : 1
            DistinguishedName : CN=Sites,CN=Configuration,DC=EguibarIT,DC=local
            IdentityReference : EguibarIT\XXXX
            ActiveDirectoryRights : CreateChild
            AccessControlType : Allow
            ObjectType : nTDSDSA [ClassSchema]
            InheritanceType : Descendents
            InheritedObjectType : GuidNULL
            IsInherited : False

            ACENumber : 2
            DistinguishedName : CN=Sites,CN=Configuration,DC=EguibarIT,DC=local
            IdentityReference : EguibarIT\XXXX
            ActiveDirectoryRights : WriteDacl
            AccessControlType : Allow
            ObjectType : GuidNULL
            InheritanceType : Descendents
            InheritedObjectType : nTDSDSA [ClassSchema]
            IsInherited : False

            ACENumber : 3
            DistinguishedName : CN=Sites,CN=Configuration,DC=EguibarIT,DC=local
            IdentityReference : EguibarIT\XXXX
            ActiveDirectoryRights : CreateChild
            AccessControlType : Allow
            ObjectType : server [ClassSchema]
            InheritanceType : Descendents
            InheritedObjectType : GuidNULL
            IsInherited : False

            ACENumber : 4
            DistinguishedName : CN=Sites,CN=Configuration,DC=EguibarIT,DC=local
            IdentityReference : EguibarIT\XXXX
            ActiveDirectoryRights : CreateChild
            AccessControlType : Allow
            ObjectType : nTDSConnection [ClassSchema]
            InheritanceType : Descendents
            InheritedObjectType : GuidNULL
            IsInherited : False
        #>



            ####################
            # Prepare Staging container for to-be-promoted server
            # In our DM the server staging is: "OU=InfraStaging,OU=Infra,OU=Admin,DC=EguibarIT,DC=local"
            If ($PSBoundParameters['StagingOU']) {

                $existingOU = Get-ADOrganizationalUnit -Filter { DistinguishedName -like $StagingOU } -ErrorAction SilentlyContinue

                If (-not($existingOU)) {
                    $parameters = @{
                        Message           = 'Staging OU is a controlled OU where the server to be promoted resides. Computer object must have the corresponding permissions.'
                        Category          = ObjectNotFound
                        CategoryReason    = 'Staging OU could not be found!'
                        RecommendedAction = 'Ensure Staging OU {0} exists and is accessible.' -f $PSBoundParameters['StagingOU']
                    }
                    Write-Error @parameters
                } else {
                    Write-Verbose -Message ('Staging OU found ({0}). Setting the permissions.' -f $existingOU)

                    <#
                    ACENumber : 1
                    Id : EguibarIT\XXX
                    LDAPpath : OU=InfraStaging,OU=Infra,OU=Admin,DC=EguibarIT,DC=local
                    AdRight : WriteProperty
                    AccessControlType : Allow
                    ObjectType : servicePrincipalName [attributeSchema]
                    AdSecurityInheritance : Descendents
                    InheritedObjectType : computer [classSchema]
                    IsInherited : False

                    ACENumber : 2
                    Id : EguibarIT\XXX
                    LDAPpath : OU=InfraStaging,OU=Infra,OU=Admin,DC=EguibarIT,DC=local
                    AdRight : WriteProperty
                    AccessControlType : Allow
                    ObjectType : serverReference [attributeSchema]
                    AdSecurityInheritance : Descendents
                    InheritedObjectType : computer [classSchema]
                    IsInherited : False

                    ACENumber : 3
                    Id : EguibarIT\XXX
                    LDAPpath : OU=InfraStaging,OU=Infra,OU=Admin,DC=EguibarIT,DC=local
                    AdRight : WriteProperty
                    AccessControlType : Allow
                    ObjectType : userAccountControl [attributeSchema]
                    AdSecurityInheritance : Descendents
                    InheritedObjectType : computer [classSchema]
                    IsInherited : False

                    ACENumber : 4
                    Id : EguibarIT\XXX
                    LDAPpath : OU=InfraStaging,OU=Infra,OU=Admin,DC=EguibarIT,DC=local
                    AdRight : WriteProperty
                    AccessControlType : Allow
                    ObjectType : cn [attributeSchema]
                    AdSecurityInheritance : Descendents
                    InheritedObjectType : computer [classSchema]
                    IsInherited : False

                    ACENumber : 5
                    Id : EguibarIT\XXX
                    LDAPpath : OU=InfraStaging,OU=Infra,OU=Admin,DC=EguibarIT,DC=local
                    AdRight : WriteProperty
                    AccessControlType : Allow
                    ObjectType : name [attributeSchema]
                    AdSecurityInheritance : Descendents
                    InheritedObjectType : computer [classSchema]
                    IsInherited : False

                    ACENumber : 6
                    Id : EguibarIT\XXX
                    LDAPpath : OU=InfraStaging,OU=Infra,OU=Admin,DC=EguibarIT,DC=local
                    AdRight : WriteProperty
                    AccessControlType : Allow
                    ObjectType : distinguishedName [attributeSchema]
                    AdSecurityInheritance : Descendents
                    InheritedObjectType : computer [classSchema]
                    IsInherited : False

                    ACENumber : 7
                    Id : EguibarIT\XXX
                    LDAPpath : OU=InfraStaging,OU=Infra,OU=Admin,DC=EguibarIT,DC=local
                    AdRight : DeleteChild
                    AccessControlType : Allow
                    ObjectType : computer [classSchema]
                    AdSecurityInheritance : None
                    InheritedObjectType : All [GuidNULL]
                    IsInherited : False
                #>


                } #end If-Else
            } #end If

            ####################
            # Set the necessary permissions on the domain controllers OU

            $Splat = @{
                Group    = $CurrentGroup
                LDAPPath = 'DC=Domain Controllers,{0}' -f $Variables.AdDN
            }

            # Create/Delete Computers
            Set-AdAclCreateDeleteComputer @Splat

            # Reset Computer Password
            Set-AdAclResetComputerPassword @Splat

            # Change Computer Password
            Set-AdAclChangeComputerPassword @Splat

            # Validated write to DNS host name
            Set-AdAclValidateWriteDnsHostName @Splat

            # Validated write to SPN
            Set-AdAclValidateWriteSPN @Splat

            # Change Computer Account Restriction
            Set-AdAclComputerAccountRestriction @Splat

            # Change DNS Hostname Info
            Set-AdAclDnsInfo @Splat

            # Change MS TerminalServices info
            Set-AdAclMsTsGatewayInfo @Splat

            # Access to BitLocker & TMP info
            Set-AdAclBitLockerTPM @Splat

        } #end If

    } #end Process

    End {

        if ($RemoveRule) {
            Write-Verbose ('Permissions removal process completed for group: {0}' -f $PSBoundParameters['Group'])
        } else {
            Write-Verbose ('Permissions delegation process completed for group: {0}' -f $PSBoundParameters['Group'])
        } #end If-Else

        $txt = ($Variables.FooterDelegation -f $MyInvocation.InvocationName,
            'delegating DCPromote.'
        )
        Write-Verbose -Message $txt
    } #end END
} #end Function Set-AdAclPromoteDomain