DSCResources/MSFT_xADComputer/MSFT_xADComputer.psm1

$moduleRoot = Split-Path -Path $MyInvocation.MyCommand.Path -Parent

# Import the common AD functions
$adCommonFunctions = Join-Path `
    -Path (Split-Path -Path $PSScriptRoot -Parent) `
    -ChildPath '\MSFT_xADCommon\MSFT_xADCommon.psm1'
Import-Module -Name $adCommonFunctions

#region LocalizedData
$culture = 'en-us'
if (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath $PSUICulture))
{
    $culture = $PSUICulture
}
$importLocalizedDataParams = @{
    BindingVariable = 'LocalizedData'
    Filename = 'MSFT_xADComputer.psd1'
    BaseDirectory = $moduleRoot
    UICulture = $culture
}
Import-LocalizedData @importLocalizedDataParams
#endregion

# Create a property map that maps the DSC resource parameters to the
# Active Directory computer attributes.
$adPropertyMap = @(
    @{ Parameter = 'ComputerName'; ADProperty = 'cn'; }
    @{ Parameter = 'Location'; }
    @{ Parameter = 'DnsHostName'; }
    @{ Parameter = 'ServicePrincipalNames'; }
    @{ Parameter = 'UserPrincipalName'; }
    @{ Parameter = 'DisplayName'; }
    @{ Parameter = 'Path'; ADProperty = 'distinguishedName'; }
    @{ Parameter = 'Description'; }
    @{ Parameter = 'Enabled'; }
    @{ Parameter = 'Manager'; ADProperty = 'managedBy'; }
)


function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        # Common Name
        [Parameter(Mandatory)]
        [System.String] $ComputerName,

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

        [ValidateNotNull()]
        [System.String] $UserPrincipalName,

        [ValidateNotNull()]
        [System.String] $DisplayName,

        [ValidateNotNull()]
        [System.String] $Path,

        [ValidateNotNull()]
        [System.String] $Location,

        [ValidateNotNull()]
        [System.String] $DnsHostName,

        [ValidateNotNull()]
        [System.String[]] $ServicePrincipalNames,

        [ValidateNotNull()]
        [System.String] $Description,

        # Computer's manager specified as a Distinguished Name (DN)
        [ValidateNotNull()]
        [System.String] $Manager,

        [ValidateNotNull()]
        [System.String] $RequestFile,

        [ValidateNotNull()]
        [System.Boolean] $Enabled = $true,

        [ValidateNotNull()]
        [System.String] $DomainController,

        # Ideally this should just be called 'Credential' but is here for consistency with xADUser
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $DomainAdministratorCredential,

        [Parameter()]
        [ValidateNotNull()]
        [System.Boolean]
        $RestoreFromRecycleBin
    )

    Assert-Module -ModuleName 'ActiveDirectory';
    Import-Module -Name 'ActiveDirectory' -Verbose:$false;

    try
    {
        $adCommonParameters = Get-ADCommonParameters @PSBoundParameters;

        $adProperties = @();
        # Create an array of the AD property names to retrieve from the property map
        foreach ($property in $adPropertyMap)
        {

            if ($property.ADProperty)
            {
                $adProperties += $property.ADProperty;
            }
            else
            {
                $adProperties += $property.Parameter;
            }
        }

        Write-Verbose -Message ($LocalizedData.RetrievingADComputer -f $ComputerName);
        $adComputer = Get-ADComputer @adCommonParameters -Properties $adProperties;
        Write-Verbose -Message ($LocalizedData.ADComputerIsPresent -f $ComputerName);
        $Ensure = 'Present';
    }
    catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]
    {
        Write-Verbose -Message ($LocalizedData.ADComputerNotPresent -f $ComputerName);
        $Ensure = 'Absent';
    }
    catch
    {
        Write-Error -Message ($LocalizedData.RetrievingADComputerError -f $ComputerName);
        throw $_;
    }

    $targetResource = @{
        ComputerName      = $ComputerName;
        DistinguishedName = $adComputer.DistinguishedName; # Read-only property
        SID               = $adComputer.SID; # Read-only property
        Ensure            = $Ensure;
        DomainController  = $DomainController;
        RequestFile    = $RequestFile;
    }

    # Retrieve each property from the ADPropertyMap and add to the hashtable
    foreach ($property in $adPropertyMap)
    {
        $propertyName = $property.Parameter;
        if ($propertyName -eq 'Path') {
            # The path returned is not the parent container
            if (-not [System.String]::IsNullOrEmpty($adComputer.DistinguishedName))
            {
                $targetResource['Path'] = Get-ADObjectParentDN -DN $adComputer.DistinguishedName;
            }
        }
        elseif ($property.ADProperty)
        {
            # The AD property name is different to the function parameter to use this
            $targetResource[$propertyName] = $adComputer.($property.ADProperty);
        }
        else
        {
            # The AD property name matches the function parameter
            if ($adComputer.$propertyName -is [Microsoft.ActiveDirectory.Management.ADPropertyValueCollection])
            {
                $targetResource[$propertyName] = $adComputer.$propertyName -as [System.String[]];
            }
            else
            {
                $targetResource[$propertyName] = $adComputer.$propertyName;
            }
        }
    }
    return $targetResource;

} #end function Get-TargetResource


function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        # Common Name
        [Parameter(Mandatory)]
        [System.String] $ComputerName,

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

        [ValidateNotNull()]
        [System.String] $UserPrincipalName,

        [ValidateNotNull()]
        [System.String] $DisplayName,

        [ValidateNotNull()]
        [System.String] $Path,

        [ValidateNotNull()]
        [System.String] $Location,

        [ValidateNotNull()]
        [System.String] $DnsHostName,

        [ValidateNotNull()]
        [System.String[]] $ServicePrincipalNames,

        [ValidateNotNull()]
        [System.String] $Description,

        # Computer's manager specified as a Distinguished Name (DN)
        [ValidateNotNull()]
        [System.String] $Manager,

        [ValidateNotNull()]
        [System.String] $RequestFile,

        [ValidateNotNull()]
        [System.Boolean] $Enabled = $true,

        [ValidateNotNull()]
        [System.String] $DomainController,

        # Ideally this should just be called 'Credential' but is here for backwards compatibility
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $DomainAdministratorCredential,

        [ValidateNotNull()]
        [System.Boolean]
        $RestoreFromRecycleBin
    )

    $targetResource = Get-TargetResource @PSBoundParameters;
    $isCompliant = $true;

    if ($Ensure -eq 'Absent')
    {
        if ($targetResource.Ensure -eq 'Present')
        {
            Write-Verbose -Message ($LocalizedData.ADComputerNotDesiredPropertyState -f `
                                    'Ensure', $PSBoundParameters.Ensure, $targetResource.Ensure);
            $isCompliant = $false;
        }
    }
    else
    {
        # Add ensure and enabled as they may not be explicitly passed and we want to enumerate them
        $PSBoundParameters['Ensure'] = $Ensure;
        $PSBoundParameters['Enabled'] = $Enabled;

        foreach ($parameter in $PSBoundParameters.Keys)
        {
            if ($targetResource.ContainsKey($parameter))
            {
                # This check is required to be able to explicitly remove values with an empty string, if required
                if (([System.String]::IsNullOrEmpty($PSBoundParameters.$parameter)) -and
                    ([System.String]::IsNullOrEmpty($targetResource.$parameter)))
                {
                    # Both values are null/empty and therefore we are compliant
                }
                elseif ($parameter -eq 'ServicePrincipalNames')
                {
                    $testMembersParams = @{
                        ExistingMembers = $targetResource.ServicePrincipalNames -as [System.String[]];
                        Members = $ServicePrincipalNames;
                    }
                    if (-not (Test-Members @testMembersParams))
                    {
                        $existingSPNs = $testMembersParams['ExistingMembers'] -join ',';
                        $desiredSPNs = $ServicePrincipalNames -join ',';
                        Write-Verbose -Message ($LocalizedData.ADComputerNotDesiredPropertyState -f `
                                                'ServicePrincipalNames', $desiredSPNs, $existingSPNs);
                        $isCompliant = $false;
                    }
                }
                elseif ($PSBoundParameters.$parameter -ne $targetResource.$parameter)
                {
                    Write-Verbose -Message ($LocalizedData.ADComputerNotDesiredPropertyState -f `
                                            $parameter, $PSBoundParameters.$parameter, $targetResource.$parameter);
                    $isCompliant = $false;
                }
            }
        } #end foreach PSBoundParameter
    }

    if ($isCompliant)
    {
        Write-Verbose -Message ($LocalizedData.ADComputerInDesiredState -f $ComputerName)
        return $true
    }
    else
    {
        Write-Verbose -Message ($LocalizedData.ADComputerNotInDesiredState -f $ComputerName)
        return $false
    }

} #end function Test-TargetResource


function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        # Common Name
        [Parameter(Mandatory)]
        [System.String] $ComputerName,

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

        [ValidateNotNull()]
        [System.String] $UserPrincipalName,

        [ValidateNotNull()]
        [System.String] $DisplayName,

        [ValidateNotNull()]
        [System.String] $Path,

        [ValidateNotNull()]
        [System.String] $Location,

        [ValidateNotNull()]
        [System.String] $DnsHostName,

        [ValidateNotNull()]
        [System.String[]] $ServicePrincipalNames,

        [ValidateNotNull()]
        [System.String] $Description,

        # Computer's manager specified as a Distinguished Name (DN)
        [ValidateNotNull()]
        [System.String] $Manager,

        [ValidateNotNull()]
        [System.String] $RequestFile,

        [ValidateNotNull()]
        [System.Boolean] $Enabled = $true,

        [ValidateNotNull()]
        [System.String] $DomainController,

        # Ideally this should just be called 'Credential' but is here for backwards compatibility
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $DomainAdministratorCredential,

        [ValidateNotNull()]
        [System.Boolean]
        $RestoreFromRecycleBin
    )

    $targetResource = Get-TargetResource @PSBoundParameters;

    # Add ensure and enabled as they may not be explicitly passed and we want to enumerate them
    $PSBoundParameters['Ensure'] = $Ensure;
    $PSBoundParameters['Enabled'] = $Enabled;

    if ($Ensure -eq 'Present')
    {
        if ($targetResource.Ensure -eq 'Absent') {
            # Try to restore account if it exists
            if($RestoreFromRecycleBin)
            {
                Write-Verbose -Message ($LocalizedData.RestoringADComputer -f $ComputerName)
                $restoreParams = Get-ADCommonParameters @PSBoundParameters
                $restorationSuccessful = Restore-ADCommonObject @restoreParams -ObjectClass Computer -ErrorAction Stop
            }

            <#
                Computer does not exist and needs creating
                or account not present in recycle bin
            #>

            if (-not $RestoreFromRecycleBin -or ($RestoreFromRecycleBin -and -not $restorationSuccessful))
            {
                if ($RequestFile)
                {
                    # Use DJOIN to create the computer account as well as the ODJ Request file.
                    Write-Verbose -Message ($LocalizedData.ODJRequestStartMessage -f `
                            $DomainName,$ComputerName,$RequestFile)

                    # This should only be performed on a Domain Member, so detect the Domain Name.
                    $DomainName = Get-DomainName
                    $DJoinParameters = @(
                        '/PROVISION'
                        '/DOMAIN',$DomainName
                        '/MACHINE',$ComputerName )
                    if ($PSBoundParameters.ContainsKey('Path'))
                    {
                        $DJoinParameters += @( '/MACHINEOU',$Path )
                    } # if

                    if ($PSBoundParameters.ContainsKey('DomainController'))
                    {
                        $DJoinParameters += @( '/DCNAME',$DomainController )
                    } # if

                    $DJoinParameters += @( '/SAVEFILE',$RequestFile )
                    $Result = & djoin.exe @DjoinParameters

                    if ($LASTEXITCODE -ne 0)
                    {
                        $errorId = 'ODJRequestError'
                        $errorMessage = $($LocalizedData.ODJRequestError `
                            -f $LASTEXITCODE,$Result)
                        ThrowInvalidOperationError -ErrorId $errorId -ErrorMessage $errorMessage
                    } # if

                    Write-Verbose -Message ($LocalizedData.ODJRequestCompleteMessage -f `
                            $DomainName,$ComputerName,$RequestFile)
                }
                else
                {
                    # Create the computer account using New-ADComputer
                    $newADComputerParams = Get-ADCommonParameters @PSBoundParameters -UseNameParameter;
                    if ($PSBoundParameters.ContainsKey('Path'))
                    {
                        Write-Verbose -Message ($LocalizedData.UpdatingADComputerProperty -f 'Path', $Path);
                        $newADComputerParams['Path'] = $Path;
                    }
                    Write-Verbose -Message ($LocalizedData.AddingADComputer -f $ComputerName);
                    New-ADComputer @newADComputerParams;
                } # if
            }
            # Now retrieve the newly created computer
            $targetResource = Get-TargetResource @PSBoundParameters;
        }

        $setADComputerParams = Get-ADCommonParameters @PSBoundParameters;
        $replaceComputerProperties = @{};
        $removeComputerProperties = @{};
        foreach ($parameter in $PSBoundParameters.Keys)
        {
            # Only check/action properties specified/declared parameters that match one of the function's
            # parameters. This will ignore common parameters such as -Verbose etc.
            if ($targetResource.ContainsKey($parameter))
            {
                if ($parameter -eq 'Path' -and ($PSBoundParameters.Path -ne $targetResource.Path))
                {
                    # Cannot move computers by updating the DistinguishedName property
                    $adCommonParameters = Get-ADCommonParameters @PSBoundParameters;
                    # Using the SamAccountName for identity with Move-ADObject does not work, use the DN instead
                    $adCommonParameters['Identity'] = $targetResource.DistinguishedName;
                    Write-Verbose -Message ($LocalizedData.MovingADComputer -f `
                                            $targetResource.Path, $PSBoundParameters.Path);
                    Move-ADObject @adCommonParameters -TargetPath $PSBoundParameters.Path;
                }
                elseif ($parameter -eq 'ServicePrincipalNames')
                {
                    Write-Verbose -Message ($LocalizedData.UpdatingADComputerProperty -f `
                                            'ServicePrincipalNames', ($ServicePrincipalNames -join ','));
                    $replaceComputerProperties['ServicePrincipalName'] = $ServicePrincipalNames;
                }
                elseif ($parameter -eq 'Enabled' -and ($PSBoundParameters.$parameter -ne $targetResource.$parameter))
                {
                    # We cannot enable/disable an account with -Add or -Replace parameters, but inform that
                    # we will change this as it is out of compliance (it always gets set anyway)
                    Write-Verbose -Message ($LocalizedData.UpdatingADComputerProperty -f `
                                            $parameter, $PSBoundParameters.$parameter);
                }
                elseif ($PSBoundParameters.$parameter -ne $targetResource.$parameter)
                {
                    # Find the associated AD property
                    $adProperty = $adPropertyMap | Where-Object { $_.Parameter -eq $parameter };

                    if ([System.String]::IsNullOrEmpty($adProperty))
                    {
                        # We can't do anything with an empty AD property!
                    }
                    elseif ([System.String]::IsNullOrEmpty($PSBoundParameters.$parameter))
                    {
                        # We are removing properties
                        # Only remove if the existing value in not null or empty
                        if (-not ([System.String]::IsNullOrEmpty($targetResource.$parameter)))
                        {
                            Write-Verbose -Message ($LocalizedData.RemovingADComputerProperty -f `
                                                    $parameter, $PSBoundParameters.$parameter);
                            if ($adProperty.UseCmdletParameter -eq $true)
                            {
                                # We need to pass the parameter explicitly to Set-ADComputer, not via -Remove
                                $setADComputerParams[$adProperty.Parameter] = $PSBoundParameters.$parameter;
                            }
                            elseif ([System.String]::IsNullOrEmpty($adProperty.ADProperty))
                            {
                                $removeComputerProperties[$adProperty.Parameter] = $targetResource.$parameter;
                            }
                            else
                            {
                                $removeComputerProperties[$adProperty.ADProperty] = $targetResource.$parameter;
                            }
                        }
                    } #end if remove existing value
                    else
                    {
                        # We are replacing the existing value
                        Write-Verbose -Message ($LocalizedData.UpdatingADComputerProperty -f `
                                                $parameter, $PSBoundParameters.$parameter);
                        if ($adProperty.UseCmdletParameter -eq $true)
                        {
                            # We need to pass the parameter explicitly to Set-ADComputer, not via -Replace
                            $setADComputerParams[$adProperty.Parameter] = $PSBoundParameters.$parameter;
                        }
                        elseif ([System.String]::IsNullOrEmpty($adProperty.ADProperty))
                        {
                            $replaceComputerProperties[$adProperty.Parameter] = $PSBoundParameters.$parameter;
                        }
                        else
                        {
                            $replaceComputerProperties[$adProperty.ADProperty] = $PSBoundParameters.$parameter;
                        }
                    } #end if replace existing value
                }

            } #end if TargetResource parameter
        } #end foreach PSBoundParameter

        # Only pass -Remove and/or -Replace if we have something to set/change
        if ($replaceComputerProperties.Count -gt 0)
        {
            $setADComputerParams['Replace'] = $replaceComputerProperties;
        }
        if ($removeComputerProperties.Count -gt 0)
        {
            $setADComputerParams['Remove'] = $removeComputerProperties;
        }

        Write-Verbose -Message ($LocalizedData.UpdatingADComputer -f $ComputerName);
        [ref] $null = Set-ADComputer @setADComputerParams -Enabled $Enabled;
    }
    elseif (($Ensure -eq 'Absent') -and ($targetResource.Ensure -eq 'Present'))
    {
        # User exists and needs removing
        Write-Verbose ($LocalizedData.RemovingADComputer -f $ComputerName);
        $adCommonParameters = Get-ADCommonParameters @PSBoundParameters;
        [ref] $null = Remove-ADComputer @adCommonParameters -Confirm:$false;
    }

} #end function Set-TargetResource

Export-ModuleMember -Function *-TargetResource