DSCResources/xDSCVault_LocalUser/xDSCVault_LocalUser.psm1

# User name and password needed for this resource and Write-Verbose Used in helper functions
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUserNameAndPassWordParams', '')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSDSCUseVerboseMessageInDSCResource', '')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')]
param ()
$errorActionPreference = 'Stop'
Set-StrictMode -Version 'Latest'

Import-Module -Name (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) `
        -ChildPath 'CommonResourceHelper.psm1')

# Localized messages for verbose and error statements in this resource
$script:localizedData = Get-LocalizedData -ResourceName 'xDSCVault_LocalUser'

if (-not (Test-IsNanoServer))
{
    Add-Type -AssemblyName 'System.DirectoryServices.AccountManagement'
}
# get rid of this else once the fix for this is released
else
{
    Import-Module -Name 'Microsoft.Powershell.LocalAccounts'
}

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

        [parameter(Mandatory = $true)]
        [System.String]
        $VaultPath,

        [parameter(Mandatory = $true)]
        [System.String]
        $UserName,

        [System.Boolean]
        $Disabled = $false,

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

        [System.String]
        $ApiPrefix = 'v1',

        [System.Boolean]
        $PasswordChangeNotAllowed = $false,

        [System.Boolean]
        $PasswordChangeRequired = $false,

        [System.Boolean]
        $PasswordNeverExpires = $false,
    
        [System.String]
        $Description,
    
        [System.String]
        $FullName,

        [System.String]
        $AuthBackend = 'approle'
    )

    Write-Verbose -Message ($script:localizedData.ObtainClientToken)
    $clientToken = Start-VaultAuth -VaultAddress $VaultAddress -ApiPrefix $ApiPrefix -AuthBackend $AuthBackend
  
    $currentVaultValue = Read-VaultData -VaultAddress $VaultAddress -ClientToken $clientToken.auth.client_token -VaultPath $VaultPath -ApiPrefix $ApiPrefix
    
    if ($clientToken.auth.client_token -ne $null) 
    {
      $clientTokenResult = ConvertTo-SecureString -String $clientToken.auth.client_token -AsPlainText -Force
    }
    else 
    {
        $clientTokenResult = 'Error obtaining client token'
    }
    
    if ($currentVaultValue -ne 404) 
    {
      $currentVaultValueOutput = ConvertTo-SecureString -String $currentVaultValue.data.value -AsPlainText -Force
    }
    else 
    {
        $currentVaultValueOutput = $null
    }
    
    if ($currentVaultValue -eq 404) 
    {
        $readResult = $currentVaultValue
    }
    else 
    {
        $readResult = 200
    }
  
    $returnValue = @{
        VaultAddress             = $VaultAddress
        VaultPath                = $VaultPath
        ApiPath                  = ($VaultAddress + '/' + $ApiPrefix + '/' + $VaultPath)
        ClientToken              = $clientTokenResult
        CurrentVaultValue        = $currentVaultValueOutput
        ReadResultStatus         = $readResult
        Disabled                 = $Disabled
        Ensure                   = $Ensure
        PasswordChangeNotAllowed = $PasswordChangeNotAllowed
        PasswordChangeRequired   = $PasswordChangeRequired
        PasswordNeverExpires     = $PasswordNeverExpires
        Username                 = $UserName
        Description              = $Description
        FullName                 = $FullName
        AuthBackend              = $AuthBackend
    }
  
    $returnValue
}


function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        [parameter(Mandatory = $true)]
        [System.String]
        $VaultAddress,

        [parameter(Mandatory = $true)]
        [System.String]
        $VaultPath,

        [System.String]
        $ApiPrefix = 'v1',

        [parameter(Mandatory = $true)]
        [System.String]
        $UserName,

        [System.String]
        $Description,

        [System.Boolean]
        $Disabled = $false,

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

        [System.String]
        $FullName,

        [System.Boolean]
        $PasswordChangeNotAllowed = $false,

        [System.Boolean]
        $PasswordChangeRequired = $false,

        [System.Boolean]
        $PasswordNeverExpires = $false,

        [System.String]
        $AuthBackend = 'approle'
    )

    $resourceData = Get-TargetResource @PSBoundParameters

    if ($resourceData.CurrentVaultValue.GetType().Name -eq 'SecureString')
    {
        $Password = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ($UserName, $resourceData.CurrentVaultValue)
    }

    $PSBoundParameters += @{
        Password = $Password
    }  
      
    if (Test-IsNanoServer)
    {
        Set-TargetResourceOnNanoServer @PSBoundParameters
    }
    else
    {
        Set-TargetResourceOnFullSKU @PSBoundParameters
    }
}


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

        [parameter(Mandatory = $true)]
        [System.String]
        $VaultPath,

        [System.String]
        $ApiPrefix = 'v1',

        [parameter(Mandatory = $true)]
        [System.String]
        $UserName,

        [System.String]
        $Description,

        [System.Boolean]
        $Disabled = $false,

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

        [System.String]
        $FullName,

        [System.Boolean]
        $PasswordChangeNotAllowed = $false,

        [System.Boolean]
        $PasswordChangeRequired = $false,

        [System.Boolean]
        $PasswordNeverExpires = $false,

        [System.String]
        $AuthBackend = 'approle'
    )
  
    $resourceData = Get-TargetResource @PSBoundParameters

    if ($resourceData.CurrentVaultValue.GetType().Name -eq 'SecureString')
    {
        $Password = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ($UserName, $resourceData.CurrentVaultValue)
    }

    $PSBoundParameters += @{
        Password = $Password
    }  

    if (Test-IsNanoServer)
    {
        Test-TargetResourceOnNanoServer @PSBoundParameters
    }
    else
    {
        Test-TargetResourceOnFullSKU @PSBoundParameters
    }  
}

<#
    .SYNOPSIS
    Retrieves the user with the given username when on a full server
 
    .PARAMETER UserName
    The name of the user to retrieve.
#>

function Get-TargetResourceOnFullSKU
{
    [OutputType([Hashtable])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $UserName
    )

    Set-StrictMode -Version Latest

    Assert-UserNameValid -UserName $UserName

    $disposables = @()

    try
    {
        Write-Verbose -Message 'Starting Get-TargetResource on FullSKU'

        $user = Find-UserByNameOnFullSku -UserName $UserName
        $disposables += $user
        $valuesToReturn = @{}

        if ($null -ne $user)
        {
            $valuesToReturn = @{
                UserName                 = $user.Name
                Ensure                   = 'Present'
                FullName                 = $user.DisplayName
                Description              = $user.Description
                Disabled                 = (-not $user.Enabled)
                PasswordNeverExpires     = $user.PasswordNeverExpires
                PasswordChangeRequired   = $null
                PasswordChangeNotAllowed = $user.UserCannotChangePassword
            }
        }
        else
        {
            # The user is not found. Return Ensure = Absent.
            $valuesToReturn = @{
                UserName = $UserName
                Ensure   = 'Absent'
            }
        }

        return $valuesToReturn
    }
    catch
    {
        New-InvalidOperationException -Message ($script:localizedData.MultipleMatches + $_)
    }
    finally
    {
        Remove-DisposableObject -Disposables $disposables
    }
}

<#
    .SYNOPSIS
    Creates, modifies, or deletes a user when on a full server.
     
    .PARAMETER UserName
    The name of the user to create, modify, or delete.
 
    .PARAMETER Ensure
    Specifies whether the user should exist or not.
    By default this is set to Present
 
    .PARAMETER FullName
    The (optional) full name or display name of the user.
    If not provided this value will remain blank.
 
    .PARAMETER Description
    Optional description for the user.
 
    .PARAMETER Password
    The desired password for the user.
 
    .PARAMETER Disabled
    Specifies whether the user should be disabled or not.
    By default this is set to $false
 
    .PARAMETER PasswordNeverExpires
    Specifies whether the password should ever expire or not.
    By default this is set to $false
 
    .PARAMETER PasswordChangeRequired
    Specifies whether the user must reset their password or not.
    By default this is set to $false
 
    .PARAMETER PasswordChangeNotAllowed
    Specifies whether the user is allowed to change their password or not.
    By default this is set to $false
 
    .NOTES
    If Ensure is set to 'Present' then the Password parameter is required.
#>

function Set-TargetResourceOnFullSKU
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $UserName,

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

        [String]
        $FullName,

        [String]
        $Description,

        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Password,

        [Boolean]
        $Disabled,

        [Boolean]
        $PasswordNeverExpires,

        [Boolean]
        $PasswordChangeRequired,

        [Boolean]
        $PasswordChangeNotAllowed,

        [parameter(Mandatory = $true)]
        [System.String]
        $VaultAddress,

        [parameter(Mandatory = $true)]
        [System.String]
        $VaultPath,

        [System.String]
        $ApiPrefix,

        [System.String]
        $AuthBackend = 'approle'
    )

    Set-StrictMode -Version Latest

    Write-Verbose -Message ($script:localizedData.ConfigurationStarted -f $UserName)

    Assert-UserNameValid -UserName $UserName

    $disposables = @()

    try
    {
        if ($Ensure -eq 'Present')
        {
            try
            {
                $user = Find-UserByNameOnFullSku -UserName $UserName
            }
            catch
            {
                $disposables += $user
                New-InvalidOperationException -Message ($script:localizedData.MultipleMatches + $_)
            }

            $disposables += $user

            $userExists = $false
            $saveChanges = $false

            if ($null -eq $user)
            {
                Write-Verbose -Message ($script:localizedData.UserWithName -f $UserName, $script:localizedData.AddOperation)
            }
            else
            {
                $userExists = $true
                Write-Verbose -Message ($script:localizedData.UserWithName -f $UserName, $script:localizedData.SetOperation)
            }

            if (-not $userExists)
            {
                # The user with the provided name does not exist so add a new user
                if ($PSBoundParameters.ContainsKey('Password'))
                {
                    $user = Add-UserOnFullSku -UserName $UserName -Password $Password
                }
                else
                {
                    $user = Add-UserOnFullSku -UserName $UserName
                }

                $saveChanges = $true
            }

            # Set user properties.
            if ($PSBoundParameters.ContainsKey('FullName') -and ((-not $userExists) -or ($FullName -ne $user.DisplayName)))
            {
                $user.DisplayName = $FullName
                $saveChanges = $true
            }
            elseif (-not $userExists)
            {
                <#
            For a newly created user, set the DisplayName property to an empty string
            since by default DisplayName is set to user's name.
        #>

                $user.DisplayName = [String]::Empty
            }

            if ($PSBoundParameters.ContainsKey('Description') -and ((-not $userExists) -or ($Description -ne $user.Description)))
            {
                $user.Description = $Description
                $saveChanges = $true
            }

            if ($PSBoundParameters.ContainsKey('Password') -and $userExists)
            {
                Set-UserPasswordOnFullSku -User $user -Password $Password
                $saveChanges = $true
            }

            if ($PSBoundParameters.ContainsKey('Disabled') -and ((-not $userExists) -or ($Disabled -eq $user.Enabled)))
            {
                $user.Enabled = -not $Disabled
                $saveChanges = $true
            }

            if ($PSBoundParameters.ContainsKey('PasswordNeverExpires') -and ((-not $userExists) -or ($PasswordNeverExpires -ne $user.PasswordNeverExpires)))
            {
                $user.PasswordNeverExpires = $PasswordNeverExpires
                $saveChanges = $true
            }

            if ($PSBoundParameters.ContainsKey('PasswordChangeRequired') -and $PasswordChangeRequired)
            {
                # Expire the password which will force the user to change the password at the next logon
                Revoke-UserPassword -User $user
                $saveChanges = $true
            }

            if ($PSBoundParameters.ContainsKey('PasswordChangeNotAllowed') -and ((-not $userExists) -or ($PasswordChangeNotAllowed -ne $user.UserCannotChangePassword)))
            {
                $user.UserCannotChangePassword = $PasswordChangeNotAllowed
                $saveChanges = $true
            }

            if ($saveChanges)
            {
                Save-UserOnFullSku -User $user

                # Send an operation success verbose message
                if ($userExists)
                {
                    Write-Verbose -Message ($script:localizedData.UserUpdated -f $UserName)
                }
                else
                {
                    Write-Verbose -Message ($script:localizedData.UserCreated -f $UserName)
                }
            }
            else
            {
                Write-Verbose -Message ($script:localizedData.NoConfigurationRequired -f $UserName)
            }
        }
        else
        {
            Remove-UserOnFullSku -UserName $UserName
        }

        Write-Verbose -Message ($script:localizedData.ConfigurationCompleted -f $UserName)
    }
    catch
    {
        New-InvalidOperationException -Message $_
    }
    finally
    {
        Remove-DisposableObject -Disposables $disposables
    }
}

<#
    .SYNOPSIS
    Tests if a user is in the desired state when on a full server.
 
    .PARAMETER UserName
    The name of the user to test the state of.
 
    .PARAMETER Ensure
    Specifies whether the user should exist or not.
    By default this is set to Present
 
    .PARAMETER FullName
    The full name/display name that the user should have.
    If not provided, this value will not be tested.
 
    .PARAMETER Description
    The description that the user should have.
    If not provided, this value will not be tested.
 
    .PARAMETER Password
    The password the user should have.
 
    .PARAMETER Disabled
    Specifies whether the user account should be disabled or not.
 
    .PARAMETER PasswordNeverExpires
    Specifies whether the password should ever expire or not.
 
    .PARAMETER PasswordChangeRequired
    Not used in Test-TargetResource as there is no easy way to test this value.
 
    .PARAMETER PasswordChangeNotAllowed
    Specifies whether the user should be allowed to change their password or not.
#>

function Test-TargetResourceOnFullSKU
{
    [OutputType([Boolean])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $UserName,

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

        [String]
        $FullName,

        [String]
        $Description,

        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Password,

        [Boolean]
        $Disabled,

        [Boolean]
        $PasswordNeverExpires,

        [Boolean]
        $PasswordChangeRequired,

        [Boolean]
        $PasswordChangeNotAllowed,

        [parameter(Mandatory = $true)]
        [System.String]
        $VaultAddress,

        [parameter(Mandatory = $true)]
        [System.String]
        $VaultPath,

        [System.String]
        $ApiPrefix,

        [System.String]
        $AuthBackend = 'approle'
    )

    Set-StrictMode -Version Latest

    Assert-UserNameValid -UserName $UserName

    $disposables = @()

    try
    {
        $user = Find-UserByNameOnFullSku -UserName $UserName
        $disposables += $user

        $inDesiredState = $true

        if ($null -eq $user)
        {
            # A user with the provided name does not exist
            Write-Verbose -Message ($script:localizedData.UserDoesNotExist -f $UserName)

            if ($Ensure -eq 'Absent')
            {
                return $true
            }
            else
            {
                return $false
            }
        }

        # A user with the provided name exists
        Write-Verbose -Message ($script:localizedData.UserExists -f $UserName)

        # Validate separate properties
        if ($Ensure -eq 'Absent')
        {
            # The Ensure property does not match
            Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Ensure', 'Absent', 'Present')
            $inDesiredState = $false
        }

        if ($PSBoundParameters.ContainsKey('FullName') -and $FullName -ne $user.DisplayName)
        {
            # The FullName property does not match
            Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'FullName', $FullName, $user.DisplayName)
            $inDesiredState = $false
        }

        if ($PSBoundParameters.ContainsKey('Description') -and $Description -ne $user.Description)
        {
            # The Description property does not match
            Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Description', $Description, $user.Description)
            $inDesiredState = $false
        }

        # Password
        if ($PSBoundParameters.ContainsKey('Password'))
        {
            if (-not (Test-UserPasswordOnFullSku -UserName $UserName -Password $Password))
            {
                # The Password property does not match
                Write-Verbose -Message ($script:localizedData.PasswordPropertyMismatch -f 'Password')
                $inDesiredState = $false
            }
        }

        if ($PSBoundParameters.ContainsKey('Disabled') -and $Disabled -eq $user.Enabled)
        {
            # The Disabled property does not match
            Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Disabled', $Disabled, $user.Enabled)
            $inDesiredState = $false
        }

        if ($PSBoundParameters.ContainsKey('PasswordNeverExpires') -and $PasswordNeverExpires -ne $user.PasswordNeverExpires)
        {
            # The PasswordNeverExpires property does not match
            Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'PasswordNeverExpires', $PasswordNeverExpires, $user.PasswordNeverExpires)
            $inDesiredState = $false
        }

        if ($PSBoundParameters.ContainsKey('PasswordChangeNotAllowed') -and $PasswordChangeNotAllowed -ne $user.UserCannotChangePassword)
        {
            # The PasswordChangeNotAllowed property does not match
            Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'PasswordChangeNotAllowed', $PasswordChangeNotAllowed, $user.UserCannotChangePassword)
            $inDesiredState = $false
        }

        if ($inDesiredState)
        {
            Write-Verbose -Message ($script:localizedData.AllUserPropertiesMatch -f 'User', $UserName)
        }

        return $inDesiredState
    }
    catch
    {
        New-InvalidOperationException -Message ($script:localizedData.MultipleMatches + $_)
    }
    finally
    {
        Remove-DisposableObject -Disposables $disposables
    }
}


<#
    .SYNOPSIS
    Retrieves the user with the given username when on Nano Server.
 
    .PARAMETER UserName
    The name of the user to retrieve.
#>

function Get-TargetResourceOnNanoServer
{
    [OutputType([Hashtable])]
    [CmdletBinding()]
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $UserName
    )

    Assert-UserNameValid -UserName $UserName

    $returnValue = @{}

    # Try to find a user by a name
    try
    {
        Write-Verbose -Message 'Starting Get-TargetResource on NanoServer'
        $user = Find-UserByNameOnNanoServer -UserName $UserName

        # The user is found. Return all user properties and Ensure = 'Present'.
        $returnValue = @{
            UserName                 = $user.Name
            Ensure                   = 'Present'
            FullName                 = $user.FullName
            Description              = $user.Description
            Disabled                 = -not $user.Enabled
            PasswordChangeRequired   = $null
            PasswordChangeNotAllowed = -not $user.UserMayChangePassword
        }

        if ($user.PasswordExpires)
        {
            $returnValue.Add('PasswordNeverExpires', $false)
        }
        else
        {
            $returnValue.Add('PasswordNeverExpires', $true)
        }
    }
    catch [System.Exception]
    {
        if ($_.FullyQualifiedErrorId -match 'UserNotFound')
        {
            # The user is not found
            $returnValue = @{
                UserName = $UserName
                Ensure   = 'Absent'
            }
        }
        else
        {
            New-InvalidOperationException -ErrorRecord $_
        }
    }

    return $returnValue
}

<#
    .SYNOPSIS
    Creates, modifies, or deletes a user when on Nano Server.
     
    .PARAMETER UserName
    The name of the user to create, modify, or delete.
 
    .PARAMETER Ensure
    Specifies whether the user should exist or not.
    By default this is set to Present
 
    .PARAMETER FullName
    The (optional) full name or display name of the user.
    If not provided this value will remain blank.
 
    .PARAMETER Description
    Optional description for the user.
 
    .PARAMETER Password
    The desired password for the user.
 
    .PARAMETER Disabled
    Specifies whether the user should be disabled or not.
    By default this is set to $false
 
    .PARAMETER PasswordNeverExpires
    Specifies whether the password should ever expire or not.
    By default this is set to $false
 
    .PARAMETER PasswordChangeRequired
    Specifies whether the user must reset their password or not.
    By default this is set to $false
 
    .PARAMETER PasswordChangeNotAllowed
    Specifies whether the user is allowed to change their password or not.
    By default this is set to $false
 
    .NOTES
    If Ensure is set to 'Present' then the Password parameter is required.
#>

function Set-TargetResourceOnNanoServer
{
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $UserName,

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

        [String]
        $FullName,

        [String]
        $Description,

        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Password,

        [Boolean]
        $Disabled,

        [Boolean]
        $PasswordNeverExpires,

        [Boolean]
        $PasswordChangeRequired,

        [Boolean]
        $PasswordChangeNotAllowed,

        [parameter(Mandatory = $true)]
        [System.String]
        $VaultAddress,

        [parameter(Mandatory = $true)]
        [System.String]
        $VaultPath,

        [System.String]
        $ApiPrefix,

        [System.String]
        $AuthBackend = 'approle'
    )

    Set-StrictMode -Version Latest

    Write-Verbose -Message ($script:localizedData.ConfigurationStarted -f $UserName)

    Assert-UserNameValid -UserName $UserName

    # Try to find a user by a name.
    $userExists = $false
    
    try
    {
        $user = Find-UserByNameOnNanoServer -UserName $UserName
        $userExists = $true
    }
    catch [System.Exception]
    {
        if ($_.FullyQualifiedErrorId -match 'UserNotFound')
        {
            # The user is not found.
            Write-Verbose -Message ($script:localizedData.UserDoesNotExist -f $UserName)
        }
        else
        {
            New-InvalidOperationException -ErrorRecord $_
        }
    }

    if ($Ensure -eq 'Present')
    {
        # Ensure is set to 'Present'

        if (-not $userExists)
        {
            # The user with the provided name does not exist so add a new user
            New-LocalUser -Name $UserName -NoPassword
            Write-Verbose -Message ($script:localizedData.UserCreated -f $UserName)
        }

        # Set user properties
        if ($PSBoundParameters.ContainsKey('FullName'))
        {
            if (-not $userExists -or $FullName -ne $user.FullName)
            {
                Set-LocalUser -Name $UserName -FullName $FullName
            }
        }
        elseif (-not $userExists)
        {
            # For a newly created user, set the DisplayName property to an empty string since by default DisplayName is set to user's name.
            Set-LocalUser -Name $UserName -FullName ([String]::Empty)
        }

        if ($PSBoundParameters.ContainsKey('Description') -and ((-not $userExists) -or ($Description -ne $user.Description)))
        {
            Set-LocalUser -Name $UserName -Description $Description
        }

        # Set the password regardless of the state of the user
        if ($PSBoundParameters.ContainsKey('Password'))
        {
            Set-LocalUser -Name $UserName -Password $Password.Password
        }

        if ($PSBoundParameters.ContainsKey('Disabled') -and ((-not $userExists) -or ($Disabled -eq $user.Enabled)))
        {
            if ($Disabled)
            {
                Disable-LocalUser -Name $UserName
            }
            else
            {
                Enable-LocalUser -Name $UserName
            }
        }

        $existingUserPasswordNeverExpires = (($userExists) -and ($null -eq $user.PasswordExpires))
        if ($PSBoundParameters.ContainsKey('PasswordNeverExpires') -and ((-not $userExists) -or ($PasswordNeverExpires -ne $existingUserPasswordNeverExpires)))
        {
            Set-LocalUser -Name $UserName -PasswordNeverExpires:$PasswordNeverExpires
        }

        # Only set the AccountExpires attribute if PasswordChangeRequired is set to true
        if ($PSBoundParameters.ContainsKey('PasswordChangeRequired') -and ($PasswordChangeRequired))
        {
            Set-LocalUser -Name $UserName -AccountExpires ([DateTime]::Now)
        }

        # NOTE: The parameter name and the property name have opposite meaning.
        $expected = (-not $PasswordChangeNotAllowed)
        $actual = $expected
        
        if ($userExists)
        {
            $actual = $user.UserMayChangePassword
        }
        
        if ($PSBoundParameters.ContainsKey('PasswordChangeNotAllowed') -and ((-not $userExists) -or ($expected -ne $actual)))
        {
            Set-LocalUser -Name $UserName -UserMayChangePassword $expected
        }
    }
    else
    {
        # Ensure is set to 'Absent'
        if ($userExists)
        {
            # The user exists
            Remove-LocalUser -Name $UserName

            Write-Verbose -Message ($script:localizedData.UserRemoved -f $UserName)
        }
        else
        {
            Write-Verbose -Message ($script:localizedData.NoConfigurationRequiredUserDoesNotExist -f $UserName)
        }
    }

    Write-Verbose -Message ($script:localizedData.ConfigurationCompleted -f $UserName)
}

<#
    .SYNOPSIS
    Tests if a user is in the desired state when on Nano Server.
 
    .PARAMETER UserName
    The name of the user to test the state of.
 
    .PARAMETER Ensure
    Specifies whether the user should exist or not.
    By default this is set to Present
 
    .PARAMETER FullName
    The full name/display name that the user should have.
    If not provided, this value will not be tested.
 
    .PARAMETER Description
    The description that the user should have.
    If not provided, this value will not be tested.
 
    .PARAMETER Password
    The password the user should have.
 
    .PARAMETER Disabled
    Specifies whether the user account should be disabled or not.
 
    .PARAMETER PasswordNeverExpires
    Specifies whether the password should ever expire or not.
 
    .PARAMETER PasswordChangeRequired
    Not used in Test-TargetResource as there is no easy way to test this value.
 
    .PARAMETER PasswordChangeNotAllowed
    Specifies whether the user should be allowed to change their password or not.
#>

function Test-TargetResourceOnNanoServer
{
    [OutputType([Boolean])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $UserName,

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

        [String]
        $FullName,

        [String]
        $Description,

        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Password,

        [Boolean]
        $Disabled,

        [Boolean]
        $PasswordNeverExpires,

        [Boolean]
        $PasswordChangeRequired,

        [Boolean]
        $PasswordChangeNotAllowed,

        [parameter(Mandatory = $true)]
        [System.String]
        $VaultAddress,

        [parameter(Mandatory = $true)]
        [System.String]
        $VaultPath,

        [System.String]
        $ApiPrefix,

        [System.String]
        $AuthBackend = 'approle'
    )

    Assert-UserNameValid -UserName $UserName

    # Try to find a user by a name
    try
    {
        $user = Find-UserByNameOnNanoServer -UserName $UserName
    }
    catch [System.Exception]
    {
        if ($_.FullyQualifiedErrorId -match 'UserNotFound')
        {
            # The user is not found
            return ($Ensure -eq 'Absent')
        }
        else
        {
            New-InvalidOperationException -ErrorRecord $_
        }
    }

    # A user with the provided name exists
    Write-Verbose -Message ($script:localizedData.UserExists -f $UserName)

    # Validate separate properties
    if ($Ensure -eq 'Absent')
    {
        # The Ensure property does not match
        Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Ensure', 'Absent', 'Present')
        return $false
    }

    if ($PSBoundParameters.ContainsKey('FullName') -and $FullName -ne $user.FullName)
    {
        # The FullName property does not match
        Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'FullName', $FullName, $user.FullName)
        return $false
    }

    if ($PSBoundParameters.ContainsKey('Description') -and $Description -ne $user.Description)
    {
        # The Description property does not match
        Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Description', $Description, $user.Description)
        return $false
    }

    if ($PSBoundParameters.ContainsKey('Password'))
    {
        if (-not (Test-CredentialsValidOnNanoServer -UserName $UserName -Password $Password.Password))
        {
            # The Password property does not match
            Write-Verbose -Message ($script:localizedData.PasswordPropertyMismatch -f 'Password')
            return $false
        }
    }

    if ($PSBoundParameters.ContainsKey('Disabled') -and ($Disabled -eq $user.Enabled))
    {
        # The Disabled property does not match
        Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Disabled', $Disabled, $user.Enabled)
        return $false
    }

    $existingUserPasswordNeverExpires = ($null -eq $user.PasswordExpires)
    if ($PSBoundParameters.ContainsKey('PasswordNeverExpires') -and $PasswordNeverExpires -ne $existingUserPasswordNeverExpires)
    {
        # The PasswordNeverExpires property does not match
        Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'PasswordNeverExpires', $PasswordNeverExpires, $existingUserPasswordNeverExpires)
        return $false
    }

    if ($PSBoundParameters.ContainsKey('PasswordChangeNotAllowed') -and $PasswordChangeNotAllowed -ne (-not $user.UserMayChangePassword))
    {
        # The PasswordChangeNotAllowed property does not match
        Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'PasswordChangeNotAllowed', $PasswordChangeNotAllowed, (-not $user.UserMayChangePassword))
        return $false
    }

    # All properties match. Return $true.
    Write-Verbose -Message ($script:localizedData.AllUserPropertiesMatch -f 'User', $UserName)
    return $true
}

<#
    .SYNOPSIS
    Checks that the username does not contain invalid characters.
 
    .PARAMETER UserName
    The username to validate.
#>

function Assert-UserNameValid
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $UserName
    )

    # Check if the name consists of only periods and/or white spaces
    $wrongName = $true
    
    for ($i = 0; $i -lt $UserName.Length; $i++)
    {
        if (-not [Char]::IsWhiteSpace($UserName, $i) -and $UserName[$i] -ne '.')
        {
            $wrongName = $false
            break
        }
    }

    $invalidChars = @('\', '/', '"', '[', ']', ':', '|', '<', '>', '+', '=', ';', ',', '?', '*', '@')

    if ($wrongName)
    {
        New-InvalidArgumentException `
            -Message ($script:localizedData.InvalidUserName -f $UserName, [String]::Join(' ', $invalidChars)) `
            -ArgumentName 'UserName'
    }

    if ($UserName.IndexOfAny($invalidChars) -ne -1)
    {
        New-InvalidArgumentException `
            -Message ($script:localizedData.InvalidUserName -f $UserName, [String]::Join(' ', $invalidChars)) `
            -ArgumentName 'UserName'
    }
}

<#
    .SYNOPSIS
    Tests the local user's credentials on the local machine.
     
    .PARAMETER UserName
    The username to validate the credentials of.
 
    .PARAMETER Password
    The password of the given user.
#>

function Test-CredentialsValidOnNanoServer
{
    [OutputType([Boolean])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $UserName,

        [ValidateNotNullOrEmpty()]
        [SecureString]
        $Password
    )

    $source = @'
        [Flags]
        private enum LogonType
        {
            Logon32LogonInteractive = 2,
            Logon32LogonNetwork,
            Logon32LogonBatch,
            Logon32LogonService,
            Logon32LogonUnlock,
            Logon32LogonNetworkCleartext,
            Logon32LogonNewCredentials
        }
        [Flags]
        private enum LogonProvider
        {
            Logon32ProviderDefault = 0,
            Logon32ProviderWinnt35,
            Logon32ProviderWinnt40,
            Logon32ProviderWinnt50
        }
        [DllImport("api-ms-win-security-logon-l1-1-1.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        private static extern Boolean LogonUser(
            String lpszUserName,
            String lpszDomain,
            IntPtr lpszPassword,
            LogonType dwLogonType,
            LogonProvider dwLogonProvider,
            out IntPtr phToken
            );
        [DllImport("api-ms-win-core-handle-l1-1-0.dll",
            EntryPoint = "CloseHandle", SetLastError = true,
            CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
        internal static extern bool CloseHandle(IntPtr handle);
        public static bool ValidateCredentials(string username, SecureString password)
        {
            IntPtr tokenHandle = IntPtr.Zero;
            IntPtr unmanagedPassword = IntPtr.Zero;
            unmanagedPassword = SecureStringMarshal.SecureStringToCoTaskMemUnicode(password);
            try
            {
                return LogonUser(
                    username,
                    null,
                    unmanagedPassword,
                    LogonType.Logon32LogonInteractive,
                    LogonProvider.Logon32ProviderDefault,
                    out tokenHandle);
            }
            catch
            {
                return false;
            }
            finally
            {
                if (tokenHandle != IntPtr.Zero)
                {
                    CloseHandle(tokenHandle);
                }
                if (unmanagedPassword != IntPtr.Zero) {
                    Marshal.ZeroFreeCoTaskMemUnicode(unmanagedPassword);
                }
                unmanagedPassword = IntPtr.Zero;
            }
        }
'@


    $null = Add-Type -PassThru -Namespace Microsoft.Windows.DesiredStateConfiguration.NanoServer.UserResource `
        -Name CredentialsValidationTool -MemberDefinition $source -UsingNamespace System.Security -ReferencedAssemblies System.Security.SecureString.dll
    return [Microsoft.Windows.DesiredStateConfiguration.NanoServer.UserResource.CredentialsValidationTool]::ValidateCredentials($UserName, $Password)
}
<#
    .SYNOPSIS
    Queries a user by the given username. If found the function returns a UserPrincipal object.
    Otherwise, the function returns $null.
     
    .PARAMETER UserName
    The username to search for.
#>

function Find-UserByNameOnFullSku
{
    [OutputType([System.DirectoryServices.AccountManagement.UserPrincipal])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $UserName
    )

    $principalContext = New-Object `
        -TypeName System.DirectoryServices.AccountManagement.PrincipalContext `
        -ArgumentList ([System.DirectoryServices.AccountManagement.ContextType]::Machine)

    $user = [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($principalContext, $UserName)

    return $user
}

<#
    .SYNOPSIS
    Adds a user with the given username and returns the new user object
     
    .PARAMETER UserName
    The username for the new user
#>

function Add-UserOnFullSku
{
    [OutputType([System.DirectoryServices.AccountManagement.UserPrincipal])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $UserName,

        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Password
    )

    $principalContext = New-Object `
        -TypeName 'System.DirectoryServices.AccountManagement.PrincipalContext' `
        -ArgumentList @( [System.DirectoryServices.AccountManagement.ContextType]::Machine )

    $user = New-Object -TypeName 'System.DirectoryServices.AccountManagement.UserPrincipal' `
        -ArgumentList @( $principalContext )
    $user.Name = $UserName

    if ($PSBoundParameters.ContainsKey('Password'))
    {
        $user.SetPassword($Password.GetNetworkCredential().Password)
    }

    return $user
}

<#
    .SYNOPSIS
    Sets the password for the given user
     
    .PARAMETER User
    The user to set the password for
 
    .PARAMETER Password
    The credential to use for the user's password
#>

function Set-UserPasswordOnFullSku
{
    [OutputType([System.DirectoryServices.AccountManagement.UserPrincipal])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.DirectoryServices.AccountManagement.UserPrincipal]
        $user,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Password
    )

    $user.SetPassword($Password.GetNetworkCredential().Password)
}

<#
    .SYNOPSIS
    Validates the password is correct for the given user. Returns $true if the
    Password is correct for the given username, false otherwise.
     
    .PARAMETER UserName
    The UserName to check
 
    .PARAMETER Password
    The credential to check
#>

function Test-UserPasswordOnFullSku
{
    [OutputType([Boolean])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $UserName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Password
    )

    $principalContext = New-Object `
        -TypeName 'System.DirectoryServices.AccountManagement.PrincipalContext' `
        -ArgumentList @( [System.DirectoryServices.AccountManagement.ContextType]::Machine )
    try
    {
        $credentailsValid = $principalContext.ValidateCredentials($UserName, $Password.GetNetworkCredential().Password)
        return $credentailsValid
    }
    finally
    {
        $principalContext.Dispose()
    }
}


<#
    .SYNOPSIS
    Queries a user by the given username. If found the function returns a UserPrincipal object.
    Otherwise, the function returns $null.
     
    .PARAMETER UserName
    The username to search for.
#>

function Remove-UserOnFullSku
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $UserName
    )

    $user = Find-UserByNameOnFullSku -Username $UserName

    if ($null -ne $user)
    {
        try
        {
            Write-Verbose -Message ($script:localizedData.UserWithName -f $UserName, $script:localizedData.RemoveOperation)
            $user.Delete()
            Write-Verbose -Message ($script:localizedData.UserRemoved -f $UserName)
        }
        finally
        {
            $user.Dispose()
        }
    }
    else
    {
        Write-Verbose -Message ($script:localizedData.NoConfigurationRequiredUserDoesNotExist -f $UserName)
    }
}

<#
    .SYNOPSIS
    Saves changes for the given user on a machine.
     
    .PARAMETER User
    The user to save the changes of
#>

function Save-UserOnFullSku
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.DirectoryServices.AccountManagement.UserPrincipal]
        $user
    )

    $user.Save()
}

<#
    .SYNOPSIS
    Expires the password of the given user.
     
    .PARAMETER User
    The user to expire the password of.
#>

function Revoke-UserPassword
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.DirectoryServices.AccountManagement.UserPrincipal]
        $user
    )

    $user.ExpirePasswordNow()
}

<#
    .SYNOPSIS
    Queries a user by the given username. If found the function returns a LocalUser object.
    Otherwise, the function throws an error that the user was not found.
     
    .PARAMETER UserName
    The username to search for.
#>

function Find-UserByNameOnNanoServer
{
    #[OutputType([Microsoft.PowerShell.Commands.LocalUser])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $UserName
    )

    return Get-LocalUser -Name $UserName -ErrorAction Stop
}

<#
    .SYNOPSIS
    Disposes of the contents of an array list containing IDisposable objects.
 
    .PARAMETER Disosables
    The array list of IDisposable Objects to dispose of.
#>

function Remove-DisposableObject
{
    [CmdletBinding()]
    param
    (
        [System.Collections.ArrayList]
        [AllowEmptyCollection()]
        $disposables
    )

    if ($null -ne $disposables)
    {
        foreach ($disposable in $disposables)
        {
            if ($disposable -is [System.IDisposable])
            {
                $disposable.Dispose()
            }
        }
    }
}

Export-ModuleMember -Function *-TargetResource