diff --git a/CHANGELOG.md b/CHANGELOG.md index b0b2571b6..78bfacfa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ For older change log history see the [historic changelog](HISTORIC_CHANGELOG.md) - ADFineGrainedPasswordPolicy - New resource for creating and updating Fine Grained Password Policies for AD principal subjects. ([issue #584](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/584)). +- ADDomainController + - Added support to demote domain controller when `ensure` is set to `Absent`. + ([issue #251](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/251)) ### Changed diff --git a/source/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.psm1 b/source/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.psm1 index c0a26c6a3..b4d5e9bd1 100644 --- a/source/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.psm1 +++ b/source/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.psm1 @@ -79,10 +79,10 @@ function Get-TargetResource $allowedPasswordReplicationAccountName = ( Get-ADDomainControllerPasswordReplicationPolicy -Allowed -Identity $domainControllerObject | - ForEach-Object -MemberName sAMAccountName) + ForEach-Object -MemberName sAMAccountName) $deniedPasswordReplicationAccountName = ( Get-ADDomainControllerPasswordReplicationPolicy -Denied -Identity $domainControllerObject | - ForEach-Object -MemberName sAMAccountName) + ForEach-Object -MemberName sAMAccountName) $serviceNTDS = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters' $serviceNETLOGON = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters' $installDns = [System.Boolean](Get-Service -Name dns -ErrorAction SilentlyContinue) @@ -146,6 +146,9 @@ function Get-TargetResource .PARAMETER SafemodeAdministratorPassword Provide a password that will be used to set the DSRM password. This is a PSCredential. + .PARAMETER Ensure + Specifies if the node will be configured as a domain controller + .PARAMETER DatabasePath Provide the path where the NTDS.dit will be created and stored. @@ -191,6 +194,8 @@ function Get-TargetResource Name | Module ---------------------------------------------------|-------------------------- Install-ADDSDomainController | ActiveDirectory + Test-ADDSDomainControllerUninstallation | ActiveDirectory + Uninstall-ADDSDomainController | ActiveDirectory Get-ADDomain | ActiveDirectory Get-ADForest | ActiveDirectory Set-ADObject | ActiveDirectory @@ -226,6 +231,11 @@ function Set-TargetResource [System.Management.Automation.PSCredential] $SafemodeAdministratorPassword, + [Parameter()] + [ValidateSet('Absent', 'Present')] + [System.String] + $Ensure = 'Present', + [Parameter()] [System.String] $DatabasePath, @@ -280,7 +290,59 @@ function Set-TargetResource $targetResource = Get-TargetResource @getTargetResourceParameters - if ($targetResource.Ensure -eq $false) + if ($targetResource.Ensure) + { + $ensureValue = 'Present' + } + else + { + $ensureValue = 'Absent' + } + + + if ($Ensure -eq 'Absent') + { + if ($targetResource.Ensure -eq $false) + { + break + } + + # Test to make sure the domain controller can be removed from the domain + $ADDSDomainUninstallationParameters = @{ + LocalAdministratorPassword = $SafemodeAdministratorPassword.Password + Credential = $Credential + NoRebootOnCompletion = $true + Force = $true + } + $testStatus = Test-ADDSDomainControllerUninstallation @ADDSDomainUninstallationParameters + + if ($testStatus.Status -eq 'Error') + { + New-InvalidOperationException -Message ($script:localizedData.TestDemoteStatus -f $testStatus.Status, $testStatus.Message) + } + elseif ($testStatus.Status -eq 'Success') + { + # No issues found that will cause issues demoting the domain controller + try + { + Uninstall-ADDSDomainController @ADDSDomainUninstallationParameters -ErrorAction Stop + Write-Verbose -Message ($script:localizedData.Demoted -f $env:COMPUTERNAME) + } + catch + { + $errorMessage = $script:localizedData.FailedToDemote + New-InvalidResultException -Message $errorMessage -ErrorRecord $_ + } + <# + Signal to the LCM to reboot the node to compensate for the one we + suppressed from Uninstall-ADDSDomainController + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Set LCM DSCMachineStatus to indicate reboot required')] + $global:DSCMachineStatus = 1 + } + } + elseif ($targetResource.Ensure -eq $false) { Write-Verbose -Message ($script:localizedData.Promoting -f $env:COMPUTERNAME, $DomainName) @@ -560,6 +622,9 @@ function Set-TargetResource .PARAMETER SafemodeAdministratorPassword Provide a password that will be used to set the DSRM password. This is a PSCredential. + .PARAMETER Ensure + Specifies if the node will be configured as a domain controller + .PARAMETER DatabasePath Provide the path where the NTDS.dit will be created and stored. @@ -632,6 +697,11 @@ function Test-TargetResource [System.Management.Automation.PSCredential] $SafemodeAdministratorPassword, + [Parameter()] + [ValidateSet('Absent', 'Present')] + [System.String] + $Ensure = 'Present', + [Parameter()] [System.String] $DatabasePath, @@ -707,6 +777,22 @@ function Test-TargetResource $testTargetResourceReturnValue = $existingResource.Ensure + if ($testTargetResourceReturnValue) + { + $ensureValue = 'Present' + } + else + { + $ensureValue = 'Absent' + } + + + if ($ensureValue -ne $Ensure) + { + Write-Verbose -Message ($script:localizedData.EnsureMismatch -f $ensureValue, $Ensure) + $testTargetResourceReturnValue = $false + } + if ($PSBoundParameters.ContainsKey('ReadOnlyReplica') -and $ReadOnlyReplica) { if ($testTargetResourceReturnValue -and -not $existingResource.ReadOnlyReplica) @@ -795,6 +881,15 @@ function Test-TargetResource } } + <# + If the node is not a domain controller and ensure is set to Absent we need to return + True as it will fail if other options are set on the resource. + #> + if ($ensureValue -eq 'Absent' -and $Ensure -eq 'Absent') + { + $testTargetResourceReturnValue = $true + } + return $testTargetResourceReturnValue } diff --git a/source/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.schema.mof b/source/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.schema.mof index e235028c6..eef324b6f 100644 --- a/source/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.schema.mof +++ b/source/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.schema.mof @@ -10,7 +10,7 @@ class MSFT_ADDomainController : OMI_BaseResource [Write, Description("The name of the site this Domain Controller will be added to.")] String SiteName; [Write, Description("The path of the media you want to use install the Domain Controller.")] String InstallationMediaPath; [Write, Description("Specifies if the domain controller will be a Global Catalog (GC).")] Boolean IsGlobalCatalog; - [Read, Description("Returns the state of the Domain Controller.")] String Ensure; + [Write, Description("Specifies the state of the Domain Controller."), ValueMap{"Absent", "Present"}, Values{"Absent", "Present"}] String Ensure; [Write, Description("Indicates that the cmdlet installs the domain controller as an Read-Only Domain Controller (RODC) for an existing domain.")] Boolean ReadOnlyReplica; [Write, Description("Specifies an array of names of user accounts, group accounts, and computer accounts whose passwords can be replicated to this Read-Only Domain Controller (RODC).")] String AllowPasswordReplicationAccountName[]; [Write, Description("Specifies the names of user accounts, group accounts, and computer accounts whose passwords are not to be replicated to this Read-Only Domain Controller (RODC).")] String DenyPasswordReplicationAccountName[]; diff --git a/source/DSCResources/MSFT_ADDomainController/en-US/MSFT_ADDomainController.strings.psd1 b/source/DSCResources/MSFT_ADDomainController/en-US/MSFT_ADDomainController.strings.psd1 index 278c5b538..0003e8bf1 100644 --- a/source/DSCResources/MSFT_ADDomainController/en-US/MSFT_ADDomainController.strings.psd1 +++ b/source/DSCResources/MSFT_ADDomainController/en-US/MSFT_ADDomainController.strings.psd1 @@ -20,4 +20,8 @@ ConvertFrom-StringData @' CannotConvertToRODC = Cannot convert a existing domain controller to a Read-Only Domain Controller (RODC). (ADDC0023) NotOwnerOfFlexibleSingleMasterOperationRole = The domain controller was expected to be the owner of the Flexible Single Master Operation (FSMO) role '{0}', but it is not. (ADDC0024) MovingFlexibleSingleMasterOperationRole = The Flexible Single Master Operation (FSMO) role '{0}' is being moved from domain controller '{1}' to this domain controller. (ADDC0025) + EnsureMismatch = The current domain controller ensure does not match. Got {0}, expected was {1}. (ADDC0026) + TestDemoteStatus = Received status: {0} with message {0}. (ADDC0027) + Demoted = The current node '{0}' has been demoted from a domain controller to a member server. (ADDC0028) + FailedToDemote = Failed to demote the domain controller. (ADDC0029) '@ diff --git a/source/DSCResources/MSFT_ADDomainController/en-US/about_ADDomainController.help.txt b/source/DSCResources/MSFT_ADDomainController/en-US/about_ADDomainController.help.txt index cea22f990..18c11e64f 100644 --- a/source/DSCResources/MSFT_ADDomainController/en-US/about_ADDomainController.help.txt +++ b/source/DSCResources/MSFT_ADDomainController/en-US/about_ADDomainController.help.txt @@ -454,4 +454,62 @@ Configuration ADDomainController_AddDomainControllerUsingInstallDns_Config } } +.EXAMPLE 7 + +This configuration will demote an exiting domain controller from the domain contoso.com. + +Configuration ADDomainController_DemoteDomainController_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $SafeModePassword + ) + + Import-DscResource -ModuleName PSDesiredStateConfiguration + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + WindowsFeature 'InstallADDomainServicesFeature' + { + Ensure = 'Present' + Name = 'AD-Domain-Services' + } + + WindowsFeature 'RSATADPowerShell' + { + Ensure = 'Present' + Name = 'RSAT-AD-PowerShell' + + DependsOn = '[WindowsFeature]InstallADDomainServicesFeature' + } + + WaitForADDomain 'WaitForestAvailability' + { + DomainName = 'contoso.com' + Credential = $Credential + + DependsOn = '[WindowsFeature]RSATADPowerShell' + } + + ADDomainController 'DemoteDomainController' + { + Ensure = 'Absent' + DomainName = 'contoso.com' + Credential = $Credential + SafeModeAdministratorPassword = $SafeModePassword + + DependsOn = '[WaitForADDomain]WaitForestAvailability' + } + } +} + diff --git a/source/Examples/Resources/ADDomainController/7-ADDomainController_DemoteDomainController_Config.ps1 b/source/Examples/Resources/ADDomainController/7-ADDomainController_DemoteDomainController_Config.ps1 new file mode 100644 index 000000000..1413eab37 --- /dev/null +++ b/source/Examples/Resources/ADDomainController/7-ADDomainController_DemoteDomainController_Config.ps1 @@ -0,0 +1,75 @@ +<#PSScriptInfo +.VERSION 1.0.1 +.GUID 50ee1ed8-2e39-4194-93f5-ceff18443e13 +.AUTHOR DSC Community +.COMPANYNAME DSC Community +.COPYRIGHT DSC Community contributors. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/dsccommunity/ActiveDirectoryDsc/blob/master/LICENSE +.PROJECTURI https://github.com/dsccommunity/ActiveDirectoryDsc +.ICONURI https://dsccommunity.org/images/DSC_Logo_300p.png +.RELEASENOTES +Updated author, copyright notice, and URLs. +#> + +#Requires -Module ActiveDirectoryDsc + +<# + .DESCRIPTION + This configuration will demote an exiting domain controller from the domain contoso.com. +#> + + +Configuration ADDomainController_DemoteDomainController_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $SafeModePassword + ) + + Import-DscResource -ModuleName PSDesiredStateConfiguration + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + WindowsFeature 'InstallADDomainServicesFeature' + { + Ensure = 'Present' + Name = 'AD-Domain-Services' + } + + WindowsFeature 'RSATADPowerShell' + { + Ensure = 'Present' + Name = 'RSAT-AD-PowerShell' + + DependsOn = '[WindowsFeature]InstallADDomainServicesFeature' + } + + WaitForADDomain 'WaitForestAvailability' + { + DomainName = 'contoso.com' + Credential = $Credential + + DependsOn = '[WindowsFeature]RSATADPowerShell' + } + + ADDomainController 'DemoteDomainController' + { + Ensure = 'Absent' + DomainName = 'contoso.com' + Credential = $Credential + SafeModeAdministratorPassword = $SafeModePassword + + DependsOn = '[WaitForADDomain]WaitForestAvailability' + } + } +} diff --git a/tests/Unit/MSFT_ADDomainController.Tests.ps1 b/tests/Unit/MSFT_ADDomainController.Tests.ps1 index 6296b1ff3..5a011b0b5 100644 --- a/tests/Unit/MSFT_ADDomainController.Tests.ps1 +++ b/tests/Unit/MSFT_ADDomainController.Tests.ps1 @@ -461,6 +461,24 @@ try $result | Should -Be $true } + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 + } + } + Context 'When property Ensure is set to "Absent" and in desired state' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DomainName = $correctDomainName + Ensure = $false + } + } + } + It 'Should return $true' { + $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -Ensure 'Absent' + $result | Should -Be $true + } + It 'Should call the expected mocks' { Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 } @@ -492,6 +510,25 @@ try Assert-MockCalled -CommandName Test-ADReplicationSite -Exactly -Times 0 } } + Context 'When ensure is not in desired state' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DomainName = $correctDomainName + Ensure = $True + } + } + } + + It 'Should return $false' { + $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -Ensure 'Absent' + $result | Should -Be $false + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 + } + } Context 'When properties are not in desired state' { Context 'When property SiteName is not in desired state' { @@ -1183,6 +1220,84 @@ try Assert-MockCalled -CommandName Move-ADDirectoryServerOperationMasterRole -Exactly -Times 1 } } + Context 'When demoting a domain controller' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + Ensure = $true + } + } + Mock -CommandName Uninstall-ADDSDomainController -MockWith { + return @{ + Status = 'Success' + } + } + } + Context 'When domain controller cannot be demoted' { + BeforeAll { + Mock -CommandName Test-ADDSDomainControllerUninstallation -MockWith { + return @{ + Status = 'Error' + Message = 'mock message' + } + } + } + It 'Should throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName ` + -Ensure 'Absent' } | Should -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Test-ADDSDomainControllerUninstallation ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Uninstall-ADDSDomainController ` + -Exactly -Times 0 + } + } + Context 'When domain controller fails to be demoted' { + BeforeAll { + Mock -CommandName Test-ADDSDomainControllerUninstallation -MockWith { + return @{ + Status = 'Success' + Message = 'Operation completed successfully' + } + } + Mock -CommandName Uninstall-ADDSDomainController -MockWith { throw } + } + It 'Should throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName ` + -Ensure 'Absent' } | Should -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Test-ADDSDomainControllerUninstallation ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Uninstall-ADDSDomainController ` + -Exactly -Times 1 + } + } + Context 'When domain controller is demoted' { + BeforeAll { + Mock -CommandName Test-ADDSDomainControllerUninstallation -MockWith { + return @{ + Status = 'Success' + Message = 'Operation completed successfully' + } + } + } + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName ` + -Ensure 'Absent' } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Test-ADDSDomainControllerUninstallation ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Uninstall-ADDSDomainController ` + -Exactly -Times 1 + } + } + } } Context 'When the system is in the desired state' {