Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ 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 -
Fixes [Issue #457](https://github.com/dsccommunity/ComputerManagementDsc/issues/457).

### Changed

- `azure-pipelines.yml`
Expand Down
80 changes: 62 additions & 18 deletions source/DSCResources/DSC_Computer/DSC_Computer.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Write-Verbose -Message ($script:localizedData.GettingComputerStateMessage -f $Name)
Expand All @@ -115,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
Expand Down Expand Up @@ -160,6 +170,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
{
Expand Down Expand Up @@ -203,7 +218,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)
Expand Down Expand Up @@ -262,13 +281,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.SkippingExistingComputerObjectDeletion -f $Name)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (-not [System.String]::IsNullOrEmpty($Options))
Expand Down Expand Up @@ -458,6 +484,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
{
Expand Down Expand Up @@ -502,7 +533,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)
Expand Down Expand Up @@ -812,6 +847,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
{
Expand Down Expand Up @@ -855,7 +895,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
Expand Down
1 change: 1 addition & 0 deletions source/DSCResources/DSC_Computer/DSC_Computer.schema.mof
Original file line number Diff line number Diff line change
Expand Up @@ -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 (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;
};

11 changes: 11 additions & 0 deletions source/DSCResources/DSC_Computer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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}'.
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.
'@
Original file line number Diff line number Diff line change
@@ -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
}
}
}
93 changes: 92 additions & 1 deletion tests/Unit/DSC_Computer.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down