DSCResources/MSFT_ADManagedServiceAccount/MSFT_ADManagedServiceAccount.psm1

$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent
$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules'

$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common'
Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1')

$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADManagedServiceAccount'

<#
    .SYNOPSIS
        Gets the specified managed service account.
 
    .PARAMETER ServiceAccountName
        Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName 'sAMAccountName').
        To be compatible with older operating systems, create a SAM account name that is 20 characters or less. Once created,
        the user's SamAccountName and CN cannot be changed.
 
    .PARAMETER MembershipAttribute
        Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSAs).
        If not specified, this value defaults to SamAccountName. Only used when 'Group' is selected for 'AccountType'
 
    .PARAMETER AccountTypeForce
        Specifies whether or not to remove the service account and recreate it when going from single MSA to
        group MSA and vice-versa. If not specified, this value defaults to False.
 
    .PARAMETER Credential
        Specifies the user account credentials to use to perform this task.
        This is only required if not executing the task on a domain controller or using the -DomainController parameter.
 
    .PARAMETER DomainController
        Specifies the Active Directory Domain Controller instance to use to perform the task.
        This is only required if not executing the task on a domain controller.
#>

function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ServiceAccountName,

        [Parameter()]
        [ValidateSet('SamAccountName', 'DistinguishedName', 'SID', 'ObjectGUID')]
        [System.String]
        $MembershipAttribute = 'SamAccountName',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.Boolean]
        $AccountTypeForce = $false,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $DomainController
    )

    Assert-Module -ModuleName 'ActiveDirectory'
    $adServiceAccountParameters = Get-ADCommonParameters @PSBoundParameters

    $targetResource = @{
        ServiceAccountName  = $ServiceAccountName
        DistinguishedName   = $null
        Path                = $null
        Description         = $null
        DisplayName         = $null
        AccountType         = $null
        AccountTypeForce    = $AccountTypeForce
        Ensure              = $null
        Enabled             = $false
        Members             = @()
        MembershipAttribute = $MembershipAttribute
        Credential          = $Credential
        DomainController    = $DomainController
    }

    try
    {
        Write-Verbose -Message ($script:localizedData.RetrievingServiceAccount -f $ServiceAccountName)

        $adServiceAccount = Get-ADServiceAccount @adServiceAccountParameters -Properties @(
            'Name'
            'DistinguishedName'
            'Description'
            'DisplayName'
            'ObjectClass'
            'Enabled'
            'PrincipalsAllowedToRetrieveManagedPassword'
            'SamAccountName'
            'DistinguishedName'
            'SID'
            'ObjectGUID'
        )

        $targetResource['Ensure'] = 'Present'
        $targetResource['Path'] = Get-ADObjectParentDN -DN $adServiceAccount.DistinguishedName
        $targetResource['Description'] = $adServiceAccount.Description
        $targetResource['DisplayName'] = $adServiceAccount.DisplayName
        $targetResource['Enabled'] = [System.Boolean] $adServiceAccount.Enabled
        $targetResource['DistinguishedName'] = $adServiceAccount.DistinguishedName

        if ( $adServiceAccount.ObjectClass -eq 'msDS-ManagedServiceAccount' )
        {
            $targetResource['AccountType'] = 'Single'
        }
        elseif ( $adServiceAccount.ObjectClass -eq 'msDS-GroupManagedServiceAccount' )
        {
            Write-Verbose -Message ($script:localizedData.RetrievingPrincipalMembers -f $MembershipAttribute)
            $adServiceAccount.PrincipalsAllowedToRetrieveManagedPassword |
                ForEach-Object {
                    $member = (Get-ADObject -Identity $_ -Properties $MembershipAttribute).$MembershipAttribute
                    $targetResource['Members'] += $member
                }

            $targetResource['AccountType'] = 'Group'
        }
    }
    catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]
    {
        Write-Verbose -Message ($script:localizedData.ManagedServiceAccountNotFound -f $ServiceAccountName)
        $targetResource['Ensure'] = 'Absent'
    }
    catch
    {
        $errorMessage = $script:localizedData.RetrievingServiceAccountError -f $ServiceAccountName
        New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
    }

    return $targetResource
} #end function Get-TargetResource


<#
    .SYNOPSIS
        Tests the state of the managed service account.
 
    .PARAMETER ServiceAccountName
        Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName 'sAMAccountName').
        To be compatible with older operating systems, create a SAM account name that is 20 characters or less. Once created,
        the user's SamAccountName and CN cannot be changed.
 
    .PARAMETER AccountType
        The type of managed service account. Single will create a Single Managed Service Account (sMSA) and Group will
        create a Group Managed Service Account (gMSA). If not specified, this vaule defaults to Single.
 
    .PARAMETER AccountTypeForce
        Specifies whether or not to remove the service account and recreate it when going from single MSA to
        group MSA and vice-versa. If not specified, this value defaults to False.
 
    .PARAMETER Path
        Specifies the X.500 path of the Organizational Unit (OU) or container where the new object is created.
        Specified as a Distinguished Name (DN).
 
    .PARAMETER Ensure
        Specifies whether the user account is created or deleted. If not specified, this value defaults to Present.
 
    .PARAMETER Description
        Specifies a description of the object (ldapDisplayName 'description').
 
    .PARAMETER DisplayName
        Specifies the display name of the object (ldapDisplayName 'displayName').
 
    .PARAMETER Members
        Specifies the members of the object (ldapDisplayName 'PrincipalsAllowedToRetrieveManagedPassword').
        Only used when 'Group' is selected for 'AccountType'.
 
    .PARAMETER MembershipAttribute
        Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSAs).
        If not specified, this value defaults to SamAccountName. Only used when 'Group' is selected for 'AccountType'.
 
    .PARAMETER Credential
        Specifies the user account credentials to use to perform this task.
        This is only required if not executing the task on a domain controller or using the -DomainController parameter.
 
    .PARAMETER DomainController
        Specifies the Active Directory Domain Controller instance to use to perform the task.
        This is only required if not executing the task on a domain controller.
#>

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ServiceAccountName,

        [Parameter()]
        [ValidateSet('Group', 'Single')]
        [System.String]
        $AccountType = 'Single',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.Boolean]
        $AccountTypeForce = $false,

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

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

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

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

        [Parameter()]
        [System.String[]]
        $Members,

        [Parameter()]
        [ValidateSet('SamAccountName', 'DistinguishedName', 'SID', 'ObjectGUID')]
        [System.String]
        $MembershipAttribute = 'SamAccountName',

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $DomainController
    )

    # Need to set these parameters to compare if users are using the default parameter values
    $PSBoundParameters['Ensure'] = $Ensure
    $PSBoundParameters['AccountType'] = $AccountType
    $PSBoundParameters['MembershipAttribute'] = $MembershipAttribute

    $compareTargetResourceNonCompliant = Compare-TargetResourceState @PSBoundParameters |
        Where-Object -FilterScript { $_.Pass -eq $false }

    # Check if Absent, if so then we don't need to propagate any other parameters
    if ($Ensure -eq 'Absent')
    {
        $ensureState = $compareTargetResourceNonCompliant |
            Where-Object -FilterScript { $_.Parameter -eq 'Ensure' }

        if ($ensureState)
        {
            Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f `
                    'Ensure', $ensureState.Expected, $ensureState.Actual)
        }
        else
        {
            Write-Verbose -Message ($script:localizedData.MSAInDesiredState -f $ServiceAccountName)
            return $true
        }
    }
    else
    {
        $compareTargetResourceNonCompliant |
            ForEach-Object {
                Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f `
                        $_.Parameter, $_.Expected, $_.Actual)
            }
    }

    if ($compareTargetResourceNonCompliant)
    {
        Write-Verbose -Message ($script:localizedData.MSANotInDesiredState -f $ServiceAccountName)
        return $false
    }
    else
    {
        Write-Verbose -Message ($script:localizedData.MSAInDesiredState -f $ServiceAccountName)
        return $true
    }

} #end function Test-TargetResource

<#
    .SYNOPSIS
        Sets the state of the managed service account.
 
    .PARAMETER ServiceAccountName
        Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName 'sAMAccountName').
        To be compatible with older operating systems, create a SAM account name that is 20 characters or less. Once created,
        the user's SamAccountName and CN cannot be changed.
 
    .PARAMETER AccountType
        The type of managed service account. Single will create a Single Managed Service Account (sMSA) and Group will
        create a Group Managed Service Account (gMSA). If not specified, this vaule defaults to Single.
 
    .PARAMETER AccountTypeForce
        Specifies whether or not to remove the service account and recreate it when going from single MSA to
        group MSA and vice-versa. If not specified, this value defaults to False.
 
    .PARAMETER Path
        Specifies the X.500 path of the Organizational Unit (OU) or container where the new object is created.
        Specified as a Distinguished Name (DN).
 
    .PARAMETER Ensure
        Specifies whether the user account is created or deleted. If not specified, this value defaults to Present.
 
    .PARAMETER Description
        Specifies a description of the object (ldapDisplayName 'description').
 
    .PARAMETER DisplayName
        Specifies the display name of the object (ldapDisplayName 'displayName').
 
    .PARAMETER Members
        Specifies the members of the object (ldapDisplayName 'PrincipalsAllowedToRetrieveManagedPassword').
        Only used when 'Group' is selected for 'AccountType'.
 
    .PARAMETER MembershipAttribute
        Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSAs).
        If not specified, this value defaults to SamAccountName. Only used when 'Group' is selected for 'AccountType'.
 
    .PARAMETER Credential
        Specifies the user account credentials to use to perform this task.
        This is only required if not executing the task on a domain controller or using the -DomainController parameter.
 
    .PARAMETER DomainController
        Specifies the Active Directory Domain Controller instance to use to perform the task.
        This is only required if not executing the task on a domain controller.
#>

function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ServiceAccountName,

        [Parameter()]
        [ValidateSet('Group', 'Single')]
        [System.String]
        $AccountType = 'Single',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.Boolean]
        $AccountTypeForce = $false,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Path,

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

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

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

        [Parameter()]
        [System.String[]]
        $Members,

        [Parameter()]
        [ValidateSet('SamAccountName', 'DistinguishedName', 'SID', 'ObjectGUID')]
        [System.String]
        $MembershipAttribute = 'SamAccountName',

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $DomainController
    )

    # Need to set these to compare if not specified since user is using defaults
    $PSBoundParameters['Ensure'] = $Ensure
    $PSBoundParameters['AccountType'] = $AccountType
    $PSBoundParameters['MembershipAttribute'] = $MembershipAttribute

    $compareTargetResource = Compare-TargetResourceState @PSBoundParameters
    $compareTargetResourceNonCompliant = @($compareTargetResource |
            Where-Object -FilterScript { $_.Pass -eq $false })

    $adServiceAccountParameters = Get-ADCommonParameters @PSBoundParameters
    $setServiceAccountParameters = $adServiceAccountParameters.Clone()
    $moveADObjectParameters = $adServiceAccountParameters.Clone()

    try
    {
        if ($Ensure -eq 'Present')
        {
            $isEnsureNonCompliant = $false

            if ($compareTargetResourceNonCompliant |
                    Where-Object -FilterScript { $_.Parameter -eq 'Ensure' })
            {
                $isEnsureNonCompliant = $true
            }

            # We want the account to be present, but it currently does not exist
            if ($isEnsureNonCompliant)
            {
                $null = $PSBoundParameters.Remove('AccountTypeForce')
                New-ADServiceAccountHelper @PSBoundParameters
            }
            else
            {
                #region Check if AccountType is compliant
                $accountTypeState = $compareTargetResourceNonCompliant |
                    Where-Object -FilterScript { $_.Parameter -eq 'AccountType' }

                # Account already exist, need to update parameters that are not in compliance
                if ($accountTypeState)
                {
                    if ($AccountTypeForce)
                    {
                        # We need to recreate account first before we can update any properties
                        Write-Verbose -Message ($script:localizedData.UpdatingManagedServiceAccountProperty -f 'AccountType', $AccountType)
                        Remove-ADServiceAccount @adServiceAccountParameters -Confirm:$false
                        $null = $PSBoundParameters.Remove('AccountTypeForce')
                        New-ADServiceAccountHelper @PSBoundParameters
                    }
                    else
                    {
                        Write-Warning -Message ($script:localizedData.AccountTypeForceNotTrue -f $accountTypeState.Actual, $accountTypeState.Expected)
                    }
                }

                # Remove AccountType since we don't want to enumerate down below
                $compareTargetResourceNonCompliant = @($compareTargetResourceNonCompliant |
                        Where-Object -FilterScript { $_.Parameter -ne 'AccountType' })
                #endregion Check if AccountType is compliant

                #region Check if Path is compliant
                $isPathNonCompliant = $false
                if ($compareTargetResourceNonCompliant |
                        Where-Object -FilterScript { $_.Parameter -eq 'Path' })
                {
                    $isPathNonCompliant = $true
                }

                if ($isPathNonCompliant)
                {
                    Write-Verbose -Message ($script:localizedData.MovingManagedServiceAccount -f $ServiceAccountName, $Path)
                    $distinguishedNameObject = $compareTargetResource |
                        Where-Object -FilterScript { $_.Parameter -eq 'DistinguishedName' }

                    $moveADObjectParameters['Identity'] = $distinguishedNameObject.Actual

                    Move-ADObject @moveADObjectParameters -TargetPath $Path
                }

                $compareTargetResourceNonCompliant = @($compareTargetResourceNonCompliant |
                        Where-Object -FilterScript { $_.Parameter -ne 'Path' })
                #endregion Check if Path is compliant

                #region Check if other parameters are compliant
                $updateProperties = $false
                $compareTargetResourceNonCompliant |
                    ForEach-Object {
                        $updateProperties = $true
                        $parameter = $_.Parameter
                        if ($parameter -eq 'Members' -and $AccountType -eq 'Group')
                        {
                            if ([system.string]::IsNullOrEmpty($Members))
                            {
                                $Members = @()
                            }
                            $listMembers = $Members -join ','

                            Write-Verbose -Message ($script:localizedData.UpdatingManagedServiceAccountProperty -f 'Members', $listMembers)
                            $setServiceAccountParameters['PrincipalsAllowedToRetrieveManagedPassword'] = $Members
                        }
                        else
                        {
                            Write-Verbose -Message ($script:localizedData.UpdatingManagedServiceAccountProperty -f $parameter, $PSBoundParameters.$parameter)
                            $setServiceAccountParameters[$parameter] = $PSBoundParameters.$parameter
                        }
                    }

                if ($compareTargetResourceNonCompliant.Count -gt 0)
                {
                    Set-ADServiceAccount @setServiceAccountParameters
                }
                #endregion Check if other parameters are compliant
            }
        }
        elseif ($Ensure -eq 'Absent')
        {
            $isEnsureNonCompliant = $false
            if ($compareTargetResourceNonCompliant |
                    Where-Object -FilterScript { $_.Parameter -eq 'Ensure' })
            {
                $isEnsureNonCompliant = $true
            }

            # We want the account to be Absent, but it is Present
            if ($isEnsureNonCompliant)
            {
                Write-Verbose -Message ($script:localizedData.RemovingManagedServiceAccount -f $ServiceAccountName)
                Remove-ADServiceAccount @adServiceAccountParameters -Confirm:$false
            }
        }
    }
    catch
    {
        $errorMessage = $script:localizedData.AddingManagedServiceAccountError -f $ServiceAccountName
        New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
    }
} #end function Set-TargetResource

<#
    .SYNOPSIS
        Adds the managed service account.
 
    .PARAMETER ServiceAccountName
        Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName 'sAMAccountName').
        To be compatible with older operating systems, create a SAM account name that is 20 characters or less. Once created,
        the user's SamAccountName and CN cannot be changed.
 
    .PARAMETER AccountType
        The type of managed service account. Single will create a Single Managed Service Account (sMSA) and Group will
        create a Group Managed Service Account (gMSA). If not specified, this vaule defaults to Single.
 
    .PARAMETER AccountTypeForce
        Specifies whether or not to remove the service account and recreate it when going from single MSA to
        group MSA and vice-versa. If not specified, this value defaults to False.
 
    .PARAMETER Path
        Specifies the X.500 path of the Organizational Unit (OU) or container where the new object is created.
        Specified as a Distinguished Name (DN).
 
    .PARAMETER Ensure
        Specifies whether the user account is created or deleted. If not specified, this value defaults to Present.
 
    .PARAMETER Description
        Specifies a description of the object (ldapDisplayName 'description').
 
    .PARAMETER DisplayName
        Specifies the display name of the object (ldapDisplayName 'displayName').
 
    .PARAMETER Members
        Specifies the members of the object (ldapDisplayName 'PrincipalsAllowedToRetrieveManagedPassword').
        Only used when 'Group' is selected for 'AccountType'.
 
    .PARAMETER MembershipAttribute
        Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSAs).
        If not specified, this value defaults to SamAccountName. Only used when 'Group' is selected for 'AccountType'.
 
    .PARAMETER Credential
        Specifies the user account credentials to use to perform this task.
        This is only required if not executing the task on a domain controller or using the -DomainController parameter.
 
    .PARAMETER DomainController
        Specifies the Active Directory Domain Controller instance to use to perform the task.
        This is only required if not executing the task on a domain controller.
#>

function New-ADServiceAccountHelper
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ServiceAccountName,

        [Parameter()]
        [ValidateSet('Group', 'Single')]
        [System.String]
        $AccountType = 'Single',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Path,

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

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

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

        [Parameter()]
        [System.String[]]
        $Members,

        [Parameter()]
        [ValidateSet('SamAccountName', 'DistinguishedName', 'SID', 'ObjectGUID')]
        [System.String]
        $MembershipAttribute = 'SamAccountName',

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $DomainController
    )

    Write-Verbose -Message ($script:localizedData.AddingManagedServiceAccount -f $ServiceAccountName)

    $adServiceAccountParameters = Get-ADCommonParameters @PSBoundParameters -UseNameParameter

    if ($Description)
    {
        $adServiceAccountParameters['Description'] = $Description
    }

    if ($DisplayName)
    {
        $adServiceAccountParameters['DisplayName'] = $DisplayName
    }

    if ($Path)
    {
        $adServiceAccountParameters['Path'] = $Path
    }


    # Create service account
    if ( $AccountType -eq 'Single' )
    {
        New-ADServiceAccount @adServiceAccountParameters -RestrictToSingleComputer -PassThru
    }
    elseif ( $AccountType -eq 'Group' )
    {
        if ($Members)
        {
            $adServiceAccountParameters['PrincipalsAllowedToRetrieveManagedPassword'] = $Members
        }

        $dnsHostName = '{0}.{1}' -f $ServiceAccountName, $(Get-DomainName)
        $adServiceAccountParameters['DNSHostName'] = $dnsHostName

        New-ADServiceAccount @adServiceAccountParameters -PassThru
    }
} #end function New-ADServiceAccountHelper

<#
    .SYNOPSIS
        Compares the state of the managed service account.
 
    .PARAMETER ServiceAccountName
        Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName 'sAMAccountName').
        To be compatible with older operating systems, create a SAM account name that is 20 characters or less. Once created,
        the user's SamAccountName and CN cannot be changed.
 
    .PARAMETER AccountType
        The type of managed service account. Single will create a Single Managed Service Account (sMSA) and Group will
        create a Group Managed Service Account (gMSA). If not specified, this vaule defaults to Single.
 
    .PARAMETER AccountTypeForce
        Specifies whether or not to remove the service account and recreate it when going from single MSA to
        group MSA and vice-versa. If not specified, this value defaults to False.
 
    .PARAMETER Path
        Specifies the X.500 path of the Organizational Unit (OU) or container where the new object is created.
        Specified as a Distinguished Name (DN).
 
    .PARAMETER Ensure
        Specifies whether the user account is created or deleted. If not specified, this value defaults to Present.
 
    .PARAMETER Description
        Specifies a description of the object (ldapDisplayName 'description').
 
    .PARAMETER DisplayName
        Specifies the display name of the object (ldapDisplayName 'displayName').
 
    .PARAMETER Members
        Specifies the members of the object (ldapDisplayName 'PrincipalsAllowedToRetrieveManagedPassword').
        Only used when 'Group' is selected for 'AccountType'.
 
    .PARAMETER MembershipAttribute
        Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSAs).
        If not specified, this value defaults to SamAccountName. Only used when 'Group' is selected for 'AccountType'.
 
    .PARAMETER Credential
        Specifies the user account credentials to use to perform this task.
        This is only required if not executing the task on a domain controller or using the -DomainController parameter.
 
    .PARAMETER DomainController
        Specifies the Active Directory Domain Controller instance to use to perform the task.
        This is only required if not executing the task on a domain controller.
#>

function Compare-TargetResourceState
{
    [CmdletBinding()]
    [OutputType([System.Array])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ServiceAccountName,

        [Parameter()]
        [ValidateSet('Group', 'Single')]
        [System.String]
        $AccountType,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.Boolean]
        $AccountTypeForce,

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

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

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

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

        [Parameter()]
        [System.String[]]
        $Members,

        [Parameter()]
        [ValidateSet('SamAccountName', 'DistinguishedName', 'SID', 'ObjectGUID')]
        [System.String]
        $MembershipAttribute,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $DomainController
    )

    $getTargetResourceParameters = @{
        ServiceAccountName  = $ServiceAccountName
        Credential          = $Credential
        DomainController    = $DomainController
        MembershipAttribute = $MembershipAttribute
        AccountTypeForce    = $AccountTypeForce
    }

    @($getTargetResourceParameters.Keys) |
        ForEach-Object {
            if (-not $PSBoundParameters.ContainsKey($_))
            {
                $getTargetResourceParameters.Remove($_)
            }
        }

    $getTargetResource = Get-TargetResource @getTargetResourceParameters
    $compareTargetResource = @()

    # Add DistinguishedName as it won't be passed as an argument, but we want to get the DN in Set
    $PSBoundParameters['DistinguishedName'] = $getTargetResource['DistinguishedName']

    <#
        Set MembershipAttribute as it's not required to be compliant. It's only
        used when setting/getting members for gMSA and there is no way to check
        if it is in compliance since whatever is passed would be compliant itself.
    #>

    $PSBoundParameters['MembershipAttribute'] = $getTargetResource['MembershipAttribute']

    foreach ($parameter in $PSBoundParameters.Keys)
    {
        if ($PSBoundParameters.$parameter -eq $getTargetResource.$parameter)
        {
            # Check if parameter is in compliance
            $compareTargetResource += [pscustomobject] @{
                Parameter = $parameter
                Expected  = $PSBoundParameters.$parameter
                Actual    = $getTargetResource.$parameter
                Pass      = $true
            }
        }
        elseif ($parameter -eq 'Members')
        {
            # Members is only for Group MSAs, if it's single computer, we can skip over this parameter
            if ($PSBoundParameters.AccountType -eq 'Group')
            {
                $testMembersParams = @{
                    ExistingMembers = $getTargetResource.Members -as [System.String[]]
                    Members         = $Members
                }

                $expectedMembers = ($Members |
                        Sort-Object) -join ','

                $actualMembers = ($testMembersParams['ExistingMembers'] |
                        Sort-Object) -join ','

                if (-not (Test-Members @testMembersParams))
                {
                    $compareTargetResource += [pscustomobject] @{
                        Parameter = $parameter
                        Expected  = $expectedMembers
                        Actual    = $actualMembers
                        Pass      = $false
                    }
                }
                else
                {
                    $compareTargetResource += [pscustomobject] @{
                        Parameter = $parameter
                        Expected  = $expectedMembers
                        Actual    = $actualMembers
                        Pass      = $true
                    }
                }
            }
        }

        # Need to check if parameter is part of schema, otherwise ignore all other parameters like verbose
        elseif ($getTargetResource.ContainsKey($parameter))
        {
            <#
                We are out of compliance if we get here
                $PSBoundParameters.$parameter -ne $getTargetResource.$parameter
            #>

            $compareTargetResource += [pscustomobject] @{
                Parameter = $parameter
                Expected  = $PSBoundParameters.$parameter
                Actual    = $getTargetResource.$parameter
                Pass      = $false
            }
        }
    } #end foreach PSBoundParameter

    return $compareTargetResource
} #end function Compare-TargetResourceState

Export-ModuleMember -Function *-TargetResource