Skip to content

Commit

Permalink
Added support to demote domain controller dsccommunity#251
Browse files Browse the repository at this point in the history
  • Loading branch information
limiteddenial committed Nov 17, 2020
1 parent 87b1308 commit fb8d5c5
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 4 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ For older change log history see the [historic changelog](HISTORIC_CHANGELOG.md)
## [Unreleased]

### Added

- ADGroup
- Added support for managing AD group membership of Foreign Security Principals. This involved completely
refactoring group membership management to utilize the `Set-ADGroup` cmdlet and referencing SID values.
([issue #619](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/619)).
- 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -191,6 +191,8 @@ function Get-TargetResource
Name | Module
---------------------------------------------------|--------------------------
Install-ADDSDomainController | ActiveDirectory
Test-ADDSDomainControllerUninstallation | ActiveDirectory
Uninstall-ADDSDomainController | ActiveDirectory
Get-ADDomain | ActiveDirectory
Get-ADForest | ActiveDirectory
Set-ADObject | ActiveDirectory
Expand Down Expand Up @@ -226,6 +228,11 @@ function Set-TargetResource
[System.Management.Automation.PSCredential]
$SafemodeAdministratorPassword,

[Parameter()]
[ValidateSet('Absent', 'Present')]
[System.String]
$Ensure = 'Present',

[Parameter()]
[System.String]
$DatabasePath,
Expand Down Expand Up @@ -280,7 +287,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)

Expand Down Expand Up @@ -632,6 +691,11 @@ function Test-TargetResource
[System.Management.Automation.PSCredential]
$SafemodeAdministratorPassword,

[Parameter()]
[ValidateSet('Absent', 'Present')]
[System.String]
$Ensure = 'Present',

[Parameter()]
[System.String]
$DatabasePath,
Expand Down Expand Up @@ -707,6 +771,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)
Expand Down Expand Up @@ -795,6 +875,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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
'@
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
}


115 changes: 115 additions & 0 deletions tests/Unit/MSFT_ADDomainController.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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' {
Expand Down Expand Up @@ -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' {
Expand Down

0 comments on commit fb8d5c5

Please sign in to comment.