From 7c3feec1973788377b3ace90b7e0cfdc7aebb0cb Mon Sep 17 00:00:00 2001 From: Chris Hill <53898223+Borgquite@users.noreply.github.com> Date: Sun, 18 Aug 2024 12:31:39 +0100 Subject: [PATCH] ADReadOnlyDomainControllerAccount: New resource (#713) ### Added - ADDomainController - New parameter UseExistingAccount for attaching a server to an existing RODC account (issue #711). - ADReadOnlyDomainControllerAccount - New resource for pre-creating Read Only Domain Controller accounts (issue #40 and issue #711). ### Fixed - ActiveDirectoryDsc.Common - Fixed Get-DomainControllerObject to allow checking non-local domain controller accounts. - Update build process to pin GitVersion to 5.* to resolve errors (issue #477). --- CHANGELOG.md | 17 + azure-pipelines.yml | 2 +- source/ActiveDirectoryDsc.psd1 | 1 + .../MSFT_ADDomainController.psm1 | 50 +- .../MSFT_ADDomainController.schema.mof | 3 +- .../MSFT_ADDomainController/README.md | 18 +- .../MSFT_ADDomainController.strings.psd1 | 2 +- .../en-US/about_ADDomainController.help.txt | 71 +- ...SFT_ADReadOnlyDomainControllerAccount.psm1 | 692 ++++++++++ ...ReadOnlyDomainControllerAccount.schema.mof | 14 + .../README.md | 12 + ...adOnlyDomainControllerAccount.strings.psd1 | 21 + ...ADReadOnlyDomainControllerAccount.help.txt | 165 +++ .../ActiveDirectoryDsc.Common.psm1 | 11 +- ...ainControllerAccount.Integration.Tests.ps1 | 85 ++ ...ReadOnlyDomainControllerAccount.config.ps1 | 50 + .../Unit/ActiveDirectoryDsc.Common.Tests.ps1 | 34 +- tests/Unit/MSFT_ADDomainController.Tests.ps1 | 35 +- ...DReadOnlyDomainControllerAccount.Tests.ps1 | 1208 +++++++++++++++++ 19 files changed, 2461 insertions(+), 30 deletions(-) create mode 100644 source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/MSFT_ADReadOnlyDomainControllerAccount.psm1 create mode 100644 source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/MSFT_ADReadOnlyDomainControllerAccount.schema.mof create mode 100644 source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/README.md create mode 100644 source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/en-US/MSFT_ADReadOnlyDomainControllerAccount.strings.psd1 create mode 100644 source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/en-US/about_ADReadOnlyDomainControllerAccount.help.txt create mode 100644 tests/Integration/MSFT_ADReadOnlyDomainControllerAccount.Integration.Tests.ps1 create mode 100644 tests/Integration/MSFT_ADReadOnlyDomainControllerAccount.config.ps1 create mode 100644 tests/Unit/MSFT_ADReadOnlyDomainControllerAccount.Tests.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 42db179f1..1e634da55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,23 @@ For older change log history see the [historic changelog](HISTORIC_CHANGELOG.md) ## [Unreleased] +### Added + +- ADDomainController + - New parameter UseExistingAccount for attaching a server to an existing RODC account. + ([issue #711](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/711)). +- ADReadOnlyDomainControllerAccount + - New resource for pre-creating Read Only Domain Controller accounts. + ([issue #40](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/40)) + ([issue #711](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/711)). + +### Fixed + +- ActiveDirectoryDsc.Common + - Fixed Get-DomainControllerObject to allow checking non-local domain controller accounts. +- Update build process to pin GitVersion to 5.* to resolve errors + (https://github.com/gaelcolas/Sampler/issues/477). + ## [6.5.0] - 2024-05-17 ### Added diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6b70c1d82..6b8ea2488 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -28,7 +28,7 @@ stages: vmImage: 'windows-latest' steps: - pwsh: | - dotnet tool install --global GitVersion.Tool + dotnet tool install --global GitVersion.Tool --version 5.* $gitVersionObject = dotnet-gitversion | ConvertFrom-Json $gitVersionObject.PSObject.Properties.ForEach{ Write-Host -Object "Setting Task Variable '$($_.Name)' with value '$($_.Value)'." diff --git a/source/ActiveDirectoryDsc.psd1 b/source/ActiveDirectoryDsc.psd1 index d9dd1f1f1..a0dff3d0a 100644 --- a/source/ActiveDirectoryDsc.psd1 +++ b/source/ActiveDirectoryDsc.psd1 @@ -62,6 +62,7 @@ DscResourcesToExport = @( 'ADObjectPermissionEntry' 'ADOptionalFeature' 'ADOrganizationalUnit' + 'ADReadOnlyDomainControllerAccount' 'ADReplicationSite' 'ADReplicationSiteLink' 'ADServicePrincipalName' diff --git a/source/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.psm1 b/source/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.psm1 index b6c45381d..f0054dc28 100644 --- a/source/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.psm1 +++ b/source/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.psm1 @@ -24,6 +24,11 @@ $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' .PARAMETER SafemodeAdministratorPassword Provide a password that will be used to set the DSRM password. This is a PSCredential. + .PARAMETER UseExistingAccount + Specifies whether to use an existing read only domain controller account. + + Not used in Get-TargetResource. + .NOTES Used Functions: Name | Module @@ -50,7 +55,11 @@ function Get-TargetResource [Parameter(Mandatory = $true)] [System.Management.Automation.PSCredential] - $SafemodeAdministratorPassword + $SafemodeAdministratorPassword, + + [Parameter()] + [System.Boolean] + $UseExistingAccount ) Assert-Module -ModuleName 'ActiveDirectory' @@ -79,11 +88,13 @@ function Get-TargetResource $delegateAdministratorAccountName = $null if ($domainControllerObject.IsReadOnly) { - $domainControllerComputerObject = $domainControllerObject.ComputerObjectDN | Get-ADComputer -Properties ManagedBy -Credential $Credential + $domainControllerComputerObject = $domainControllerObject.ComputerObjectDN | + Get-ADComputer -Properties ManagedBy -Credential $Credential if ($domainControllerComputerObject.ManagedBy) { - $domainControllerManagedByObject = $domainControllerComputerObject.ManagedBy | Get-ADObject -Properties objectSid -Credential $Credential - + $domainControllerManagedByObject = $domainControllerComputerObject.ManagedBy | + Get-ADObject -Properties objectSid -Credential $Credential + $delegateAdministratorAccountName = Resolve-SamAccountName -ObjectSid $domainControllerManagedByObject.objectSid } } @@ -115,6 +126,7 @@ function Get-TargetResource SafemodeAdministratorPassword = $SafemodeAdministratorPassword SiteName = $domainControllerObject.Site SysvolPath = $serviceNETLOGON.SysVol -replace '\\sysvol$', '' + UseExistingAccount = $UseExistingAccount } } else @@ -138,6 +150,7 @@ function Get-TargetResource SafemodeAdministratorPassword = $SafemodeAdministratorPassword SiteName = $null SysvolPath = $null + UseExistingAccount = $UseExistingAccount } } @@ -202,6 +215,10 @@ function Get-TargetResource The parameter `InstallDns` is only used during the provisioning of a domain controller. The parameter cannot be used to install or uninstall the DNS server on an already provisioned domain controller. + + .PARAMETER UseExistingAccount + Specifies whether to use an existing read only domain controller account. + .NOTES Used Functions: Name | Module @@ -289,7 +306,11 @@ function Set-TargetResource [Parameter()] [System.Boolean] - $InstallDns + $InstallDns, + + [Parameter()] + [System.Boolean] + $UseExistingAccount ) $getTargetResourceParameters = @{ @@ -395,6 +416,11 @@ function Set-TargetResource $installADDSDomainControllerParameters.Add('InstallDns', $InstallDns) } + if ($PSBoundParameters.ContainsKey('UseExistingAccount')) + { + $installADDSDomainControllerParameters.Add('UseExistingAccount', $UseExistingAccount) + } + if (-not [System.String]::IsNullOrWhiteSpace($InstallationMediaPath)) { $installADDSDomainControllerParameters.Add('InstallationMediaPath', $InstallationMediaPath) @@ -464,7 +490,8 @@ function Set-TargetResource $delegateAdministratorAccountSecurityIdentifier = Resolve-SecurityIdentifier -SamAccountName $DelegatedAdministratorAccountName - Set-ADComputer -Identity $domainControllerObject.ComputerObjectDN -ManagedBy $delegateAdministratorAccountSecurityIdentifier -Credential $Credential + Set-ADComputer -Identity $domainControllerObject.ComputerObjectDN ` + -ManagedBy $delegateAdministratorAccountSecurityIdentifier -Credential $Credential } } @@ -671,6 +698,11 @@ function Set-TargetResource Not used in Test-TargetResource. + .PARAMETER UseExistingAccount + Specifies whether to use an existing read only domain controller account. + + Not used in Test-TargetResource. + .NOTES Used Functions: Name | Module @@ -747,7 +779,11 @@ function Test-TargetResource [Parameter()] [System.Boolean] - $InstallDns + $InstallDns, + + [Parameter()] + [System.Boolean] + $UseExistingAccount ) Write-Verbose -Message ($script:localizedData.TestingConfiguration -f $env:COMPUTERNAME, $DomainName) diff --git a/source/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.schema.mof b/source/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.schema.mof index a31e1acfa..54b147641 100644 --- a/source/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.schema.mof +++ b/source/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.schema.mof @@ -11,10 +11,11 @@ class MSFT_ADDomainController : OMI_BaseResource [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("Indicates that the cmdlet installs the domain controller as an Read-Only Domain Controller (RODC) for an existing domain.")] Boolean ReadOnlyReplica; + [Write, Description("Indicates that the resource installs the domain controller as an Read-Only Domain Controller (RODC) for an existing domain.")] Boolean ReadOnlyReplica; [Write, Description("Specifies the user or group that is the delegated administrator of this Read-Only Domain Controller (RODC).")] String DelegatedAdministratorAccountName; [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[]; [Write, Description("Specifies one or more Flexible Single Master Operation (FSMO) roles to move to this domain controller. The current owner must be online and responding for the move to be allowed."), ValueMap{"DomainNamingMaster", "SchemaMaster", "InfrastructureMaster", "PDCEmulator", "RIDMaster"}, Values{"DomainNamingMaster", "SchemaMaster", "InfrastructureMaster", "PDCEmulator", "RIDMaster"}] String FlexibleSingleMasterOperationRole[]; [Write, Description("Specifies if the DNS Server service should be installed and configured on the Domain Controller. If this is not set the default value of the parameter `InstallDns` of the cmdlet Install-ADDSDomainController is used. This parameter is only used during the provisioning of a domain controller. The parameter cannot be used to install or uninstall the DNS server on an already provisioned domain controller.")] Boolean InstallDns; + [Write, Description("Indicates that the resource attaches a server to an existing Read-Only Domain Controller (RODC) account. If specified, a member of the Domain Admins group or a delegated user can apply this resource.")] Boolean UseExistingAccount; }; diff --git a/source/DSCResources/MSFT_ADDomainController/README.md b/source/DSCResources/MSFT_ADDomainController/README.md index e82abc53e..18a258dd5 100644 --- a/source/DSCResources/MSFT_ADDomainController/README.md +++ b/source/DSCResources/MSFT_ADDomainController/README.md @@ -13,13 +13,13 @@ is used. The parameter `InstallDns` is only used during the provisioning of a domain controller. The parameter cannot be used to install or uninstall the DNS server on an already provisioned domain controller. ->**Note:** If the account used for the parameter `Credential` ->cannot connect to another domain controller, for example using a credential ->without the domain name, then the cmdlet `Install-ADDSDomainController` will ->seemingly halt (without reporting an error) when trying to replicate ->information from another domain controller. ->Make sure to use a correct domain account with the correct permission as ->the account for the parameter `Credential`. +> **Note:** If the account used for the parameter `Credential` +> cannot connect to another domain controller, for example using a credential +> without the domain name, then the cmdlet `Install-ADDSDomainController` will +> seemingly halt (without reporting an error) when trying to replicate +> information from another domain controller. +> Make sure to use a correct domain account with the correct permission as +> the account for the parameter `Credential`. The parameter `FlexibleSingleMasterOperationRole` is ignored until the node has been provisioned as a domain controller. Take extra care @@ -27,8 +27,8 @@ to make sure the Flexible Single Master Operation (FSMO) roles are moved accordingly to avoid that two domain controller try to get to be the owner of the same role (potential "ping-pong"-behavior). ->The resource does not support seizing of Flexible Single Master Operation ->(FSMO) roles +> The resource does not support seizing of Flexible Single Master Operation +> (FSMO) roles ## Requirements 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 83dd96bdc..af106d7e1 100644 --- a/source/DSCResources/MSFT_ADDomainController/en-US/MSFT_ADDomainController.strings.psd1 +++ b/source/DSCResources/MSFT_ADDomainController/en-US/MSFT_ADDomainController.strings.psd1 @@ -1,6 +1,6 @@ ConvertFrom-StringData @' ResolveDomainName = Resolving the domain name '{0}'. (ADDC0001) - DomainPresent = The domain '{0}' is present. Looking for domain controllers. (ADDC0002) + DomainPresent = The domain '{0}' is present. Looking for domain controller. (ADDC0002) NotDomainController = The current node '{0}' is not a domain controller. (ADDC0006) IsDomainController = The current node '{0}' is a domain controller for the domain '{1}'. (ADDC0007) MissingDomain = Current node could not find the domain '{0}'. (ADDC0008) 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 f1e16f5fc..55f61425e 100644 --- a/source/DSCResources/MSFT_ADDomainController/en-US/about_ADDomainController.help.txt +++ b/source/DSCResources/MSFT_ADDomainController/en-US/about_ADDomainController.help.txt @@ -78,7 +78,7 @@ .PARAMETER ReadOnlyReplica Write - Boolean - Indicates that the cmdlet installs the domain controller as an Read-Only Domain Controller (RODC) for an existing domain. + Indicates that the resource installs the domain controller as an Read-Only Domain Controller (RODC) for an existing domain. .PARAMETER DelegatedAdministratorAccountName Write - String @@ -101,6 +101,10 @@ Write - Boolean Specifies if the DNS Server service should be installed and configured on the Domain Controller. If this is not set the default value of the parameter `InstallDns` of the cmdlet Install-ADDSDomainController is used. This parameter is only used during the provisioning of a domain controller. The parameter cannot be used to install or uninstall the DNS server on an already provisioned domain controller. +.PARAMETER UseExistingAccount + Write - Boolean + Indicates that the resource attaches a server to an existing Read-Only Domain Controller (RODC) account. If specified, a member of the Domain Admins group or a delegated user can apply this resource. + .EXAMPLE 1 This configuration will add a domain controller to the domain @@ -284,7 +288,7 @@ Configuration ADDomainController_AddDomainControllerToDomainUsingIFM_Config .EXAMPLE 4 This configuration will add a read-only domain controller to the domain contoso.com -and specify a list of account, whose passwords are allowed/denied for synchronisation. +and specify a list of accounts, whose passwords are allowed/denied for synchronisation. Configuration ADDomainController_AddReadOnlyDomainController_Config { @@ -346,6 +350,65 @@ Configuration ADDomainController_AddReadOnlyDomainController_Config .EXAMPLE 5 +This configuration will add a read-only domain controller to the domain contoso.com +with a delegated credential, using an existing read only domain controller account. + +Configuration ADDomainController_AddReadOnlyDomainControllerExistingAccount_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $DelegatedCredential, + + [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 = $DelegatedCredential + + DependsOn = '[WindowsFeature]RSATADPowerShell' + } + + ADDomainController 'Read-OnlyDomainController(RODC)ExistingAccount' + { + DomainName = 'contoso.com' + Credential = $DelegatedCredential + SafeModeAdministratorPassword = $SafeModePassword + UseExistingAccount = $true + + DependsOn = '[WaitForADDomain]WaitForestAvailability' + } + } +} + +.EXAMPLE 6 + This configuration will add a domain controller to the domain contoso.com, and when the configuration is enforced it will move the Flexible Single Master Operation (FSMO) role @@ -405,7 +468,7 @@ Configuration ADDomainController_AddDomainControllerAndMoveRole_Config } } -.EXAMPLE 6 +.EXAMPLE 7 This configuration will add a domain controller to the domain contoso.com without installing the local DNS server service and using the one in the existing domain. @@ -458,5 +521,3 @@ Configuration ADDomainController_AddDomainControllerUsingInstallDns_Config } } } - - diff --git a/source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/MSFT_ADReadOnlyDomainControllerAccount.psm1 b/source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/MSFT_ADReadOnlyDomainControllerAccount.psm1 new file mode 100644 index 000000000..2aa8261a2 --- /dev/null +++ b/source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/MSFT_ADReadOnlyDomainControllerAccount.psm1 @@ -0,0 +1,692 @@ +$resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$modulesFolderPath = Join-Path -Path $resourceModulePath -ChildPath 'Modules' + +$aDCommonModulePath = Join-Path -Path $modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name $aDCommonModulePath + +$dscResourceCommonModulePath = Join-Path -Path $modulesFolderPath -ChildPath 'DscResource.Common' +Import-Module -Name $dscResourceCommonModulePath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current state of the read only domain controller account. + + .PARAMETER DomainControllerAccountName + Provide the name of the Read Domain Controller Account which will be created. + + .PARAMETER DomainName + Provide the FQDN of the domain the Read Domain Controller Account is being created in. + + .PARAMETER Credential + Specifies the credential for the account used to add the read only domain controller account. + + .PARAMETER SiteName + Provide the name of the site you want the Read Only Domain Controller Account to be added to. + + .PARAMETER InstallDns + Specifies if the DNS Server service should be installed and configured on + the read only domain controller. If this is not set the default value of the parameter + InstallDns of the cmdlet Add-ADDSReadOnlyDomainControllerAccount is used. + The parameter `InstallDns` is only used during the provisioning of a read only domain + controller. The parameter cannot be used to install or uninstall the DNS + server on an already provisioned read only domain controller. + + Not used in Get-TargetResource. + + .NOTES + Used Functions: + Name | Module + ------------------------------------------------|-------------------------- + Get-ADDomain | ActiveDirectory + Get-ADDomainControllerPasswordReplicationPolicy | ActiveDirectory + Get-DomainControllerObject | ActiveDirectoryDsc.Common + Assert-Module | DscResource.Common + New-ObjectNotFoundException | DscResource.Common +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainControllerAccountName, + + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [System.String] + $SiteName, + + [Parameter()] + [System.Boolean] + $InstallDns + ) + + Assert-Module -ModuleName 'ActiveDirectory' + + Write-Verbose -Message ($script:localizedData.ResolveDomainName -f $DomainName) + + $Domain = Get-DomainObject -Identity $DomainName -Credential $Credential -ErrorOnUnexpectedExceptions -Verbose:$VerbosePreference + + if (-not $Domain) + { + $errorMessage = $script:localizedData.MissingDomain -f $DomainName + New-ObjectNotFoundException -Message $errorMessage + } + + Write-Verbose -Message ($script:localizedData.DomainPresent -f $DomainName) + + $domainControllerObject = Get-DomainControllerObject ` + -DomainName $DomainName -ComputerName $DomainControllerAccountName -Credential $Credential + + if ($domainControllerObject.IsReadOnly) + { + Write-Verbose -Message ($script:localizedData.IsReadOnlyDomainControllerAccount -f + $domainControllerObject.Name, $domainControllerObject.Domain) + + # Retrieve any user or group that is a delegated administrator via the ManagedBy attribute + $delegateAdministratorAccountName = $null + $domainControllerComputerObject = $domainControllerObject.ComputerObjectDN | + Get-ADComputer -Properties ManagedBy -Credential $Credential + if ($domainControllerComputerObject.ManagedBy) + { + $domainControllerManagedByObject = $domainControllerComputerObject.ManagedBy | + Get-ADObject -Properties objectSid -Credential $Credential + + $delegateAdministratorAccountName = Resolve-SamAccountName -ObjectSid $domainControllerManagedByObject.objectSid + } + + $allowedPasswordReplicationAccountName = ( + Get-ADDomainControllerPasswordReplicationPolicy -Allowed -Identity $domainControllerObject | + ForEach-Object -MemberName sAMAccountName) + $deniedPasswordReplicationAccountName = ( + Get-ADDomainControllerPasswordReplicationPolicy -Denied -Identity $domainControllerObject | + ForEach-Object -MemberName sAMAccountName) + + $targetResource = @{ + AllowPasswordReplicationAccountName = @($allowedPasswordReplicationAccountName) + Credential = $Credential + DelegatedAdministratorAccountName = $delegateAdministratorAccountName + DenyPasswordReplicationAccountName = @($deniedPasswordReplicationAccountName) + DomainControllerAccountName = $domainControllerObject.Name + DomainName = $domainControllerObject.Domain + Ensure = $true + InstallDns = $InstallDns + IsGlobalCatalog = $domainControllerObject.IsGlobalCatalog + SiteName = $domainControllerObject.Site + } + } + else + { + Write-Verbose -Message ($script:localizedData.NotReadOnlyDomainControllerAccount -f + $domainControllerObject.Name, $domainControllerObject.Domain) + + $targetResource = @{ + AllowPasswordReplicationAccountName = $null + Credential = $Credential + DelegatedAdministratorAccountName = $null + DenyPasswordReplicationAccountName = $null + DomainControllerAccountName = $DomainControllerAccountName + DomainName = $DomainName + Ensure = $false + InstallDns = $false + IsGlobalCatalog = $false + SiteName = $null + } + } + + return $targetResource +} + +<# + .SYNOPSIS + Creates a read only domain controller account. + + .PARAMETER DomainControllerAccountName + Provide the name of the Read Domain Controller Account which will be created. + + .PARAMETER DomainName + Provide the FQDN of the domain the Read Domain Controller Account is being created in. + + .PARAMETER Credential + Specifies the credential for the account used to add the read only domain controller account. + + .PARAMETER SiteName + Provide the name of the site you want the Read Only Domain Controller Account to be added to. + + .PARAMETER IsGlobalCatalog + Specifies if the read only domain controller will be a Global Catalog (GC). + + .PARAMETER DelegatedAdministratorAccountName + Specifies the user or group that is the delegated administrator of this read only domain controller account. + + .PARAMETER AllowPasswordReplicationAccountName + Provides a list of the users, computers, and groups to add to the password replication allowed list. + + .PARAMETER DenyPasswordReplicationAccountName + Provides a list of the users, computers, and groups to add to the password replication denied list. + + .PARAMETER InstallDns + Specifies if the DNS Server service should be installed and configured on + the read only domain controller. If this is not set the default value of the parameter + InstallDns of the cmdlet Add-ADDSReadOnlyDomainControllerAccount is used. + The parameter `InstallDns` is only used during the provisioning of a read only domain + controller. The parameter cannot be used to install or uninstall the DNS + server on an already provisioned read only domain controller. + + .NOTES + Used Functions: + Name | Module + ---------------------------------------------------|-------------------------- + Add-ADDSReadOnlyDomainControllerAccount | ActiveDirectory + Set-ADObject | ActiveDirectory + Move-ADDirectoryServer | ActiveDirectory + Remove-ADDomainControllerPasswordReplicationPolicy | ActiveDirectory + Add-ADDomainControllerPasswordReplicationPolicy | ActiveDirectory + Get-DomainControllerObject | ActiveDirectoryDsc.Common + Get-DomainObject | ActiveDirectoryDsc.Common + New-InvalidOperationException | DscResource.Common +#> +function Set-TargetResource +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '', + Justification = 'Read-Only Domain Controller (RODC) Account Creation support(AllowPasswordReplicationAccountName and DenyPasswordReplicationAccountName)')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainControllerAccountName, + + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [System.String] + $SiteName, + + [Parameter()] + [System.Boolean] + $IsGlobalCatalog, + + [Parameter()] + [System.String] + $DelegatedAdministratorAccountName, + + [Parameter()] + [System.String[]] + $AllowPasswordReplicationAccountName, + + [Parameter()] + [System.String[]] + $DenyPasswordReplicationAccountName, + + [Parameter()] + [System.Boolean] + $InstallDns + ) + + $getTargetResourceParameters = @{ + DomainControllerAccountName = $DomainControllerAccountName + DomainName = $DomainName + Credential = $Credential + SiteName = $SiteName + } + + $targetResource = Get-TargetResource @getTargetResourceParameters + + if ($targetResource.Ensure -eq $false) + { + Write-Verbose -Message ($script:localizedData.Adding -f $DomainControllerAccountName, $DomainName) + + # Read only domain controller is not created so we add it. + $addADDSReadOnlyDomainControllerAccountParameters = @{ + DomainControllerAccountName = $DomainControllerAccountName + DomainName = $DomainName + Credential = $Credential + SiteName = $SiteName + Force = $true + } + + if ($PSBoundParameters.ContainsKey('DelegatedAdministratorAccountName')) + { + $addADDSReadOnlyDomainControllerAccountParameters.Add('DelegatedAdministratorAccountName', + $DelegatedAdministratorAccountName) + } + + if ($PSBoundParameters.ContainsKey('AllowPasswordReplicationAccountName')) + { + $addADDSReadOnlyDomainControllerAccountParameters.Add('AllowPasswordReplicationAccountName', + $AllowPasswordReplicationAccountName) + } + + if ($PSBoundParameters.ContainsKey('DenyPasswordReplicationAccountName')) + { + $addADDSReadOnlyDomainControllerAccountParameters.Add('DenyPasswordReplicationAccountName', + $DenyPasswordReplicationAccountName) + } + + if ($PSBoundParameters.ContainsKey('IsGlobalCatalog') -and $IsGlobalCatalog -eq $false) + { + $addADDSReadOnlyDomainControllerAccountParameters.Add('NoGlobalCatalog', $true) + } + + if ($PSBoundParameters.ContainsKey('InstallDns')) + { + $addADDSReadOnlyDomainControllerAccountParameters.Add('InstallDns', $InstallDns) + } + + Add-ADDSReadOnlyDomainControllerAccount @addADDSReadOnlyDomainControllerAccountParameters + + Write-Verbose -Message ($script:localizedData.Added -f $DomainControllerAccountName, $DomainName) + } + elseif ($targetResource.Ensure) + { + # Read only domain controller account already created. We check if other properties are in desired state + + Write-Verbose -Message ($script:localizedData.IsReadOnlyDomainControllerAccount -f $DomainControllerAccountName, $DomainName) + + $domainControllerObject = Get-DomainControllerObject ` + -DomainName $DomainName -ComputerName $DomainControllerAccountName -Credential $Credential + + # Check if Node Global Catalog state is correct + if ($PSBoundParameters.ContainsKey('IsGlobalCatalog') -and + $targetResource.IsGlobalCatalog -ne $IsGlobalCatalog) + { + # RODC is not in the expected Global Catalog state + if ($IsGlobalCatalog) + { + $globalCatalogOptionValue = 1 + + Write-Verbose -Message $script:localizedData.AddGlobalCatalog + } + else + { + $globalCatalogOptionValue = 0 + + Write-Verbose -Message $script:localizedData.RemoveGlobalCatalog + } + + Set-ADObject -Identity $domainControllerObject.NTDSSettingsObjectDN -Replace @{ + options = $globalCatalogOptionValue + } + } + + if ($targetResource.SiteName -ne $SiteName) + { + # RODC is not in correct site. Move it. + Write-Verbose -Message ($script:localizedData.MovingDomainController -f + $targetResource.SiteName, $SiteName) + + Move-ADDirectoryServer -Identity $DomainControllerAccountName -Site $SiteName -Credential $Credential + } + + if ($PSBoundParameters.ContainsKey('DelegatedAdministratorAccountName') -and + $targetResource.DelegatedAdministratorAccountName -ne $DelegatedAdministratorAccountName) + { + # Set the delegated administrator via the ManagedBy attribute + Write-Verbose -Message ($script:localizedData.UpdatingDelegatedAdministratorAccountName -f + $targetResource.DelegatedAdministratorAccountName, $DelegatedAdministratorAccountName) + + $delegateAdministratorAccountSecurityIdentifier = Resolve-SecurityIdentifier -SamAccountName $DelegatedAdministratorAccountName + + Set-ADComputer -Identity $domainControllerObject.ComputerObjectDN ` + -ManagedBy $delegateAdministratorAccountSecurityIdentifier -Credential $Credential + } + + if ($PSBoundParameters.ContainsKey('AllowPasswordReplicationAccountName')) + { + $testMembersParameters = @{ + ExistingMembers = $targetResource.AllowPasswordReplicationAccountName + Members = $AllowPasswordReplicationAccountName + } + + if (-not (Test-Members @testMembersParameters)) + { + Write-Verbose -Message ( + $script:localizedData.AllowedSyncAccountsMismatch -f + ($targetResource.AllowPasswordReplicationAccountName -join ';'), + ($AllowPasswordReplicationAccountName -join ';') + ) + + $getMembersToAddAndRemoveParameters = @{ + DesiredMembers = $AllowPasswordReplicationAccountName + CurrentMembers = $targetResource.AllowPasswordReplicationAccountName + } + + $getMembersToAddAndRemoveResult = Get-MembersToAddAndRemove @getMembersToAddAndRemoveParameters + + $adPrincipalsToRemove = $getMembersToAddAndRemoveResult.MembersToRemove + $adPrincipalsToAdd = $getMembersToAddAndRemoveResult.MembersToAdd + + if ($null -ne $adPrincipalsToRemove) + { + $removeADPasswordReplicationPolicy = @{ + Identity = $domainControllerObject + AllowedList = $adPrincipalsToRemove + } + + Remove-ADDomainControllerPasswordReplicationPolicy @removeADPasswordReplicationPolicy ` + -Confirm:$false + } + + if ($null -ne $adPrincipalsToAdd) + { + $addADPasswordReplicationPolicy = @{ + Identity = $domainControllerObject + AllowedList = $adPrincipalsToAdd + } + + Add-ADDomainControllerPasswordReplicationPolicy @addADPasswordReplicationPolicy + } + } + } + + if ($PSBoundParameters.ContainsKey('DenyPasswordReplicationAccountName')) + { + $testMembersParameters = @{ + ExistingMembers = $targetResource.DenyPasswordReplicationAccountName + Members = $DenyPasswordReplicationAccountName; + } + + if (-not (Test-Members @testMembersParameters)) + { + Write-Verbose -Message ( + $script:localizedData.DenySyncAccountsMismatch -f + ($targetResource.DenyPasswordReplicationAccountName -join ';'), + ($DenyPasswordReplicationAccountName -join ';') + ) + + $getMembersToAddAndRemoveParameters = @{ + DesiredMembers = $DenyPasswordReplicationAccountName + CurrentMembers = $targetResource.DenyPasswordReplicationAccountName + } + + $getMembersToAddAndRemoveResult = Get-MembersToAddAndRemove @getMembersToAddAndRemoveParameters + + $adPrincipalsToRemove = $getMembersToAddAndRemoveResult.MembersToRemove + $adPrincipalsToAdd = $getMembersToAddAndRemoveResult.MembersToAdd + + if ($null -ne $adPrincipalsToRemove) + { + $removeADPasswordReplicationPolicy = @{ + Identity = $domainControllerObject + DeniedList = $adPrincipalsToRemove + } + + Remove-ADDomainControllerPasswordReplicationPolicy @removeADPasswordReplicationPolicy ` + -Confirm:$false + } + + if ($null -ne $adPrincipalsToAdd) + { + $addADPasswordReplicationPolicy = @{ + Identity = $domainControllerObject + DeniedList = $adPrincipalsToAdd + } + + Add-ADDomainControllerPasswordReplicationPolicy @addADPasswordReplicationPolicy + } + + } + } + } +} + +<# + .SYNOPSIS + Determines if the read only domain controller account is in desired state. + + .PARAMETER DomainControllerAccountName + Provide the name of the Read Domain Controller Account which will be created. + + .PARAMETER DomainName + Provide the FQDN of the domain the Read Domain Controller Account is being created in. + + .PARAMETER Credential + Specifies the credential for the account used to add the read only domain controller account. + + .PARAMETER SiteName + Provide the name of the site you want the Read Only Domain Controller Account to be added to. + + .PARAMETER IsGlobalCatalog + Specifies if the read only domain controller will be a Global Catalog (GC). + + .PARAMETER DelegatedAdministratorAccountName + Specifies the user or group that is the delegated administrator of this read only domain controller account. + + .PARAMETER AllowPasswordReplicationAccountName + Provides a list of the users, computers, and groups to add to the password replication allowed list. + + .PARAMETER DenyPasswordReplicationAccountName + Provides a list of the users, computers, and groups to add to the password replication denied list. + + .PARAMETER InstallDns + Specifies if the DNS Server service should be installed and configured on + the read only domain controller. If this is not set the default value of the parameter + InstallDns of the cmdlet Add-ADDSReadOnlyDomainControllerAccount is used. + The parameter `InstallDns` is only used during the provisioning of a read only domain + controller. The parameter cannot be used to install or uninstall the DNS + server on an already provisioned read only domain controller. + + Not used in Test-TargetResource. + + .NOTES + Used Functions: + Name | Module + ------------------------------|-------------------------- + Test-ADReplicationSite | ActiveDirectoryDsc.Common + Test-Members | ActiveDirectoryDsc.Common + New-InvalidOperationException | DscResource.Common + New-ObjectNotFoundException | DscResource.Common +#> +function Test-TargetResource +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "", + Justification = 'Read-Only Domain Controller (RODC) Account Creation support(AllowPasswordReplicationAccountName and DenyPasswordReplicationAccountName)')] + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainControllerAccountName, + + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [System.String] + $SiteName, + + [Parameter()] + [System.Boolean] + $IsGlobalCatalog, + + [Parameter()] + [System.String] + $DelegatedAdministratorAccountName, + + [Parameter()] + [System.String[]] + $AllowPasswordReplicationAccountName, + + [Parameter()] + [System.String[]] + $DenyPasswordReplicationAccountName, + + [Parameter()] + [System.Boolean] + $InstallDns + ) + + Write-Verbose -Message ($script:localizedData.TestingConfiguration -f $DomainControllerAccountName, $DomainName) + + if (-not (Test-ADReplicationSite -SiteName $SiteName -DomainName $DomainName -Credential $Credential)) + { + $errorMessage = $script:localizedData.FailedToFindSite -f $SiteName, $DomainName + New-ObjectNotFoundException -Message $errorMessage + } + + $getTargetResourceParameters = @{ + DomainControllerAccountName = $DomainControllerAccountName + DomainName = $DomainName + Credential = $Credential + SiteName = $SiteName + } + + $existingResource = Get-TargetResource @getTargetResourceParameters + + $testTargetResourceReturnValue = $existingResource.Ensure + + if ($existingResource.SiteName -ne $SiteName) + { + Write-Verbose -Message ($script:localizedData.WrongSite -f $existingResource.SiteName, $SiteName) + + $testTargetResourceReturnValue = $false + } + + # Check Global Catalog Config + if ($PSBoundParameters.ContainsKey('IsGlobalCatalog') -and $existingResource.IsGlobalCatalog -ne $IsGlobalCatalog) + { + if ($IsGlobalCatalog) + { + Write-Verbose -Message ($script:localizedData.ExpectedGlobalCatalogEnabled) + } + else + { + Write-Verbose -Message ($script:localizedData.ExpectedGlobalCatalogDisabled) + } + + $testTargetResourceReturnValue = $false + } + + if ($PSBoundParameters.ContainsKey('DelegatedAdministratorAccountName') -and $existingResource.DelegatedAdministratorAccountName -ne $DelegatedAdministratorAccountName) + { + Write-Verbose -Message ($script:localizedData.DelegatedAdministratorAccountNameMismatch -f $existingResource.DelegatedAdministratorAccountName, $DelegatedAdministratorAccountName) + + $testTargetResourceReturnValue = $false + } + + if ($PSBoundParameters.ContainsKey('AllowPasswordReplicationAccountName') -and + $null -ne $existingResource.AllowPasswordReplicationAccountName) + { + $testMembersParameters = @{ + ExistingMembers = $existingResource.AllowPasswordReplicationAccountName + Members = $AllowPasswordReplicationAccountName + } + + if (-not (Test-Members @testMembersParameters)) + { + Write-Verbose -Message ( + $script:localizedData.AllowedSyncAccountsMismatch -f + ($existingResource.AllowPasswordReplicationAccountName -join ';'), + ($AllowPasswordReplicationAccountName -join ';') + ) + + $testTargetResourceReturnValue = $false + } + } + + if ($PSBoundParameters.ContainsKey('DenyPasswordReplicationAccountName') -and + $null -ne $existingResource.DenyPasswordReplicationAccountName) + { + $testMembersParameters = @{ + ExistingMembers = $existingResource.DenyPasswordReplicationAccountName + Members = $DenyPasswordReplicationAccountName; + } + + if (-not (Test-Members @testMembersParameters)) + { + Write-Verbose -Message ( + $script:localizedData.DenySyncAccountsMismatch -f + ($existingResource.DenyPasswordReplicationAccountName -join ';'), + ($DenyPasswordReplicationAccountName -join ';') + ) + + $testTargetResourceReturnValue = $false + } + } + + return $testTargetResourceReturnValue +} + +<# + .SYNOPSIS + Return a hashtable with members that are not present in CurrentMembers, + and members that are present add should not be present. + + .PARAMETER DesiredMembers + Specifies the list of desired members in the hashtable. + + .PARAMETER CurrentMembers + Specifies the list of current members in the hashtable. + + .OUTPUTS + Returns a hashtable with two properties. The property MembersToAdd contains the + members as ADPrincipal objects that are not members in the collection + provided in $CurrentMembers. The property MembersToRemove contains the + unwanted members as ADPrincipal objects in the collection provided + in $CurrentMembers. +#> +function Get-MembersToAddAndRemove +{ + param + ( + [Parameter(Mandatory = $true)] + [AllowNull()] + [AllowEmptyCollection()] + [System.String[]] + $DesiredMembers, + + [Parameter(Mandatory = $true)] + [AllowNull()] + [AllowEmptyCollection()] + [System.String[]] + $CurrentMembers + ) + + $principalsToRemove = foreach ($memberName in $CurrentMembers) + { + if ($memberName -notin $DesiredMembers) + { + New-Object -TypeName Microsoft.ActiveDirectory.Management.ADPrincipal -ArgumentList $memberName + } + } + + $principalsToAdd = foreach ($memberName in $DesiredMembers) + { + if ($memberName -notin $CurrentMembers) + { + New-Object -TypeName Microsoft.ActiveDirectory.Management.ADPrincipal -ArgumentList $memberName + } + } + + return @{ + MembersToAdd = [Microsoft.ActiveDirectory.Management.ADPrincipal[]] $principalsToAdd + MembersToRemove = [Microsoft.ActiveDirectory.Management.ADPrincipal[]] $principalsToRemove + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/MSFT_ADReadOnlyDomainControllerAccount.schema.mof b/source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/MSFT_ADReadOnlyDomainControllerAccount.schema.mof new file mode 100644 index 000000000..1cd1cabd3 --- /dev/null +++ b/source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/MSFT_ADReadOnlyDomainControllerAccount.schema.mof @@ -0,0 +1,14 @@ +[ClassVersion("1.0.1.0"), FriendlyName("ADReadOnlyDomainControllerAccount")] +class MSFT_ADReadOnlyDomainControllerAccount : OMI_BaseResource +{ + [Key, Description("The name of the Read Only Domain Controller Account which will be created.")] String DomainControllerAccountName; + [Key, Description("The fully qualified domain name (FQDN) of the domain the Read Only Domain Controller will be created in.")] String DomainName; + [Required, Description("The credentials (as a 'PSCredential' object) of a user that has Domain Administrator rights to add the Read Only Domain Controller Account to the domain."), EmbeddedInstance("MSFT_Credential")] String Credential; + [Required, Description("The name of the site this Read Only Domain Controller Account will be added to.")] String SiteName; + [Write, Description("Specifies if the read only domain controller will be a Global Catalog (GC).")] Boolean IsGlobalCatalog; + [Read, Description("Returns the state of the Read Only Domain Controller Account.")] String Ensure; + [Write, Description("Specifies the user or group that is the delegated administrator of this Read-Only Domain Controller (RODC) Account.")] String DelegatedAdministratorAccountName; + [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) Account.")] 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) Account.")] String DenyPasswordReplicationAccountName[]; + [Write, Description("Specifies if the DNS Server service should be installed and configured on the Read Only Domain Controller. If this is not set the default value of the parameter `InstallDns` of the cmdlet Add-ADDSReadOnlyDomainControllerAccount is used. This parameter is only used during the provisioning of a read only domain controller. The parameter cannot be used to install or uninstall the DNS server on an already provisioned read only domain controller.")] Boolean InstallDns; +}; diff --git a/source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/README.md b/source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/README.md new file mode 100644 index 000000000..120f4bb7d --- /dev/null +++ b/source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/README.md @@ -0,0 +1,12 @@ +# Description + +The ADReadOnlyDomainControllerAccount DSC resource will pre-create a read only domain +controller account in Active Directory. This allows the account actually installing +the read only domain controller to use delegated administrative credentials suppled in +DelegatedAdministratorAccountName rather than requiring Domain Admins permissions. + +> The resource does not support removing pre-created Read Only Domain Controller accounts. + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. diff --git a/source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/en-US/MSFT_ADReadOnlyDomainControllerAccount.strings.psd1 b/source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/en-US/MSFT_ADReadOnlyDomainControllerAccount.strings.psd1 new file mode 100644 index 000000000..b0883d6c3 --- /dev/null +++ b/source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/en-US/MSFT_ADReadOnlyDomainControllerAccount.strings.psd1 @@ -0,0 +1,21 @@ +ConvertFrom-StringData @' + ResolveDomainName = Resolving the domain name '{0}'. (ADRODCA0001) + DomainPresent = The domain '{0}' is present. Looking for read only domain controller account. (ADRODCA0002) + NotReadOnlyDomainControllerAccount = The read only domain controller account '{0}' does not exist in the domain '{1}'. (ADRODCA0003) + IsReadOnlyDomainControllerAccount = The read only domain controller account '{0}' does exist in the domain '{1}'. (ADRODCA0004) + MissingDomain = Current node could not find the domain '{0}'. (ADRODCA0005) + Adding = Adding read only domain controller account '{0}' to the domain '{1}'. (ADRODCA0006) + Added = Added read only domain controller account '{0}' to the domain '{1}'. (ADRODCA007) + AddGlobalCatalog = Adding Global Catalog to the read only domain controller account. (ADRODCA008) + RemoveGlobalCatalog = Removing Global Catalog from the read only domain controller account. (ADRODCA009) + MovingDomainController = Moving Read Only Domain Controller account from site '{0}' to site '{1}'. (ADRODCA0010) + FailedToFindSite = The site '{0}' could not be found in the domain '{1}'. (ADRODCA0011) + TestingConfiguration = Determine the state of the read only domain controller account '{0}' in the domain '{1}'. (ADRODCA0012) + WrongSite = The read only domain controller account is in the site '{0}', but expected it to be in the site '{1}'. (ADRODCA0013) + ExpectedGlobalCatalogEnabled = The read only domain controller account does not have a Global Catalog, but it was expected to have a Global Catalog. (ADRODCA0014) + ExpectedGlobalCatalogDisabled = The read only domain controller account has a Global Catalog, but it was expected to not have a Global Catalog. (ADRODCA0015) + AllowedSyncAccountsMismatch = There is a mismatch in AllowPasswordReplicationAccountName list. Got {0}, expected was {1}. (ADRODCA0016) + DenySyncAccountsMismatch = There is a mismatch in DenyPasswordReplicationAccountName list. Got {0}, expected was {1}. (ADRODCA0017) + DelegatedAdministratorAccountNameMismatch = There is a mismatch in DelegatedAdministratorAccountName. Got {0}, expected was {1}. (ADRODCA0018) + UpdatingDelegatedAdministratorAccountName = Updating the DelegatedAdministratorAccountName from the name {0} to the name {1}. (ADRODCA0019) +'@ diff --git a/source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/en-US/about_ADReadOnlyDomainControllerAccount.help.txt b/source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/en-US/about_ADReadOnlyDomainControllerAccount.help.txt new file mode 100644 index 000000000..47224cf99 --- /dev/null +++ b/source/DSCResources/MSFT_ADReadOnlyDomainControllerAccount/en-US/about_ADReadOnlyDomainControllerAccount.help.txt @@ -0,0 +1,165 @@ +.NAME + ADReadOnlyDomainControllerAccount + +.DESCRIPTION + The ADReadOnlyDomainControllerAccount DSC resource will pre-create a read only domain + controller account in Active Directory. This allows the account actually installing + the read only domain controller to use delegated administrative credentials suppled in + DelegatedAdministratorAccountName rather than requiring Domain Admins permissions. + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + +.PARAMETER DomainControllerAccountName + Key - String + The name of the Read Only Domain Controller Account which will be created. + +.PARAMETER DomainName + Key - String + The fully qualified domain name (FQDN) of the domain the Read Only Domain Controller will be created in. + +.PARAMETER Credential + Required - PSCredential + The credentials (as a 'PSCredential' object) of a user that has Domain Administrator rights to add the Read Only Domain Controller Account to the domain. + +.PARAMETER SiteName + Required - String + The name of the site this Read Only Domain Controller Account will be added to. + +.PARAMETER IsGlobalCatalog + Write - Boolean + Specifies if the read only domain controller will be a Global Catalog (GC). + +.PARAMETER Ensure + Read - String + Returns the state of the Read Only Domain Controller Account. + +.PARAMETER DelegatedAdministratorAccountName + Write - String + Specifies the user or group that is the delegated administrator of this Read-Only Domain Controller (RODC) Account. + +.PARAMETER AllowPasswordReplicationAccountName + Write - StringArray + 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) Account. + +.PARAMETER DenyPasswordReplicationAccountName + Write - StringArray + 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) Account. + +.PARAMETER InstallDns + Write - Boolean + Specifies if the DNS Server service should be installed and configured on the Read Only Domain Controller. If this is not set the default value of the parameter `InstallDns` of the cmdlet Add-ADDSReadOnlyDomainControllerAccount is used. This parameter is only used during the provisioning of a read only domain controller. The parameter cannot be used to install or uninstall the DNS server on an already provisioned read only domain controller. + +.EXAMPLE 1 + +This configuration will add a read only domain controller account to the domain +contoso.com. + +Configuration ADReadOnlyDomainControllerAccount_Minimal_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + 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' + } + + ADReadOnlyDomainControllerAccount 'ReadOnlyDomainControllerAccountMinimal' + { + DomainControllerAccountName = 'RODC01' + DomainName = 'contoso.com' + Credential = $Credential + SiteName = 'Default-First-Site-Name' + + DependsOn = '[WaitForADDomain]WaitForestAvailability' + } + } +} + +.EXAMPLE 2 + +This configuration will add a read only domain controller account to the domain +contoso.com, specifying all properties of the resource. + +Configuration ADReadOnlyDomainControllerAccount_AllProperties_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + 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' + } + + ADReadOnlyDomainControllerAccount 'ReadOnlyDomainControllerAccountAllProperties' + { + DomainControllerAccountName = 'RODC01' + DomainName = 'contoso.com' + Credential = $Credential + SiteName = 'Default-First-Site-Name' + IsGlobalCatalog = $true + DelegatedAdministratorAccountName = 'contoso\adm.pvdi' + AllowPasswordReplicationAccountName = @('pvdi.test1', 'pvdi.test') + DenyPasswordReplicationAccountName = @('SVC_PVS', 'TA2SCVMM') + InstallDns = $true + + DependsOn = '[WaitForADDomain]WaitForestAvailability' + } + } +} diff --git a/source/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psm1 b/source/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psm1 index e0ae97647..53384adb7 100644 --- a/source/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psm1 +++ b/source/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psm1 @@ -1351,10 +1351,15 @@ function Get-DomainControllerObject $domainControllerObject = Get-ADDomainController @getADDomainControllerParameters - if (-not $domainControllerObject -and (Test-IsDomainController) -eq $true) + # If we are getting the domain controller object for the local computer + if ($env:COMPUTERNAME -eq $ComputerName) { - $errorMessage = $script:localizedData.WasExpectingDomainController - New-InvalidResultException -Message $errorMessage + # If we can't find the object but the computer is a domain controller, throw an exception + if (-not $domainControllerObject -and (Test-IsDomainController) -eq $true) + { + $errorMessage = $script:localizedData.WasExpectingDomainController + New-InvalidResultException -Message $errorMessage + } } } catch diff --git a/tests/Integration/MSFT_ADReadOnlyDomainControllerAccount.Integration.Tests.ps1 b/tests/Integration/MSFT_ADReadOnlyDomainControllerAccount.Integration.Tests.ps1 new file mode 100644 index 000000000..cd09f4c76 --- /dev/null +++ b/tests/Integration/MSFT_ADReadOnlyDomainControllerAccount.Integration.Tests.ps1 @@ -0,0 +1,85 @@ +$script:dscModuleName = 'ActiveDirectoryDsc' +$script:dscResourceFriendlyName = 'ADReadOnlyDomainControllerAccount' +$script:dscResourceName = "MSFT_$($script:dscResourceFriendlyName)" + +try +{ + Import-Module -Name DscResource.Test -Force -ErrorAction 'Stop' +} +catch [System.IO.FileNotFoundException] +{ + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -Tasks build" first.' +} + +$script:testEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:dscModuleName ` + -DSCResourceName $script:dscResourceName ` + -ResourceType 'Mof' ` + -TestType 'Integration' + +try +{ + $configFile = Join-Path -Path $PSScriptRoot -ChildPath "$($script:dscResourceName).config.ps1" + . $configFile + + Describe "$($script:dscResourceName)_Integration" { + BeforeAll { + $resourceId = "[$($script:dscResourceFriendlyName)]Integration_Test" + } + + $configurationName = "$($script:dscResourceName)_CreateReadOnlyDomainControllerAccount_Config" + + Context ('When using configuration {0}' -f $configurationName) { + It 'Should compile and apply the MOF without throwing' { + { + $configurationParameters = @{ + OutputPath = $TestDrive + # The variable $ConfigurationData was dot-sourced above. + ConfigurationData = $ConfigurationData + } + + & $configurationName @configurationParameters + + $startDscConfigurationParameters = @{ + Path = $TestDrive + ComputerName = 'localhost' + Wait = $true + Verbose = $true + Force = $true + ErrorAction = 'Stop' + } + + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName ` + -and $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.DomainControllerAccountName | Should -Be $ConfigurationData.AllNodes.DomainControllerAccountName + $resourceCurrentState.DomainName | Should -Be $ConfigurationData.AllNodes.DomainName + $resourceCurrentState.SiteName | Should -Be $ConfigurationData.AllNodes.SiteName + $resourceCurrentState.Ensure | Should -Be 'Present' + } + + It 'Should return $true when Test-DscConfiguration is run' { + Test-DscConfiguration -Verbose | Should -Be 'True' + } + } + } +} +finally +{ + #region FOOTER + Restore-TestEnvironment -TestEnvironment $script:testEnvironment + #endregion +} diff --git a/tests/Integration/MSFT_ADReadOnlyDomainControllerAccount.config.ps1 b/tests/Integration/MSFT_ADReadOnlyDomainControllerAccount.config.ps1 new file mode 100644 index 000000000..46d9b47ed --- /dev/null +++ b/tests/Integration/MSFT_ADReadOnlyDomainControllerAccount.config.ps1 @@ -0,0 +1,50 @@ +#region HEADER +# Integration Test Config Template Version: 1.2.0 +#endregion + +$configFile = [System.IO.Path]::ChangeExtension($MyInvocation.MyCommand.Path, 'json') +if (Test-Path -Path $configFile) +{ + <# + Allows reading the configuration data from a JSON file, for real testing + scenarios outside of the CI. + #> + $ConfigurationData = Get-Content -Path $configFile | ConvertFrom-Json +} +else +{ + $currentDomain = Get-ADDomain + $dnsRoot = $currentDomain.DNSRoot + $currentSite = Get-ADReplicationSite + $siteName = $currentSite.Name + + $ConfigurationData = @{ + AllNodes = @( + @{ + NodeName = 'localhost' + DomainControllerAccountName = 'DSCINTTESTRODC1' + DomainName = $dnsRoot + SiteName = $siteName + } + ) + } +} + +<# + .SYNOPSIS + Pre-create a read only domain controller account. +#> +Configuration MSFT_ADKDSKey_CreateReadOnlyDomainControllerAccount +{ + Import-DscResource -ModuleName 'ActiveDirectoryDsc' + + node $AllNodes.NodeName + { + ADReadOnlyDomainControllerAccount 'Integration_Test' + { + DomainControllerAccountName = $Node.DomainControllerAccountName + DomainName = $Node.DomainName + SiteName = $Node.SiteName + } + } +} diff --git a/tests/Unit/ActiveDirectoryDsc.Common.Tests.ps1 b/tests/Unit/ActiveDirectoryDsc.Common.Tests.ps1 index 0ceeb1366..a1bc045cd 100644 --- a/tests/Unit/ActiveDirectoryDsc.Common.Tests.ps1 +++ b/tests/Unit/ActiveDirectoryDsc.Common.Tests.ps1 @@ -932,7 +932,7 @@ InModuleScope 'ActiveDirectoryDsc.Common' { } It 'Should return $null' { - $getDomainObjectResult = Get-DomainObject -Identity 'contoso.com' + $getDomainObjectResult = Get-DomainObject -Identity 'contoso.com' $getDomainObjectResult | Should -BeNullOrEmpty Assert-MockCalled -CommandName Get-ADDomain -Exactly -Times 1 -Scope It @@ -1129,6 +1129,38 @@ InModuleScope 'ActiveDirectoryDsc.Common' { } } + Context 'When the domain controller object is a remote computer and local computer is a domain controller' { + BeforeAll { + Mock -CommandName Get-ADDomainController -MockWith { + return @{ + Site = 'MySite' + Domain = 'contoso.com' + IsGlobalCatalog = $true + } + } + Mock -CommandName Test-IsDomainController -MockWith { + return $true + } + + $mockComputerName = "Mock-$($env:COMPUTERNAME)" + } + + It 'Should not throw and call the correct mocks' { + { Get-DomainControllerObject -DomainName 'contoso.com' -ComputerName $mockComputerName } | Should -Not -Throw + + Assert-MockCalled -CommandName Get-ADDomainController -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Test-IsDomainController -Exactly -Times 0 + } + + It 'Should return the correct values for each property' { + $getDomainControllerObjectResult = Get-DomainControllerObject -DomainName 'contoso.com' -ComputerName $mockComputerName + + $getDomainControllerObjectResult.Site | Should -Be 'MySite' + $getDomainControllerObjectResult.Domain | Should -Be 'contoso.com' + $getDomainControllerObjectResult.IsGlobalCatalog | Should -BeTrue + } + } + Context 'When current node is a domain controller' { BeforeAll { Mock -CommandName Get-ADDomainController -MockWith { diff --git a/tests/Unit/MSFT_ADDomainController.Tests.ps1 b/tests/Unit/MSFT_ADDomainController.Tests.ps1 index 49213d9a7..7165d5e79 100644 --- a/tests/Unit/MSFT_ADDomainController.Tests.ps1 +++ b/tests/Unit/MSFT_ADDomainController.Tests.ps1 @@ -74,7 +74,7 @@ try Mock -CommandName Assert-Module } - Context 'When the domain name is not available' { + Context 'When the domain could not be found' { BeforeAll { Mock -CommandName Get-DomainObject -MockWith { return $null @@ -136,6 +136,7 @@ try $result.DomainName | Should -Be $correctDomainName $result.InstallDns | Should -BeTrue + $result.UseExistingAccount | Should -BeFalse } It 'Should call the expected mocks' { @@ -185,6 +186,7 @@ try $result.DomainName | Should -Be $correctDomainName $result.InstallDns | Should -BeFalse + $result.UseExistingAccount | Should -BeFalse } It 'Should call the expected mocks' { @@ -259,7 +261,8 @@ try } It 'Should return the expected result' { - $result = Get-TargetResource @testDefaultParams -DomainName $correctDomainName + $result = Get-TargetResource @testDefaultParams -DomainName $correctDomainName ` + -UseExistingAccount $true $result.DomainName | Should -Be $correctDomainName $result.DatabasePath | Should -Be $correctDatabasePath @@ -274,6 +277,7 @@ try $result.AllowPasswordReplicationAccountName | Should -Be $allowedAccount $result.DenyPasswordReplicationAccountName | Should -Be $deniedAccount $result.InstallDns | Should -BeFalse + $result.UseExistingAccount | Should -BeTrue } It 'Should call the expected mocks' { @@ -333,6 +337,7 @@ try $result.DenyPasswordReplicationAccountName | Should -BeNullOrEmpty $result.FlexibleSingleMasterOperationRole | Should -BeNullOrEmpty $result.InstallDns | Should -BeFalse + $result.UseExistingAccount | Should -BeFalse } It 'Should call the expected mocks' { @@ -1107,6 +1112,32 @@ try } } + Context 'When the read only domain controller should use an existing account' { + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName ` + -UseExistingAccount $true } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Install-ADDSDomainController -ParameterFilter { + $UseExistingAccount -eq $true + } -Exactly -Times 1 + } + } + + Context 'When the read only domain controller should not use an existing account' { + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName ` + -UseExistingAccount $false } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Install-ADDSDomainController -ParameterFilter { + $UseExistingAccount -eq $false + } -Exactly -Times 1 + } + } + Context 'When a domain controller is in the wrong site' { BeforeAll { Mock -CommandName Move-ADDirectoryServer diff --git a/tests/Unit/MSFT_ADReadOnlyDomainControllerAccount.Tests.ps1 b/tests/Unit/MSFT_ADReadOnlyDomainControllerAccount.Tests.ps1 new file mode 100644 index 000000000..2abf991c9 --- /dev/null +++ b/tests/Unit/MSFT_ADReadOnlyDomainControllerAccount.Tests.ps1 @@ -0,0 +1,1208 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] +param () + +$script:dscModuleName = 'ActiveDirectoryDsc' +$script:dscResourceName = 'MSFT_ADReadOnlyDomainControllerAccount' + +function Invoke-TestSetup +{ + try + { + Import-Module -Name DscResource.Test -Force -ErrorAction 'Stop' + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -Tasks build" first.' + } + + $script:testEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:dscModuleName ` + -DSCResourceName $script:dscResourceName ` + -ResourceType 'Mof' ` + -TestType 'Unit' +} + +function Invoke-TestCleanup +{ + Restore-TestEnvironment -TestEnvironment $script:testEnvironment +} + +# Begin Testing + +Invoke-TestSetup + +try +{ + InModuleScope $script:dscResourceName { + Set-StrictMode -Version 1.0 + + # Load stub cmdlets and classes. + Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'Stubs\ActiveDirectory_2019.psm1') -Force + Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'Stubs\ADDSDeployment_2019.psm1') -Force + + #region Pester Test Variable Initialization + $domainControllerAccountName = 'RODC01' + $correctDomainName = 'present.com' + $testAdminCredential = [System.Management.Automation.PSCredential]::Empty + $correctSiteName = 'PresentSite' + $incorrectSiteName = 'IncorrectSite' + $mockNtdsSettingsObjectDn = 'CN=NTDS Settings,CN=ServerName,CN=Servers,CN=PresentSite,CN=Sites,CN=Configuration,DC=present,DC=com' + $delegatedAdminAccount = 'contoso\delegatedAdminAccount' + $delegatedAdminAccountSid = 'S-1-0-0' + $allowedAccount = 'allowedAccount' + $deniedAccount = 'deniedAccount' + + $testDefaultParams = @{ + DomainControllerAccountName = $domainControllerAccountName + Credential = $testAdminCredential + } + + #endregion Pester Test Variable Initialization + + #region Function Get-TargetResource + Describe 'ADReadOnlyDomainControllerAccount\Get-TargetResource' -Tag 'Get' { + BeforeAll { + Mock -CommandName Assert-Module + } + + Context 'When the domain could not be found' { + BeforeAll { + Mock -CommandName Get-DomainObject -MockWith { + return $null + } + } + + It 'Should throw the correct exception' { + { Get-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName } | + Should -Throw ($script:localizedData.MissingDomain -f $correctDomainName) + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Assert-Module ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-DomainObject ` + -Exactly -Times 1 + } + } + + Context 'When the system is in the desired state' { + BeforeAll { + Mock -CommandName Get-DomainObject -MockWith { $true } + Mock -CommandName Get-ADDomainControllerPasswordReplicationPolicy + } + + Context 'When the Read-Only Domain Controller account exists' { + BeforeAll { + $mockDomainControllerObject = New-Object ` + -TypeName Microsoft.ActiveDirectory.Management.ADDomainController + $mockDomainControllerComputerObject = New-Object ` + -TypeName Microsoft.ActiveDirectory.Management.ADComputer + $mockDomainControllerDelegatedAdminObject = New-Object ` + -TypeName Microsoft.ActiveDirectory.Management.ADObject + $mockDomainControllerObject.Name = $domainControllerAccountName + $mockDomainControllerObject.Site = $correctSiteName + $mockDomainControllerObject.Domain = $correctDomainName + $mockDomainControllerObject.IsGlobalCatalog = $true + $mockDomainControllerObject.IsReadOnly = $true + $mockDomainControllerDelegatedAdminObject.objectSid = $delegatedAdminAccountSid + $mockDomainControllerComputerObject.ManagedBy = $mockDomainControllerDelegatedAdminObject + $mockDomainControllerObject.ComputerObjectDN = $mockDomainControllerComputerObject + $mockGetADDomainControllerPasswordReplicationAllowedPolicy = @{ + SamAccountName = $allowedAccount + } + $mockGetADDomainControllerPasswordReplicationDeniedPolicy = @{ + SamAccountName = $deniedAccount + } + + Mock -CommandName Get-DomainControllerObject { $mockDomainControllerObject } + + Mock -CommandName Get-ADComputer { $mockDomainControllerComputerObject } + + Mock -CommandName Get-ADObject { $mockDomainControllerDelegatedAdminObject } + + Mock -CommandName Resolve-SamAccountName ` + -ParameterFilter { $ObjectSid -eq $delegatedAdminAccountSid } ` + -MockWith { $delegatedAdminAccount } + + Mock -CommandName Get-ADDomainControllerPasswordReplicationPolicy ` + -ParameterFilter { $Allowed.IsPresent } ` + -MockWith { $mockGetADDomainControllerPasswordReplicationAllowedPolicy } + + Mock -CommandName Get-ADDomainControllerPasswordReplicationPolicy ` + -ParameterFilter { $Denied.IsPresent } ` + -MockWith { $mockGetADDomainControllerPasswordReplicationDeniedPolicy } + } + + It 'Should return the expected result' { + $result = Get-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName + + $result.DomainControllerAccountName | Should -Be $domainControllerAccountName + $result.DomainName | Should -Be $correctDomainName + $result.SiteName | Should -Be $correctSiteName + $result.Ensure | Should -BeTrue + $result.IsGlobalCatalog | Should -BeTrue + $result.DelegatedAdministratorAccountName | Should -Be $delegatedAdminAccount + $result.AllowPasswordReplicationAccountName | Should -HaveCount 1 + $result.AllowPasswordReplicationAccountName | Should -Be $allowedAccount + $result.DenyPasswordReplicationAccountName | Should -Be $deniedAccount + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Assert-Module ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-DomainObject ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-DomainControllerObject ` + -ParameterFilter { $DomainName -eq $correctDomainName -and $ComputerName -eq $domainControllerAccountName } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ADComputer ` + -ParameterFilter { $Properties -eq 'ManagedBy' } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ADObject ` + -ParameterFilter { $Properties -eq 'objectSid' } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Resolve-SamAccountName ` + -ParameterFilter { $ObjectSid -eq $delegatedAdminAccountSid } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ADDomainControllerPasswordReplicationPolicy ` + -ParameterFilter { $Allowed -eq $true } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ADDomainControllerPasswordReplicationPolicy ` + -ParameterFilter { $Denied -eq $true } ` + -Exactly -Times 1 + } + } + + Context 'When the Read-Only Domain Controller account does not exist' { + BeforeAll { + Mock -CommandName Get-DomainControllerObject + } + + It 'Should return the expected result' { + $result = Get-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName + + $result.DomainControllerAccountName | Should -Be $domainControllerAccountName + $result.DomainName | Should -Be $correctDomainName + $result.SiteName | Should -BeNullOrEmpty + $result.Ensure | Should -BeFalse + $result.IsGlobalCatalog | Should -BeFalse + $result.DelegatedAdministratorAccountName | Should -BeNullOrEmpty + $result.AllowPasswordReplicationAccountName | Should -BeNullOrEmpty + $result.DenyPasswordReplicationAccountName | Should -BeNullOrEmpty + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Assert-Module ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-DomainObject ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-DomainControllerObject ` + -ParameterFilter { $DomainName -eq $correctDomainName -and $ComputerName -eq $domainControllerAccountName } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ADDomainControllerPasswordReplicationPolicy ` + -ParameterFilter { $Allowed -eq $true } ` + -Exactly -Times 0 + Assert-MockCalled -CommandName Get-ADDomainControllerPasswordReplicationPolicy ` + -ParameterFilter { $Denied -eq $true } ` + -Exactly -Times 0 + } + } + } + } + #endregion + + #region Function Test-TargetResource + Describe 'ADReadOnlyDomainControllerAccount\Test-TargetResource' -Tag 'Test' { + BeforeAll { + $mockGetADDomainControllerPasswordReplicationAllowedPolicy = @{ + SamAccountName = $allowedAccount + } + $mockGetADDomainControllerPasswordReplicationDeniedPolicy = @{ + SamAccountName = $deniedAccount + } + + Mock -CommandName Get-ADDomainControllerPasswordReplicationPolicy ` + -ParameterFilter { $Allowed.IsPresent } ` + -MockWith { $mockGetADDomainControllerPasswordReplicationAllowedPolicy } + + Mock -CommandName Get-ADDomainControllerPasswordReplicationPolicy ` + -ParameterFilter { $Denied.IsPresent } ` + -MockWith { $mockGetADDomainControllerPasswordReplicationDeniedPolicy } + } + + Context 'When the system is in the desired state' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DomainControllerAccountName = $domainControllerAccountName + DomainName = $correctDomainName + SiteName = $correctSiteName + IsGlobalCatalog = $true + DelegatedAdministratorAccountName = $delegatedAdminAccount + AllowPasswordReplicationAccountName = @($allowedAccount) + DenyPasswordReplicationAccountName = @($deniedAccount) + Ensure = $true + } + } + + Mock -CommandName Test-ADReplicationSite -MockWith { $true } + } + + Context 'When creating a read only domain controller account with only mandatory parameters' { + It 'Should return $true' { + $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName + $result | Should -BeTrue + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 + Assert-MockCalled -CommandName Test-ADReplicationSite -Exactly -Times 1 + } + } + + Context 'When property IsGlobalCatalog is in desired state' { + It 'Should return $true' { + $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -IsGlobalCatalog $true + $result | Should -BeTrue + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 + Assert-MockCalled -CommandName Test-ADReplicationSite -Exactly -Times 1 + } + } + + Context 'When property DelegatedAdministratorAccountName is in desired state' { + It 'Should return $true' { + $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -DelegatedAdministratorAccountName $delegatedAdminAccount + $result | Should -BeTrue + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 + Assert-MockCalled -CommandName Test-ADReplicationSite -Exactly -Times 1 + } + } + + Context 'When property AllowPasswordReplicationAccountName is in desired state' { + It 'Should return $true' { + $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -AllowPasswordReplicationAccountName @($allowedAccount) + $result | Should -BeTrue + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 + Assert-MockCalled -CommandName Test-ADReplicationSite -Exactly -Times 1 + } + } + + Context 'When property DenyPasswordReplicationAccountName is in desired state' { + It 'Should return $true' { + $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -DenyPasswordReplicationAccountName @($deniedAccount) + $result | Should -BeTrue + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 + Assert-MockCalled -CommandName Test-ADReplicationSite -Exactly -Times 1 + } + } + } + + Context 'When the system is not in the desired state' { + BeforeAll { + Mock -CommandName Test-ADReplicationSite -MockWith { $true } + } + + Context 'When creating a read only domain controller account with only mandatory parameters' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DomainControllerAccountName = $domainControllerAccountName + DomainName = 'WrongDomainName' + Ensure = $false + } + } + } + + It 'Should return $false' { + $result = Test-TargetResource @testDefaultParams -DomainName 'WrongDomainName' -SiteName $correctSiteName + $result | Should -BeFalse + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 + Assert-MockCalled -CommandName Test-ADReplicationSite -Exactly -Times 1 + } + } + + Context 'When properties are not in desired state' { + Context 'When property SiteName is not in desired state' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DomainControllerAccountName = $domainControllerAccountName + DomainName = $correctDomainName + SiteName = $correctSiteName + Ensure = $true + } + } + } + + It 'Should return $false' { + $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName ` + -SiteName 'NewSiteName' + $result | Should -BeFalse + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 + Assert-MockCalled -CommandName Test-ADReplicationSite -Exactly -Times 1 + } + } + + Context 'When property IsGlobalCatalog is not in desired state' { + Context 'When Read Only Domain Controller Account should be a Global Catalog' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DomainControllerAccountName = $domainControllerAccountName + DomainName = $correctDomainName + IsGlobalCatalog = $false + Ensure = $true + } + } + } + + It 'Should return $false' { + $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -IsGlobalCatalog $true + $result | Should -BeFalse + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 + Assert-MockCalled -CommandName Test-ADReplicationSite -Exactly -Times 1 + } + } + + Context 'When Read Only Domain Controller Account should not be a Global Catalog' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DomainControllerAccountName = $domainControllerAccountName + DomainName = $correctDomainName + IsGlobalCatalog = $true + Ensure = $true + } + } + } + + It 'Should return $false' { + $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -IsGlobalCatalog $false + $result | Should -BeFalse + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 + Assert-MockCalled -CommandName Test-ADReplicationSite -Exactly -Times 1 + } + } + } + + Context 'When property DelegatedAdministratorAccountName is not in desired state' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DomainControllerAccountName = $domainControllerAccountName + DomainName = $correctDomainName + DelegatedAdministratorAccountName = $delegatedAdminAccount + Ensure = $true + } + } + } + + It 'Should return $false' { + $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -DelegatedAdministratorAccountName 'contoso\NewDelegatedAdminAccount' + $result | Should -BeFalse + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 + Assert-MockCalled -CommandName Test-ADReplicationSite -Exactly -Times 1 + } + } + + Context 'When property AllowPasswordReplicationAccountName is not in desired state' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DomainControllerAccountName = $domainControllerAccountName + DomainName = $correctDomainName + AllowPasswordReplicationAccountName = @($allowedAccount, 'Member2') + Ensure = $true + } + } + } + + Context 'When there are different members than the desired state' { + It 'Should return $false' { + $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -AllowPasswordReplicationAccountName @('NewMember1', 'NewMember2') + $result | Should -BeFalse + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 + Assert-MockCalled -CommandName Test-ADReplicationSite -Exactly -Times 1 + } + } + + Context 'When there exist less members than the desired state' { + It 'Should return $false' { + $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -AllowPasswordReplicationAccountName @($allowedAccount, 'Member2', 'NewMember') + $result | Should -BeFalse + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 + Assert-MockCalled -CommandName Test-ADReplicationSite -Exactly -Times 1 + } + } + + Context 'When there exist more members that the desired state' { + It 'Should return $false' { + $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -AllowPasswordReplicationAccountName @($allowedAccount) + $result | Should -BeFalse + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 + Assert-MockCalled -CommandName Test-ADReplicationSite -Exactly -Times 1 + } + } + } + + Context 'When property DenyPasswordReplicationAccountName is not in desired state' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DomainControllerAccountName = $domainControllerAccountName + DomainName = $correctDomainName + DenyPasswordReplicationAccountName = @($deniedAccount, 'Member2') + Ensure = $true + } + } + } + + Context 'When there are different members than the desired state' { + It 'Should return $false' { + $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -DenyPasswordReplicationAccountName @('NewMember1', 'NewMember2') + $result | Should -BeFalse + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 + Assert-MockCalled -CommandName Test-ADReplicationSite -Exactly -Times 1 + } + } + + Context 'When there exist less members than the desired state' { + It 'Should return $false' { + $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -DenyPasswordReplicationAccountName @($allowedAccount, 'Member2', 'NewMember') + $result | Should -BeFalse + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 + Assert-MockCalled -CommandName Test-ADReplicationSite -Exactly -Times 1 + } + } + + Context 'When there exist more members that the desired state' { + It 'Should return $false' { + $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -DenyPasswordReplicationAccountName @($allowedAccount) + $result | Should -BeFalse + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 + Assert-MockCalled -CommandName Test-ADReplicationSite -Exactly -Times 1 + } + } + } + } + + Context 'When a specified site does not exist in the Active Directory' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DomainControllerAccountName = $domainControllerAccountName + DomainName = $correctDomainName + SiteName = $correctSiteName + Ensure = $true + } + } + + Mock -CommandName Test-ADReplicationSite -MockWith { + return $false + } + } + + It 'Should throw the correct error' { + { + Test-TargetResource @testDefaultParams -DomainName $correctDomainName ` + -SiteName $correctSiteName + } | Should -Throw ($script:localizedData.FailedToFindSite -f $correctSiteName, $correctDomainName) + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 0 + Assert-MockCalled -CommandName Test-ADReplicationSite -Exactly -Times 1 + } + } + } + } + #endregion + + #region Function Set-TargetResource + Describe 'ADReadOnlyDomainControllerAccount\Set-TargetResource' -Tag 'Set' { + Context 'When the system is not in the desired state' { + BeforeAll { + Mock -CommandName Add-ADDSReadOnlyDomainControllerAccount + Mock -CommandName Remove-ADDomainControllerPasswordReplicationPolicy + Mock -CommandName Add-ADDomainControllerPasswordReplicationPolicy + + Mock -CommandName Get-TargetResource -MockWith { + return @{ + Ensure = $false + } + } + } + + Context 'When adding a read only domain controller account that should not be a Global Catalog' { + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -IsGlobalCatalog $false } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Add-ADDSReadOnlyDomainControllerAccount -ParameterFilter { + $NoGlobalCatalog -eq $true + } -Exactly -Times 1 + } + } + + Context 'When adding a read only domain controller account with DelegatedAdministratorAccountName' { + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -DelegatedAdministratorAccountName $delegatedAdminAccount } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Add-ADDSReadOnlyDomainControllerAccount -ParameterFilter { + $DelegatedAdministratorAccountName -eq $delegatedAdminAccount + } -Exactly -Times 1 + } + } + + Context 'When adding a read only domain controller account with AllowPasswordReplicationAccountName' { + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -AllowPasswordReplicationAccountName $allowedAccount } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Add-ADDSReadOnlyDomainControllerAccount -ParameterFilter { + $AllowPasswordReplicationAccountName -eq $allowedAccount + } -Exactly -Times 1 + } + } + + Context 'When adding a read only domain controller account with DenyPasswordReplicationAccountName' { + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -DenyPasswordReplicationAccountName $deniedAccount } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Add-ADDSReadOnlyDomainControllerAccount -ParameterFilter { + $DenyPasswordReplicationAccountName -eq $deniedAccount + } -Exactly -Times 1 + } + } + + Context 'When the read only domain controller account should have a DNS installed' { + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -InstallDns $true } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Add-ADDSReadOnlyDomainControllerAccount -ParameterFilter { + $InstallDns -eq $true + } -Exactly -Times 1 + } + } + + Context 'When the read only domain controller account should not have a DNS installed' { + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -InstallDns $false } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Add-ADDSReadOnlyDomainControllerAccount -ParameterFilter { + $InstallDns -eq $false + } -Exactly -Times 1 + } + } + + Context 'When a read only domain controller account is in the wrong site' { + BeforeAll { + Mock -CommandName Move-ADDirectoryServer + Mock -CommandName Get-TargetResource -MockWith { + return @{ + Ensure = $true + SiteName = 'IncorrectSite' + } + } + + Mock -CommandName Get-DomainControllerObject -MockWith { + return (New-Object -TypeName Microsoft.ActiveDirectory.Management.ADDomainController) + } + } + + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Move-ADDirectoryServer -ParameterFilter { + $Site.ToString() -eq $correctSiteName + } -Exactly -Times 1 + } + } + + Context 'When specifying the IsGlobalCatalog parameter' { + BeforeAll { + Mock -CommandName Set-ADObject + Mock -CommandName Get-DomainControllerObject { + return @{ + NTDSSettingsObjectDN = $mockNtdsSettingsObjectDn + } + } + } + + Context 'When the read only domain controller account should be a Global Catalog' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return $stubTargetResource = @{ + Ensure = $true + SiteName = $correctSiteName + IsGlobalCatalog = $false + } + } + } + + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -IsGlobalCatalog $true } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Set-ADObject -ParameterFilter { + $Replace['options'] -eq 1 + } -Exactly -Times 1 + } + } + + Context 'When the read only domain controller account should not be a Global Catalog' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return $stubTargetResource = @{ + Ensure = $true + SiteName = $correctSiteName + IsGlobalCatalog = $true + } + } + } + + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -IsGlobalCatalog $false } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Set-ADObject -ParameterFilter { + $Replace['options'] -eq 0 + } -Exactly -Times 1 + } + } + + Context 'When the read only domain controller account should change state of Global Catalog, but fail to return a read only domain controller object' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return $stubTargetResource = @{ + Ensure = $true + SiteName = $correctSiteName + IsGlobalCatalog = $true + } + } + + Mock -CommandName Get-DomainControllerObject + } + + It 'Should throw the correct exception' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -IsGlobalCatalog $false } | Should -Throw $script:localizedData.ExpectedDomainController + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Set-ADObject -Exactly -Times 0 + } + } + } + + Context 'When DelegatedAdministratorAccountName is not compliant' { + Mock -CommandName Set-ADComputer + Mock -CommandName Resolve-SecurityIdentifier ` + -ParameterFilter { $SamAccountName -eq $delegatedAdminAccount } ` + -MockWith { $delegatedAdminAccountSid } + Mock -CommandName Get-TargetResource -MockWith { + return @{ + Ensure = $true + SiteName = $correctSiteName + DelegatedAdministratorAccountName = 'contoso\PresentDelegatedAdminAccount' + } + } + + Mock -CommandName Get-DomainControllerObject -MockWith { + $stubDomainController = New-Object ` + -TypeName Microsoft.ActiveDirectory.Management.ADDomainController + $stubDomainControllerComputerObject = New-Object ` + -TypeName Microsoft.ActiveDirectory.Management.ADComputer + $stubDomainController.ComputerObjectDN = $stubDomainControllerComputerObject + + return $stubDomainController + } + + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -DelegatedAdministratorAccountName $delegatedAdminAccount } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Resolve-SecurityIdentifier -ParameterFilter { + $SamAccountName -eq $delegatedAdminAccount + } -Exactly -Times 1 + Assert-MockCalled -CommandName Set-ADComputer -ParameterFilter { + $ManagedBy -eq $delegatedAdminAccountSid + } -Exactly -Times 1 + } + } + + Context 'When AllowPasswordReplicationAccountName is not compliant' { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + Ensure = $true + SiteName = $correctSiteName + AllowPasswordReplicationAccountName = 'allowedAccount2' + } + } + + Mock -CommandName Get-DomainControllerObject -MockWith { + $stubDomainController = New-Object ` + -TypeName Microsoft.ActiveDirectory.Management.ADDomainController + + return $stubDomainController + } + + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -AllowPasswordReplicationAccountName $allowedAccount } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Remove-ADDomainControllerPasswordReplicationPolicy ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Add-ADDomainControllerPasswordReplicationPolicy ` + -Exactly -Times 1 + } + } + + Context 'When DenyPasswordReplicationAccountName is not compliant' { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + Ensure = $true + SiteName = $correctSiteName + DenyPasswordReplicationAccountName = 'deniedAccount2' + } + } + + Mock -CommandName Get-DomainControllerObject -MockWith { + $stubDomainController = New-Object ` + -TypeName Microsoft.ActiveDirectory.Management.ADDomainController + return $stubDomainController + } + + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -DenyPasswordReplicationAccountName $deniedAccount } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Remove-ADDomainControllerPasswordReplicationPolicy ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Add-ADDomainControllerPasswordReplicationPolicy ` + -Exactly -Times 1 + } + } + } + + Context 'When the system is in the desired state' { + BeforeAll { + Mock -CommandName Remove-ADDomainControllerPasswordReplicationPolicy + Mock -CommandName Add-ADDomainControllerPasswordReplicationPolicy + } + + Context 'When the read only domain controller account is already in the correct site' { + BeforeAll { + Mock -CommandName Move-ADDirectoryServer + Mock -CommandName Get-TargetResource -MockWith { + return @{ + Ensure = $true + SiteName = $correctSiteName + } + } + Mock -CommandName Get-DomainControllerObject { + return (New-Object -TypeName Microsoft.ActiveDirectory.Management.ADDomainController) + } + } + + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Move-ADDirectoryServer -Exactly -Times 0 + } + } + + Context 'When specifying the IsGlobalCatalog parameter' { + BeforeAll { + Mock -CommandName Set-ADObject + Mock -CommandName Get-DomainControllerObject { + return @{ + NTDSSettingsObjectDN = $mockNtdsSettingsObjectDn + } + } + } + + Context 'When the read only domain controller account already is a Global Catalog' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return $stubTargetResource = @{ + Ensure = $true + SiteName = $correctSiteName + IsGlobalCatalog = $true + } + } + } + + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -IsGlobalCatalog $true } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Set-ADObject -Exactly -Times 0 + } + } + + Context 'When the read only domain controller account already is not a Global Catalog' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return $stubTargetResource = @{ + Ensure = $true + SiteName = $correctSiteName + IsGlobalCatalog = $false + } + } + } + + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -IsGlobalCatalog $false } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Set-ADObject -Exactly -Times 0 + } + } + } + + Context 'When DelegatedAdministratorAccountName is correct' { + BeforeAll { + Mock -CommandName Set-ADComputer + Mock -CommandName Resolve-SecurityIdentifier ` + -ParameterFilter { $SamAccountName -eq $delegatedAdminAccount } ` + -MockWith { $delegatedAdminAccountSid } + Mock -CommandName Get-DomainControllerObject -MockWith { + $stubDomainController = New-Object ` + -TypeName Microsoft.ActiveDirectory.Management.ADDomainController + $stubDomainControllerComputerObject = New-Object ` + -TypeName Microsoft.ActiveDirectory.Management.ADComputer + $stubDomainController.ComputerObjectDN = $stubDomainControllerComputerObject + + return $stubDomainController + } + + Mock -CommandName Get-TargetResource -MockWith { + return @{ + Ensure = $true + SiteName = $correctSiteName + DelegatedAdministratorAccountName = $delegatedAdminAccount + } + } + } + + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -DelegatedAdministratorAccountName $delegatedAdminAccount } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Resolve-SecurityIdentifier -ParameterFilter { + $SamAccountName -eq $delegatedAdminAccount + } -Exactly -Times 0 + Assert-MockCalled -CommandName Set-ADComputer -Exactly -Times 0 + } + } + + Context 'When AllowPasswordReplicationAccountName is correct' { + BeforeAll { + Mock -CommandName Get-DomainControllerObject -MockWith { + $stubDomainController = New-Object ` + -TypeName Microsoft.ActiveDirectory.Management.ADDomainController + + return $stubDomainController + } + + Mock -CommandName Get-TargetResource -MockWith { + return @{ + Ensure = $true + SiteName = $correctSiteName + AllowPasswordReplicationAccountName = $allowedAccount + } + } + } + + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -AllowPasswordReplicationAccountName $allowedAccount } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Remove-ADDomainControllerPasswordReplicationPolicy ` + -Exactly -Times 0 + Assert-MockCalled -CommandName Add-ADDomainControllerPasswordReplicationPolicy ` + -Exactly -Times 0 + } + + Context 'When DenyPasswordReplicationAccountName is correct' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + Ensure = $true + SiteName = $correctSiteName + DenyPasswordReplicationAccountName = $deniedAccount + } + } + } + + It 'Should not throw' { + { Set-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName ` + -DenyPasswordReplicationAccountName $deniedAccount } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Remove-ADDomainControllerPasswordReplicationPolicy ` + -Exactly -Times 0 + Assert-MockCalled -CommandName Add-ADDomainControllerPasswordReplicationPolicy ` + -Exactly -Times 0 + } + } + } + } + } + + #endregion + + Describe 'ADReadOnlyDomainControllerAccount\Get-MembersToAddAndRemove' -Tag 'Helper' { + Context 'When there is one desired member' { + Context 'When there are no current members' { + Context 'When proving a $null value for CurrentMembers' { + It 'Should return the correct values' { + $result = Get-MembersToAddAndRemove -DesiredMembers 'Member1' -CurrentMembers $null + $result.MembersToAdd | Should -HaveCount 1 + $result.MembersToAdd[0].ToString() | Should -Be 'Member1' + $result.MembersToRemove | Should -BeNullOrEmpty + } + } + + Context 'When proving an empty collection for CurrentMembers' { + It 'Should return the correct values' { + $result = Get-MembersToAddAndRemove -DesiredMembers 'Member1' -CurrentMembers @() + $result.MembersToAdd | Should -HaveCount 1 + $result.MembersToAdd[0].ToString() | Should -Be 'Member1' + $result.MembersToRemove | Should -BeNullOrEmpty + } + } + } + + Context 'When there is one current member' { + Context 'When proving a collection for CurrentMembers' { + It 'Should return the correct values' { + $result = Get-MembersToAddAndRemove -DesiredMembers 'Member1' -CurrentMembers @('OldMember') + $result.MembersToAdd | Should -HaveCount 1 + $result.MembersToAdd[0].ToString() | Should -Be 'Member1' + $result.MembersToRemove | Should -HaveCount 1 + $result.MembersToRemove[0].ToString() | Should -Be 'OldMember' + } + } + + Context 'When proving a string value for CurrentMembers' { + It 'Should return the correct values' { + $result = Get-MembersToAddAndRemove -DesiredMembers 'Member1' -CurrentMembers 'OldMember' + $result.MembersToAdd | Should -HaveCount 1 + $result.MembersToAdd[0].ToString() | Should -Be 'Member1' + $result.MembersToRemove | Should -HaveCount 1 + $result.MembersToRemove[0].ToString() | Should -Be 'OldMember' + } + } + } + + Context 'When there is more than one current member' { + It 'Should return the correct values' { + $result = Get-MembersToAddAndRemove ` + -DesiredMembers 'Member1' -CurrentMembers @('OldMember1', 'OldMember2') + $result.MembersToAdd | Should -HaveCount 1 + $result.MembersToAdd[0].ToString() | Should -Be 'Member1' + $result.MembersToRemove | Should -HaveCount 2 + $result.MembersToRemove[0].ToString() | Should -Be 'OldMember1' + $result.MembersToRemove[1].ToString() | Should -Be 'OldMember2' + } + } + } + + Context 'When there are no desired members' { + Context 'When there are no current members' { + Context 'When proving a $null value for DesiredMembers and CurrentMembers' { + It 'Should return the correct values' { + $result = Get-MembersToAddAndRemove -DesiredMembers $null -CurrentMembers $null + $result.MembersToAdd | Should -BeNullOrEmpty + $result.MembersToRemove | Should -BeNullOrEmpty + } + } + + Context 'When proving an empty collection for DesiredMembers and CurrentMembers' { + It 'Should return the correct values' { + $result = Get-MembersToAddAndRemove -DesiredMembers @() -CurrentMembers @() + $result.MembersToAdd | Should -BeNullOrEmpty + $result.MembersToRemove | Should -BeNullOrEmpty + } + } + } + + Context 'When there is one current member' { + Context 'When proving a collection for CurrentMembers' { + It 'Should return the correct values' { + $result = Get-MembersToAddAndRemove -DesiredMembers $null -CurrentMembers @('OldMember') + $result.MembersToAdd | Should -BeNullOrEmpty + $result.MembersToRemove | Should -HaveCount 1 + $result.MembersToRemove[0].ToString() | Should -Be 'OldMember' + } + } + + Context 'When proving a string value for CurrentMembers' { + It 'Should return the correct values' { + $result = Get-MembersToAddAndRemove -DesiredMembers $null -CurrentMembers 'OldMember' + $result.MembersToAdd | Should -BeNullOrEmpty + $result.MembersToRemove | Should -HaveCount 1 + $result.MembersToRemove[0].ToString() | Should -Be 'OldMember' + } + } + } + + Context 'When there is more than one current member' { + It 'Should return the correct values' { + $result = Get-MembersToAddAndRemove -DesiredMembers $null ` + -CurrentMembers @('OldMember1', 'OldMember2') + $result.MembersToAdd | Should -BeNullOrEmpty + $result.MembersToRemove | Should -HaveCount 2 + $result.MembersToRemove[0].ToString() | Should -Be 'OldMember1' + $result.MembersToRemove[1].ToString() | Should -Be 'OldMember2' + } + } + } + + Context 'When the same members are present in desired members and current members' { + Context 'When proving a collection for CurrentMembers' { + It 'Should return the correct values' { + $result = Get-MembersToAddAndRemove -DesiredMembers @('Member1') -CurrentMembers @('Member1') + $result.MembersToAdd | Should -BeNullOrEmpty + $result.MembersToRemove | Should -BeNullOrEmpty + } + } + + Context 'When proving a string value for CurrentMembers' { + It 'Should return the correct values' { + $result = Get-MembersToAddAndRemove -DesiredMembers 'Member1' -CurrentMembers 'Member1' + $result.MembersToAdd | Should -BeNullOrEmpty + $result.MembersToRemove | Should -BeNullOrEmpty + } + } + } + + Context 'When there are more desired members than current members' { + Context 'When proving a collection for CurrentMembers' { + It 'Should return the correct values' { + $result = Get-MembersToAddAndRemove -DesiredMembers @('Member1', 'Member2') ` + -CurrentMembers @('Member1') + $result.MembersToAdd | Should -HaveCount 1 + $result.MembersToAdd[0].ToString() | Should -Be 'Member2' + $result.MembersToRemove | Should -BeNullOrEmpty + } + } + } + + Context 'When there are fewer desired members than current members' { + Context 'When proving a string value for CurrentMembers' { + It 'Should return the correct values' { + $result = Get-MembersToAddAndRemove -DesiredMembers 'Member1' ` + -CurrentMembers @('Member1', 'Member2') + $result.MembersToAdd | Should -BeNullOrEmpty + $result.MembersToRemove | Should -HaveCount 1 + $result.MembersToRemove[0].ToString() | Should -Be 'Member2' + } + } + } + } + } +} +finally +{ + Invoke-TestCleanup +}