From 0bd1f60d3482f1c0d29aa303220da1dd3c65a69e Mon Sep 17 00:00:00 2001 From: Daymarvi Date: Thu, 18 Jun 2026 17:16:29 +0200 Subject: [PATCH 1/4] feat: Add DeleteExistingComputerAccount parameter to DSC_Computer Add a new parameter DeleteExistingComputerAccount (default: true) to control whether an existing AD computer account is deleted and recreated when joining a domain. Setting it to false preserves the existing account (SID, group memberships, GPO links). --- .../DSC_Computer/DSC_Computer.psm1 | 61 +++++++++++-- .../DSC_Computer/DSC_Computer.schema.mof | 1 + source/DSCResources/DSC_Computer/README.md | 11 +++ .../en-US/DSC_Computer.strings.psd1 | 1 + tests/Unit/DSC_Computer.Tests.ps1 | 91 +++++++++++++++++++ 5 files changed, 156 insertions(+), 9 deletions(-) diff --git a/source/DSCResources/DSC_Computer/DSC_Computer.psm1 b/source/DSCResources/DSC_Computer/DSC_Computer.psm1 index c08a58e5..d1b67052 100644 --- a/source/DSCResources/DSC_Computer/DSC_Computer.psm1 +++ b/source/DSCResources/DSC_Computer/DSC_Computer.psm1 @@ -47,6 +47,11 @@ $FailToRenameAfterJoinDomainErrorId = 'FailToRenameAfterJoinDomain,Microsoft.Pow .PARAMETER Options Specifies advanced options for the Add-Computer join operation. + + .PARAMETER DeleteExistingComputerAccount + If $true (default), an existing computer account with the same name + in the domain will be deleted and recreated. If $false, the existing + computer account will be reused. #> function Get-TargetResource { @@ -91,7 +96,11 @@ function Get-TargetResource [Parameter()] [ValidateSet('AccountCreate', 'Win9XUpgrade', 'UnsecuredJoin', 'PasswordPass', 'JoinWithNewName', 'JoinReadOnly', 'InstallInvoke')] [System.String[]] - $Options + $Options, + + [Parameter()] + [System.Boolean] + $DeleteExistingComputerAccount = $true ) Write-Verbose -Message ($script:localizedData.GettingComputerStateMessage -f $Name) @@ -160,6 +169,11 @@ function Get-TargetResource .PARAMETER Options Specifies advanced options for the Add-Computer join operation. + + .PARAMETER DeleteExistingComputerAccount + If $true (default), an existing computer account with the same name + in the domain will be deleted and recreated. If $false, the existing + computer account will be reused. #> function Set-TargetResource { @@ -203,7 +217,11 @@ function Set-TargetResource [Parameter()] [ValidateSet('AccountCreate', 'Win9XUpgrade', 'UnsecuredJoin', 'PasswordPass', 'JoinWithNewName', 'JoinReadOnly', 'InstallInvoke')] [System.String[]] - $Options + $Options, + + [Parameter()] + [System.Boolean] + $DeleteExistingComputerAccount = $true ) Write-Verbose -Message ($script:localizedData.SettingComputerStateMessage -f $Name) @@ -262,13 +280,20 @@ function Set-TargetResource $addComputerParameters.Add("Server", $Server) } - # Check for existing computer objecst using ADSI without ActiveDirectory module - $computerObject = Get-ADSIComputer -Name $Name -DomainName $DomainName -Credential $Credential + # Check for existing computer objects using ADSI without ActiveDirectory module + if ($DeleteExistingComputerAccount) + { + $computerObject = Get-ADSIComputer -Name $Name -DomainName $DomainName -Credential $Credential - if ($computerObject) + if ($computerObject) + { + Remove-ADSIObject -Path $computerObject.Path -Credential $Credential + Write-Verbose -Message ($script:localizedData.DeletedExistingComputerObject -f $Name, $computerObject.Path) + } + } + else { - Remove-ADSIObject -Path $computerObject.Path -Credential $Credential - Write-Verbose -Message ($script:localizedData.DeletedExistingComputerObject -f $Name, $computerObject.Path) + Write-Verbose -Message ($script:localizedData.KeepingExistingComputerObject -f $Name) } if (-not [System.String]::IsNullOrEmpty($Options)) @@ -458,6 +483,11 @@ function Set-TargetResource .PARAMETER Options Specifies advanced options for the Add-Computer join operation. + + .PARAMETER DeleteExistingComputerAccount + If $true (default), an existing computer account with the same name + in the domain will be deleted and recreated. If $false, the existing + computer account will be reused. #> function Test-TargetResource { @@ -502,7 +532,11 @@ function Test-TargetResource [Parameter()] [ValidateSet('AccountCreate', 'Win9XUpgrade', 'UnsecuredJoin', 'PasswordPass', 'JoinWithNewName', 'JoinReadOnly', 'InstallInvoke')] [System.String[]] - $Options + $Options, + + [Parameter()] + [System.Boolean] + $DeleteExistingComputerAccount = $true ) Write-Verbose -Message ($script:localizedData.TestingComputerStateMessage -f $Name) @@ -812,6 +846,11 @@ function Remove-ADSIObject .PARAMETER Options Specifies advanced options for the Add-Computer join operation. + + .PARAMETER DeleteExistingComputerAccount + If $true (default), an existing computer account with the same name + in the domain will be deleted and recreated. If $false, the existing + computer account will be reused. #> function Assert-ResourceProperty { @@ -855,7 +894,11 @@ function Assert-ResourceProperty [Parameter()] [ValidateSet('AccountCreate', 'Win9XUpgrade', 'UnsecuredJoin', 'PasswordPass', 'JoinWithNewName', 'JoinReadOnly', 'InstallInvoke')] [System.String[]] - $Options + $Options, + + [Parameter()] + [System.Boolean] + $DeleteExistingComputerAccount = $true ) if ($options -contains 'PasswordPass' -and diff --git a/source/DSCResources/DSC_Computer/DSC_Computer.schema.mof b/source/DSCResources/DSC_Computer/DSC_Computer.schema.mof index 3c24d254..68c7c1f1 100644 --- a/source/DSCResources/DSC_Computer/DSC_Computer.schema.mof +++ b/source/DSCResources/DSC_Computer/DSC_Computer.schema.mof @@ -10,6 +10,7 @@ class DSC_Computer : OMI_BaseResource [Write, Description("The value assigned here will be set as the local computer description.")] String Description; [Write, Description("The Active Directory Domain Controller to use to join the domain")] String Server; [Write, Description("Specifies advanced options for the Add-Computer join operation"), ValueMap{"AccountCreate","Win9XUpgrade","UnsecuredJoin","PasswordPass","JoinWithNewName","JoinReadOnly","InstallInvoke"}, Values{"AccountCreate","Win9XUpgrade","UnsecuredJoin","PasswordPass","JoinWithNewName","JoinReadOnly","InstallInvoke"}] String Options[]; + [Write, Description("If true, an existing computer account with the same name in the domain will be deleted and recreated. If false (default), the existing computer account will be reused.")] Boolean DeleteExistingComputerAccount; [Read, Description("A read-only property that specifies the organizational unit that the computer account is currently in.")] String CurrentOU; }; diff --git a/source/DSCResources/DSC_Computer/README.md b/source/DSCResources/DSC_Computer/README.md index d23b215a..f00bcb4e 100644 --- a/source/DSCResources/DSC_Computer/README.md +++ b/source/DSCResources/DSC_Computer/README.md @@ -2,3 +2,14 @@ The resource allows you to configure a computer by changing its name and description and modifying its Active Directory domain or workgroup membership. + +## Parameters + +### DeleteExistingComputerAccount + +When joining a domain, if a computer account with the same name already exists: + +- **`$true`** (default): The existing computer account is deleted and recreated + (historical behavior). +- **`$false`**: The existing computer account is reused. This preserves the + machine SID, group memberships, GPO links, and other AD attributes. diff --git a/source/DSCResources/DSC_Computer/en-US/DSC_Computer.strings.psd1 b/source/DSCResources/DSC_Computer/en-US/DSC_Computer.strings.psd1 index 22903a87..b1c57d54 100644 --- a/source/DSCResources/DSC_Computer/en-US/DSC_Computer.strings.psd1 +++ b/source/DSCResources/DSC_Computer/en-US/DSC_Computer.strings.psd1 @@ -17,6 +17,7 @@ ConvertFrom-StringData @' DomainNameAndWorkgroupNameError = Only DomainName or WorkGroupName can be specified at once. ComputerNotInDomainMessage = This machine is not a domain member. DeletedExistingComputerObject = Deleted existing computer object with name '{0}' at path '{1}'. + KeepingExistingComputerObject = Existing computer account for '{0}' found in domain. Keeping existing account (DeleteExistingComputerAccount is false). InvalidOptionPasswordPassUnsecuredJoin = Domain Join option 'PasswordPass' may not be specified if 'UnsecuredJoin' is specified. InvalidOptionCredentialUnsecuredJoinNullUsername = 'Credential' username must be null if 'UnsecuredJoin' is specified. '@ diff --git a/tests/Unit/DSC_Computer.Tests.ps1 b/tests/Unit/DSC_Computer.Tests.ps1 index 0b5ca585..986ba6da 100644 --- a/tests/Unit/DSC_Computer.Tests.ps1 +++ b/tests/Unit/DSC_Computer.Tests.ps1 @@ -862,6 +862,52 @@ Describe 'DSC_Computer\Set-TargetResource' { } } + Context 'Changes ComputerName and changes Domain to new Domain with DeleteExistingComputerAccount false' { + BeforeAll { + Mock -CommandName Get-WMIObject -MockWith { + [PSCustomObject] @{ + Domain = 'Contoso.com'; + Workgroup = 'Contoso.com'; + PartOfDomain = $true + } + } + + Mock -CommandName Get-ADSIComputer -MockWith { + [PSCustomObject] @{ + Path = 'LDAP://Contoso.com/CN=mocked-comp,OU=Computers,DC=Contoso,DC=com'; + } + } + + Mock -CommandName Get-ComputerDomain -MockWith { + 'contoso.com' + } + + Mock -CommandName Add-Computer + } + + It 'Should not delete the existing computer account' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $setTargetParams = @{ + Name = 'othername' + DomainName = 'adventure-works.com' + Credential = $credential + UnjoinCredential = $credential + DeleteExistingComputerAccount = $false + } + + Set-TargetResource @setTargetParams | Should -BeNullOrEmpty + } + + Should -Invoke -CommandName Rename-Computer -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Add-Computer -Exactly -Times 1 -Scope It -ParameterFilter { $DomainName -and $NewName } + Should -Invoke -CommandName Add-Computer -Exactly -Times 0 -Scope It -ParameterFilter { $WorkGroupName } + Should -Invoke -CommandName Get-ADSIComputer -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Remove-ADSIObject -Exactly -Times 0 -Scope It + } + } + Context 'When ComputerName changes and Domain changes to new Domain with specified OU' { BeforeAll { Mock -CommandName Get-WMIObject -MockWith { @@ -988,6 +1034,51 @@ Describe 'DSC_Computer\Set-TargetResource' { } } + Context 'When ComputerName changes and Workgroup changes to Domain with DeleteExistingComputerAccount false' { + BeforeAll { + Mock -CommandName Get-WMIObject -MockWith { + [PSCustomObject] @{ + Domain = 'Contoso'; + Workgroup = 'Contoso'; + PartOfDomain = $false + } + } + + Mock -CommandName Get-ADSIComputer -MockWith { + [PSCustomObject] @{ + Path = 'LDAP://Contoso.com/CN=mocked-comp,OU=Computers,DC=Contoso,DC=com'; + } + } + + Mock -CommandName Get-ComputerDomain -MockWith { + '' + } + + Mock -CommandName Add-Computer + } + + It 'Should not delete the existing computer account' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $setTargetParams = @{ + Name = 'othername' + DomainName = 'Contoso.com' + Credential = $credential + DeleteExistingComputerAccount = $false + } + + Set-TargetResource @setTargetParams | Should -BeNullOrEmpty + } + + Should -Invoke -CommandName Rename-Computer -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Add-Computer -Exactly -Times 1 -Scope It -ParameterFilter { $DomainName -and $NewName } + Should -Invoke -CommandName Add-Computer -Exactly -Times 0 -Scope It -ParameterFilter { $WorkGroupName } + Should -Invoke -CommandName Get-ADSIComputer -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Remove-ADSIObject -Exactly -Times 0 -Scope It + } + } + Context 'When ComputerName changes and Workgroup changes to Domain with specified Domain Controller' { BeforeAll { Mock -CommandName Get-WMIObject -MockWith { From acbfe219ec637d39cf7c3aa6d59a5eb8d7d7f4a6 Mon Sep 17 00:00:00 2001 From: Daymarvi Date: Thu, 18 Jun 2026 17:25:22 +0200 Subject: [PATCH 2/4] docs: Add CHANGELOG entry, fix schema.mof description, add example - Added entry in CHANGELOG.md under Unreleased/Added section - Fixed schema.mof description to reflect default value (true) - Added example 8: JoinDomainKeepExistingAccount_Config --- CHANGELOG.md | 8 +++ .../DSC_Computer/DSC_Computer.schema.mof | 2 +- ...r_JoinDomainKeepExistingAccount_Config.ps1 | 50 +++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 source/Examples/Resources/Computer/8-Computer_JoinDomainKeepExistingAccount_Config.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index b1c5e898..69afe2bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Computer + - Added `DeleteExistingComputerAccount` parameter to control whether an + existing AD computer account is deleted and recreated when joining a domain. + Default is `$true` (historical behavior). Set to `$false` to reuse the + existing account and preserve SID, group memberships, and GPO links. + ### Changed - `azure-pipelines.yml` diff --git a/source/DSCResources/DSC_Computer/DSC_Computer.schema.mof b/source/DSCResources/DSC_Computer/DSC_Computer.schema.mof index 68c7c1f1..35e0d163 100644 --- a/source/DSCResources/DSC_Computer/DSC_Computer.schema.mof +++ b/source/DSCResources/DSC_Computer/DSC_Computer.schema.mof @@ -10,7 +10,7 @@ class DSC_Computer : OMI_BaseResource [Write, Description("The value assigned here will be set as the local computer description.")] String Description; [Write, Description("The Active Directory Domain Controller to use to join the domain")] String Server; [Write, Description("Specifies advanced options for the Add-Computer join operation"), ValueMap{"AccountCreate","Win9XUpgrade","UnsecuredJoin","PasswordPass","JoinWithNewName","JoinReadOnly","InstallInvoke"}, Values{"AccountCreate","Win9XUpgrade","UnsecuredJoin","PasswordPass","JoinWithNewName","JoinReadOnly","InstallInvoke"}] String Options[]; - [Write, Description("If true, an existing computer account with the same name in the domain will be deleted and recreated. If false (default), the existing computer account will be reused.")] Boolean DeleteExistingComputerAccount; + [Write, Description("If true (default), an existing computer account with the same name in the domain will be deleted and recreated. If false, the existing computer account will be reused.")] Boolean DeleteExistingComputerAccount; [Read, Description("A read-only property that specifies the organizational unit that the computer account is currently in.")] String CurrentOU; }; diff --git a/source/Examples/Resources/Computer/8-Computer_JoinDomainKeepExistingAccount_Config.ps1 b/source/Examples/Resources/Computer/8-Computer_JoinDomainKeepExistingAccount_Config.ps1 new file mode 100644 index 00000000..dcfb67bc --- /dev/null +++ b/source/Examples/Resources/Computer/8-Computer_JoinDomainKeepExistingAccount_Config.ps1 @@ -0,0 +1,50 @@ +<#PSScriptInfo +.VERSION 1.0.0 +.GUID 3b2e8a4c-7f1d-4e2a-9c6b-8d5f3a1e7b9d +.AUTHOR DSC Community +.COMPANYNAME DSC Community +.COPYRIGHT Copyright the DSC Community contributors. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/dsccommunity/ComputerManagementDsc/blob/main/LICENSE +.PROJECTURI https://github.com/dsccommunity/ComputerManagementDsc +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES First version. +.PRIVATEDATA 2016-Datacenter,2016-Datacenter-Server-Core +#> + +#Requires -module ComputerManagementDsc + +<# + .DESCRIPTION + This configuration sets the machine name to 'Server01' and + joins the 'Contoso' domain while keeping any existing computer + account in Active Directory. This preserves the machine SID, + group memberships, and GPO links. + Note: this requires an AD credential to join the domain. +#> +Configuration Computer_JoinDomainKeepExistingAccount_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullorEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -Module ComputerManagementDsc + + Node localhost + { + Computer JoinDomain + { + Name = 'Server01' + DomainName = 'Contoso' + Credential = $Credential # Credential to join to domain + DeleteExistingComputerAccount = $false # Keep existing AD computer account + } + } +} From 9574efb25daf0a7dd8a3fbd1bfe1f91a94c47d33 Mon Sep 17 00:00:00 2001 From: Daymarvi Date: Thu, 18 Jun 2026 17:52:00 +0200 Subject: [PATCH 3/4] fix: Address CodeRabbit review feedback - Add issue #457 reference to CHANGELOG entry - Return DeleteExistingComputerAccount from Get-TargetResource hashtable - Rename verbose message to SkippingExistingComputerObjectDeletion (no false AD discovery claim) --- CHANGELOG.md | 3 ++- .../DSC_Computer/DSC_Computer.psm1 | 21 ++++++++++--------- .../en-US/DSC_Computer.strings.psd1 | 2 +- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69afe2bb..124e9f77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `DeleteExistingComputerAccount` parameter to control whether an existing AD computer account is deleted and recreated when joining a domain. Default is `$true` (historical behavior). Set to `$false` to reuse the - existing account and preserve SID, group memberships, and GPO links. + existing account and preserve SID, group memberships, and GPO links - + Fixes [Issue #457](https://github.com/dsccommunity/ComputerManagementDsc/issues/457). ### Changed diff --git a/source/DSCResources/DSC_Computer/DSC_Computer.psm1 b/source/DSCResources/DSC_Computer/DSC_Computer.psm1 index d1b67052..4e0c2bc0 100644 --- a/source/DSCResources/DSC_Computer/DSC_Computer.psm1 +++ b/source/DSCResources/DSC_Computer/DSC_Computer.psm1 @@ -124,15 +124,16 @@ function Get-TargetResource -ClientOnly $returnValue = @{ - Name = $env:COMPUTERNAME - DomainName = Get-ComputerDomain - JoinOU = $JoinOU - CurrentOU = Get-ComputerOU - Credential = [ciminstance] $convertToCimCredential - UnjoinCredential = [ciminstance] $convertToCimUnjoinCredential - WorkGroupName = (Get-CimInstance -Class 'Win32_ComputerSystem').Workgroup - Description = (Get-CimInstance -Class 'Win32_OperatingSystem').Description - Server = Get-LogonServer + Name = $env:COMPUTERNAME + DomainName = Get-ComputerDomain + JoinOU = $JoinOU + CurrentOU = Get-ComputerOU + Credential = [ciminstance] $convertToCimCredential + UnjoinCredential = [ciminstance] $convertToCimUnjoinCredential + WorkGroupName = (Get-CimInstance -Class 'Win32_ComputerSystem').Workgroup + Description = (Get-CimInstance -Class 'Win32_OperatingSystem').Description + Server = Get-LogonServer + DeleteExistingComputerAccount = $DeleteExistingComputerAccount } return $returnValue @@ -293,7 +294,7 @@ function Set-TargetResource } else { - Write-Verbose -Message ($script:localizedData.KeepingExistingComputerObject -f $Name) + Write-Verbose -Message ($script:localizedData.SkippingExistingComputerObjectDeletion -f $Name) } if (-not [System.String]::IsNullOrEmpty($Options)) diff --git a/source/DSCResources/DSC_Computer/en-US/DSC_Computer.strings.psd1 b/source/DSCResources/DSC_Computer/en-US/DSC_Computer.strings.psd1 index b1c57d54..076b563e 100644 --- a/source/DSCResources/DSC_Computer/en-US/DSC_Computer.strings.psd1 +++ b/source/DSCResources/DSC_Computer/en-US/DSC_Computer.strings.psd1 @@ -17,7 +17,7 @@ ConvertFrom-StringData @' DomainNameAndWorkgroupNameError = Only DomainName or WorkGroupName can be specified at once. ComputerNotInDomainMessage = This machine is not a domain member. DeletedExistingComputerObject = Deleted existing computer object with name '{0}' at path '{1}'. - KeepingExistingComputerObject = Existing computer account for '{0}' found in domain. Keeping existing account (DeleteExistingComputerAccount is false). + SkippingExistingComputerObjectDeletion = Skipping deletion of any existing computer account for '{0}' because DeleteExistingComputerAccount is set to false. InvalidOptionPasswordPassUnsecuredJoin = Domain Join option 'PasswordPass' may not be specified if 'UnsecuredJoin' is specified. InvalidOptionCredentialUnsecuredJoinNullUsername = 'Credential' username must be null if 'UnsecuredJoin' is specified. '@ From 4f7de5846b26f8968a385bee1a2ba3ed0ca0832a Mon Sep 17 00:00:00 2001 From: Daymarvi Date: Thu, 18 Jun 2026 18:13:06 +0200 Subject: [PATCH 4/4] fix: Add DeleteExistingComputerAccount to Get-TargetResource test assertion --- tests/Unit/DSC_Computer.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/DSC_Computer.Tests.ps1 b/tests/Unit/DSC_Computer.Tests.ps1 index 986ba6da..7fe15c54 100644 --- a/tests/Unit/DSC_Computer.Tests.ps1 +++ b/tests/Unit/DSC_Computer.Tests.ps1 @@ -723,7 +723,7 @@ Describe 'DSC_Computer\Get-TargetResource' { $result = Get-TargetResource @getTargetParams $result.GetType().Fullname | Should -Be 'System.Collections.Hashtable' - $result.Keys | Sort-Object | Should -Be @('Credential', 'CurrentOU', 'Description', 'DomainName', 'JoinOU', 'Name', 'Server', 'UnjoinCredential', 'WorkGroupName') + $result.Keys | Sort-Object | Should -Be @('Credential', 'CurrentOU', 'DeleteExistingComputerAccount', 'Description', 'DomainName', 'JoinOU', 'Name', 'Server', 'UnjoinCredential', 'WorkGroupName') } } }