DSCResources/MSFT_xServiceResource/MSFT_xServiceResource.psm1

# Suppressed as per PSSA Rule Severity guidelines for unit/integration tests:
# https://github.com/PowerShell/DscResources/blob/master/PSSARuleSeverities.md
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
param ()

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

# Localized messages for Write-Verbose statements in this resource
$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_xServiceResource'

<#
    .SYNOPSIS
        Get the current status of a service.
 
    .PARAMETER Name
        Indicates the service name to retrieve. Note that sometimes this is different from the
        display name.
        You can get a list of the services and their current state with the Get-Service cmdlet.
#>

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

    if (Test-ServiceExists -Name $Name -ErrorAction SilentlyContinue)
    {
        Write-Verbose -Message 'Service exists - getting service'
        $service = Get-ServiceResource -Name $Name
        $serviceWmi = Get-Win32ServiceObject -Name $Name

        $builtInAccount = $null

        if ($serviceWmi.StartName -ieq 'LocalSystem')
        {
            $builtInAccount ='LocalSystem'
        }
        elseif ($serviceWmi.StartName -ieq 'NT Authority\NetworkService')
        {
            $builtInAccount = 'NetworkService'
        }
        elseif ($serviceWmi.StartName -ieq 'NT Authority\LocalService')
        {
            $builtInAccount = 'LocalService'
        }

        $dependencies = @()

        foreach ($serviceDependedOn in $service.ServicesDependedOn)
        {
            $dependencies += $serviceDependedOn.Name.ToString()
        }
        
        return @{
            Name            = $service.Name
            StartupType     = ConvertTo-StartupTypeString -StartMode $serviceWmi.StartMode
            BuiltInAccount  = $builtInAccount
            State           = $service.Status.ToString()
            Path            = $serviceWmi.PathName
            DisplayName     = $service.DisplayName
            Description     = $serviceWmi.Description
            DesktopInteract = $serviceWmi.DesktopInteract
            Dependencies    = $dependencies
            Ensure          = 'Present'
        }
    }
    else
    {
        Write-Verbose -Message 'Service with given name does not exist'
        return @{
            Name            = $service.Name
            Ensure          = 'Absent'
        }
    }
    
} # function Get-TargetResource

<#
    .SYNOPSIS
        Creates, updates or removes a service.
 
    .PARAMETER Name
        Indicates the name of the service to create, update, or remove.
        Note that sometimes this is different from the display name.
        You can get a list of the services and their current state with the Get-Service cmdlet.
 
    .PARAMETER Ensure
        Specifies whether the service should exist or not. Optional. Defaults to Present.
 
    .PARAMETER Path
        The path to the service executable file. Optional.
 
    .PARAMETER StartupType
        Indicates the startup type for the service. Optional.
 
    .PARAMETER BuiltInAccount
        Indicates the sign-in account to use for the service. Optional.
 
    .PARAMETER Credential
        The credential to run the service under. Optional.
 
    .PARAMETER DesktopInteract
        Indicates whether the service can create or communicate with a window on the desktop or not.
        Must be false for services not running as LocalSystem. Optional. Defaults to false.
 
    .PARAMETER State
        Indicates the state the service should be in. Optional. Default is Running.
 
    .PARAMETER DisplayName
        The display name of the service. Optional.
 
    .PARAMETER Description
        The description of the service. Optional.
 
    .PARAMETER Dependencies
        An array of strings indicating the names of the dependencies of the service. Optional.
 
    .PARAMETER StartupTimeout
        The time to wait for the service to start in milliseconds. Optional. Default is 3000.
 
    .PARAMETER TerminateTimeout
        The time to wait for the service to stop in milliseconds. Optional. Default is 3000.
#>

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

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

        [ValidateNotNullOrEmpty()]
        [String]
        $Path,

        [ValidateSet('Automatic', 'Manual', 'Disabled')]
        [String]
        $StartupType,

        [ValidateSet('LocalSystem', 'LocalService', 'NetworkService')]
        [String]
        $BuiltInAccount,

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

        [Boolean]
        $DesktopInteract,

        [ValidateSet('Running', 'Stopped', 'Ignore')]
        [String]
        $State = 'Running',

        [ValidateNotNullOrEmpty()]
        [String]
        $DisplayName,

        [ValidateNotNull()]
        [String]
        $Description,

        [ValidateNotNullOrEmpty()]
        [String[]]
        $Dependencies,

        [uint32]
        $StartupTimeout = 30000,

        [uint32]
        $TerminateTimeout = 30000
    )

    if ($PSBoundParameters.ContainsKey('StartupType'))
    {
        # Throw an exception if the requested StartupType conflicts with State
        Test-StartupType -Name $Name -StartupType $StartupType -State $State
    }

    $serviceExists = Test-ServiceExists -Name $Name -ErrorAction SilentlyContinue

    if (($Ensure -eq 'Absent') -and $serviceExists)
    {
        # The service exists but needs to be deleted
        Stop-ServiceResource -Name $Name -TerminateTimeout $TerminateTimeout
        Remove-Service -Name $Name -TerminateTimeout $TerminateTimeout
        return
    }

    if ($PSBoundParameters.ContainsKey('Path') -and $serviceExists)
    {
        if (-not (Compare-ServicePath -Name $Name -Path $Path))
        {
            # Update the path - this is not yet supported, but could be
            Write-Verbose -Message ($script:localizedData.ServiceExecutablePathChangeNotSupported)
        }
    }
    elseif ($PSBoundParameters.ContainsKey('Path') -and -not $serviceExists)
    {
        $argumentsToNewService = @{}
        $argumentsToNewService.Add('Name', $Name)
        $argumentsToNewService.Add('BinaryPathName', $Path)

        try
        {
            New-Service @argumentsToNewService
        }
        catch
        {
            Write-Verbose -Message ($script:localizedData.TestStartupTypeMismatch `
            -f $argumentsToNewService['Name'], $_.Exception.Message)
            throw $_
        }
    }
    elseif (-not $PSBoundParameters.ContainsKey('Path') -and -not $serviceExists)
    {
        New-InvalidArgumentException `
            -Message ($script:localizedData.ServiceDoesNotExistPathMissingError -f $Name) `
            -ArgumentName 'Path'
    }

    # Update the parameters of the service
    $writeWritePropertiesArguments = @{
        Name = $Name
    }

    $parameterNames = @('Path', 'StartupType', 'BuiltInAccount', 'Credential', 'DesktopInteract',
                        'DisplayName', 'Description', 'Dependencies')
    foreach ($parameter in $parameterNames)
    {
        if ($PSBoundParameters.ContainsKey($parameter))
        {
            $writeWritePropertiesArguments[$parameter] = $PSBoundParameters[$parameter]
        }
    }

    $requiresRestart = Write-WriteProperty @writeWritePropertiesArguments

    if ($State -eq 'Stopped')
    {
        # Ensure service is stopped
        Stop-ServiceResource -Name $Name -TerminateTimeout $TerminateTimeout
    }
    elseif ($State -eq 'Running')
    {
        # if the service needs to be restarted then go stop it first
        if ($requiresRestart)
        {
            Write-Verbose -Message ($script:localizedData.ServiceNeedsRestartMessage -f $Name)
            Stop-ServiceResource -Name $Name -TerminateTimeout $TerminateTimeout
        }

        Start-ServiceResource -Name $Name -StartupTimeout $StartupTimeout
    }
} # function Set-TargetResource


<#
    .SYNOPSIS
        Tests if a service is in the desired state.
 
    .PARAMETER Name
        The name of the service to be tested.
        Note that sometimes this is different from the display name.
        You can get a list of the services and their current state with the Get-Service cmdlet.
 
    .PARAMETER Ensure
        Specifies whether the service should exist or not. Optional. Defaults to Present.
        If set to Absent, only the existence of the service will be checked.
 
    .PARAMETER Path
        Indicates what the path to the service executable file should be. Optional.
 
    .PARAMETER StartupType
        Indicates what the startup type for the service should be. Optional.
 
    .PARAMETER BuiltInAccount
        Indicates what the sign-in account to use for the service should be. Optional.
 
    .PARAMETER Credential
        Indicates the credential that the service should run under. Optional.
 
    .PARAMETER DesktopInteract
        Indicates if the service should be able to create or communicate with a window on the desktop.
        Must be false for services not running as LocalSystem. Optional.
 
    .PARAMETER State
        Indicates the state that the service should be in. Optional. Default is Running.
 
    .PARAMETER DisplayName
        The display name that the service should have. Optional.
 
    .PARAMETER Description
        The description that the service should have. Optional.
 
    .PARAMETER Dependencies
        An array of strings indicating the names of the dependencies that the service should have.
        Optional.
 
    .PARAMETER StartupTimeout
        Not used in Test-TargetResource.
 
    .PARAMETER TerminateTimeout
        Not used in Test-TargetResource.
#>

function Test-TargetResource
{
    [OutputType([Boolean])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Name,
      
        [ValidateSet('Present', 'Absent')]
        [String]
        $Ensure = 'Present',

        [ValidateNotNullOrEmpty()]
        [String]
        $Path,

        [ValidateSet('Automatic', 'Manual', 'Disabled')]
        [String]
        $StartupType,

        [ValidateSet('LocalSystem', 'LocalService', 'NetworkService')]
        [String]
        $BuiltInAccount,

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

        [Boolean]
        $DesktopInteract,

        [ValidateSet('Running', 'Stopped', 'Ignore')]
        [String]
        $State = 'Running',

        [ValidateNotNullOrEmpty()]
        [String]
        $DisplayName,

        [ValidateNotNull()]
        [String]
        $Description,

        [ValidateNotNullOrEmpty()]
        [String[]]
        $Dependencies,

        [uint32]
        $StartupTimeout = 30000,

        [uint32]
        $TerminateTimeout = 30000
    )

    if ($PSBoundParameters.ContainsKey('StartupType'))
    {
        # Throw an exception if the StartupTypeconflicts with the state
        Test-StartupType -Name $Name -StartupType $StartupType -State $State
    }

    $serviceExists = Test-ServiceExists -Name $Name -ErrorAction SilentlyContinue

    if ($Ensure -eq 'Absent')
    {
        Write-Verbose -Message $script:localizedData.NotCheckingOtherValuesEnsureAbsent
        return -not $serviceExists
    }

    if (-not $serviceExists)
    {
        Write-Verbose -Message $script:localizedData.ServiceDoesNotExist
        return $false
    }

    $service = Get-ServiceResource -Name $Name
    $serviceWmi = Get-Win32ServiceObject -Name $Name

    # Check the binary path
    if ($PSBoundParameters.ContainsKey('Path') -and `
        (-not (Compare-ServicePath -Name $Name -Path $Path)))
    {
        Write-Verbose -Message ($script:localizedData.TestBinaryPathMismatch `
            -f $serviceWmi.Name, $serviceWmi.PathName, $Path)
        return $false
    }

    # Check the optional parameters
    if ($PSBoundParameters.ContainsKey('DisplayName') -and `
        ($DisplayName -ne $serviceWmi.DisplayName))
    {
        Write-Verbose -Message ($script:localizedData.ParameterMismatch `
            -f 'DisplayName', $serviceWmi.DisplayName, $DisplayName)
        return $false
    }

    if ($PSBoundParameters.ContainsKey('Description') -and `
        ($Description -ne $serviceWmi.Description))
    {
        Write-Verbose -Message ($script:localizedData.ParameterMismatch `
            -f 'Description', $serviceWmi.Description, $Description)
        return $false
    }

    if ($PSBoundParameters.ContainsKey('Dependencies'))
    {
        $mismatchedDependencies = @(Compare-Object `
                                      -ReferenceObject $service.ServicesDependedOn `
                                      -DifferenceObject $Dependencies `
                                   )

        if ($mismatchedDependencies.Count -gt 0)
        {
            Write-Verbose -Message ($script:localizedData.ParameterMismatch `
                -f 'Dependencies', ($service.ServicesDependedOn -join ','), ($Dependencies -join ','))
            return $false
        }
    }

    if ($PSBoundParameters.ContainsKey('StartupType') -or `
        $PSBoundParameters.ContainsKey('BuiltInAccount') -or `
        $PSBoundParameters.ContainsKey('Credential') -or `
        $PSBoundParameters.ContainsKey('DesktopInteract'))
    {
        $getUserNameAndPasswordArgs = @{}

        if ($PSBoundParameters.ContainsKey('BuiltInAccount'))
        {
            $null = $getUserNameAndPasswordArgs.Add('BuiltInAccount', $BuiltInAccount)
        }

        if ($PSBoundParameters.ContainsKey('Credential'))
        {
            $null = $getUserNameAndPasswordArgs.Add('Credential', $Credential)
        }

        $userName, $password = Get-UserNameAndPassword @getUserNameAndPasswordArgs

        if ($null -ne $userName  -and `
            -not (Test-UserName -ServiceWmi $serviceWmi -Username $userName))
        {
            Write-Verbose -Message ($script:localizedData.TestUserNameMismatch `
                -f $serviceWmi.Name, $serviceWmi.StartName, $userName)
            return $false
        }

        if ($PSBoundParameters.ContainsKey('DesktopInteract') -and `
            ($serviceWmi.DesktopInteract -ne $DesktopInteract))
        {
            Write-Verbose -Message ($script:localizedData.TestDesktopInteractMismatch `
                -f $serviceWmi.Name, $serviceWmi.DesktopInteract, $DesktopInteract)
            return $false
        }

        if ($PSBoundParameters.ContainsKey('StartupType') -and `
            $serviceWmi.StartMode -ine (ConvertTo-StartModeString -StartupType $StartupType))
        {
            Write-Verbose -Message ($script:localizedData.TestStartupTypeMismatch `
                -f $serviceWmi.Name, $serviceWmi.StartMode, $StartupType)
            return $false
        }
    }

    if (($State -ne $service.Status) -and ($State -ne 'Ignore'))
    {
        Write-Verbose -Message ($script:localizedData.TestStateMismatch `
            -f $serviceWmi.Name, $service.Status, $State)
        return $false
    }

    return $true
} # function Test-TargetResource


<#
    .SYNOPSIS
        Tests if the given StartupType is valid with the State parameter of the service
        with the given Name.
 
    .PARAMETER Name
        The name of the service for which to check the StartupType and State
        (For error message only)
 
    .PARAMETER StartupType
        The StartupType to test.
 
    .PARAMETER State
        The State to test against. Default state is 'Running'
#>

function Test-StartupType
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Name,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Automatic', 'Manual', 'Disabled')]
        [String]
        $StartupType,

        [ValidateSet('Running', 'Stopped', 'Ignore')]
        [String]
        $State = 'Running'
    )

    if ($State -eq 'Stopped')
    {
        if ($StartupType -eq 'Automatic')
        {
            # State = Stopped conflicts with Automatic or Delayed
            New-InvalidArgumentException `
                -Message ($script:localizedData.CannotStopServiceSetToStartAutomatically -f $Name) `
                -ArgumentName 'State'
        }
    }
    elseif ($State -eq 'Running')
    {
        if ($StartupType -eq 'Disabled')
        {
            # State = Running conflicts with Disabled
            New-InvalidArgumentException `
                -Message ($script:localizedData.CannotStartAndDisable -f $Name) `
                -ArgumentName 'State'
        }
    }
} # function Test-StartupType

<#
    .SYNOPSIS
        Converts the StartupType String to the correct StartMode String returned
        in the Win32 service object.
 
    .PARAMETER StartupType
        The StartupType to convert.
#>

function ConvertTo-StartModeString
{
    [OutputType([String])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('Automatic', 'Manual', 'Disabled')]
        [String] $StartupType
    )

    if ($StartupType -eq 'Automatic')
    {
        return 'Auto'
    }

    return $StartupType
} # function ConvertTo-StartModeString

<#
    .SYNOPSIS
        Converts the StartMode string returned in a Win32_Service object to the format
        expected by this resource.
 
    .PARAMETER StartMode
        The StartMode string to convert.
#>

function ConvertTo-StartupTypeString
{
    [OutputType([String])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('Auto', 'Manual', 'Disabled')]
        $StartMode
    )

    if ($StartMode -eq 'Auto')
    {
        return 'Automatic'
    }

    return $StartMode
} # function ConvertTo-StartupTypeString

<#
    .SYNOPSIS
        Retrieves the Win32_Service object for the service with the given name.
 
    .PARAMETER Name
        The name of the service for which to get the Win32_Service object
#>

function Get-Win32ServiceObject
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        $Name
    )

    return Get-CimInstance -ClassName Win32_Service -Filter "Name='$Name'"
} # function Get-Win32ServiceObject

<#
    .SYNOPSIS
        Sets the StartupType property of the given service to the given value.
 
    .PARAMETER Win32ServiceObject
        The Win32_Service object to set the StartupType to.
 
    .PARAMETER StartupType
        The StartupType to set
#>

function Set-ServiceStartMode
{
    [CmdletBinding(SupportsShouldProcess = $true)]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $Win32ServiceObject,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Automatic', 'Manual', 'Disabled')]
        [String]
        $StartupType
    )

    if ((ConvertTo-StartupTypeString -StartMode $Win32ServiceObject.StartMode) -ine $StartupType `
        -and $PSCmdlet.ShouldProcess($Win32ServiceObject.Name, $script:localizedData.SetStartupTypeWhatIf))
    {
        $changeServiceArguments = @{ StartMode = $StartupType }

        $changeResult = Invoke-CimMethod `
            -InputObject $Win32ServiceObject `
            -MethodName 'Change' `
            -Arguments $changeServiceArguments

        if ($changeResult.ReturnValue -ne 0)
        {
            $innerMessage = ($script:localizedData.MethodFailed `
                -f 'Change', 'Win32_Service', $changeResult.ReturnValue)
            $errorMessage = ($script:localizedData.ErrorChangingProperty `
                -f 'StartupType', $innerMessage)
            New-InvalidArgumentException `
                -Message $errorMessage `
                -ArgumentName 'StartupType'
        }
    }
} # function Set-ServiceStartMode

<#
    .SYNOPSIS
        Writes all write properties if not already correctly set.
        Logs errors and respects WhatIf.
 
    .PARAMETER Name
        The name of the service to be updated.
        Note that sometimes this is different from the display name.
        You can get a list of the services and their current state with the Get-Service cmdlet.
 
    .PARAMETER Path
        The path to the service executable file. Optional.
 
    .PARAMETER StartupType
        Indicates the startup type for the service. Optional.
 
    .PARAMETER BuiltInAccount
        Indicates the sign-in account to use for the service. Optional.
 
    .PARAMETER Credential
        The credential to run the service under. Optional.
 
    .PARAMETER DesktopInteract
        The service can create or communicate with a window on the desktop.
 
    .PARAMETER DisplayName
        The display name of the service. Optional.
 
    .PARAMETER Description
        The description of the service. Optional.
 
    .PARAMETER Dependencies
        An array of strings indicating the names of the dependencies of the service. Optional.
#>

function Write-WriteProperty
{
    [OutputType([System.Boolean])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $Name,

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

        [System.String]
        [ValidateSet('Automatic', 'Manual', 'Disabled')]
        $StartupType,

        [System.String]
        [ValidateSet('LocalSystem', 'LocalService', 'NetworkService')]
        $BuiltInAccount,

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

        [Boolean]
        $DesktopInteract,

        [ValidateNotNullOrEmpty()]
        [String]
        $DisplayName,

        [ValidateNotNull()]
        [String]
        $Description,

        [ValidateNotNullOrEmpty()]
        [String[]]
        $Dependencies
    )

    $service = Get-Service -Name $Name
    $serviceWmi = Get-Win32ServiceObject -Name $Name
    $requiresRestart = $false

    # update binary path
    if ($PSBoundParameters.ContainsKey('Path'))
    {
        $writeBinaryArguments = @{
            ServiceWmi = $serviceWmi
            Path = $Path
        }

        $requiresRestart = ($requiresRestart -or (Write-BinaryProperty @writeBinaryArguments))
    }

    # update misc service properties
    $serviceprops = @{}

    if ($PSBoundParameters.ContainsKey('DisplayName') -and `
        ($DisplayName -ne $serviceWmi.DisplayName))
    {
        $serviceprops += @{ DisplayName = $DisplayName }
    }

    if ($PSBoundParameters.ContainsKey('Description') -and `
        ($Description -ne $serviceWmi.Description))
    {
        $serviceprops += @{ Description = $Description }
    }

    if ($serviceprops.count -gt 0)
    {
        $null = Set-Service -Name $Name @ServiceProps
    }

    # update the service dependencies if required
    if ($PSBoundParameters.ContainsKey('Dependencies'))
    {
        $mismatchedDependencies = @(Compare-Object `
                                    -ReferenceObject $service.ServicesDependedOn `
                                    -DifferenceObject $Dependencies)

        if ($mismatchedDependencies.Count -gt 0)
        {
            $changeServiceArguments = @{ ServiceDependencies = $Dependencies }

            $changeResult = Invoke-CimMethod `
                -InputObject $serviceWmi `
                -MethodName 'Change' `
                -Arguments $changeServiceArguments

            if ($changeResult.ReturnValue -ne 0)
            {
                $innerMessage = ($script:localizedData.MethodFailed `
                    -f 'Change', 'Win32_Service', $changeResult.ReturnValue)
                $errorMessage = ($script:localizedData.ErrorChangingProperty `
                    -f 'Dependencies', $innerMessage)

                New-InvalidArgumentException `
                    -Message $errorMessage `
                    -ArgumentName 'Dependencies'
            }
        }
    }

    # update credentials
    if ($PSBoundParameters.ContainsKey('BuiltInAccount') -or `
        $PSBoundParameters.ContainsKey('Credential') -or `
        $PSBoundParameters.ContainsKey('DesktopInteract'))
    {
        $writeCredentialPropertiesArguments = @{ 'ServiceWmi' = $serviceWmi }

        if ($PSBoundParameters.ContainsKey('BuiltInAccount'))
        {
            $null = $writeCredentialPropertiesArguments.Add('BuiltInAccount', $BuiltInAccount)
        }

        if ($PSBoundParameters.ContainsKey('Credential'))
        {
            $null = $writeCredentialPropertiesArguments.Add('Credential', $Credential)
        }

        if ($PSBoundParameters.ContainsKey('DesktopInteract'))
        {
            $null = $writeCredentialPropertiesArguments.Add('DesktopInteract', $DesktopInteract)
        }

        Write-CredentialProperty @writeCredentialPropertiesArguments
    }

    # Update startup type
    if ($PSBoundParameters.ContainsKey('StartupType'))
    {
        Set-ServiceStartMode -Win32ServiceObject $serviceWmi -StartupType $StartupType
    }

    # Return restart status
    return $requiresRestart

} # function Write-WriteProperty

<#
    .SYNOPSIS
        Writes credential properties if not already correctly set.
        Logs errors and respects WhatIf.
 
    .PARAMETER ServiceWmi
        The WMI service of which to set the credentials.
 
    .PARAMETER BuiltInAccount
        Indicates the sign-in account to use for the service. Optional.
 
    .PARAMETER Credential
        The credential to update. Optional.
 
    .PARAMETER DesktopInteract
        Indicates whether the service can create or communicate with a window on the desktop.
        Must be false for services not running as LocalSystem. Optional.
#>

function Write-CredentialProperty
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $ServiceWmi,

        [System.String]
        [ValidateSet('LocalSystem', 'LocalService', 'NetworkService')]
        $BuiltInAccount,

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

        [Boolean]
        $DesktopInteract
    )

    if (-not $PSBoundParameters.ContainsKey('Credential') -and `
        -not $PSBoundParameters.ContainsKey('BuiltInAccount') -and `
        -not $PSBoundParameters.ContainsKey('DesktopInteract'))
    {
        # No change parameters actually passed - nothing to change
        return
    }

    # These are the arguments to chnage on the service
    $changeArgs = @{}

    # Get the Username and Password to change to (if applicable)
    $getUserNameAndPasswordArgs = @{}

    if ($PSBoundParameters.ContainsKey('BuiltInAccount'))
    {
        $null = $getUserNameAndPasswordArgs.Add('BuiltInAccount',$BuiltInAccount)
    }

    if ($PSBoundParameters.ContainsKey('Credential'))
    {
        $null = $getUserNameAndPasswordArgs.Add('Credential',$Credential)
    }

    if ($getUserNameAndPasswordArgs.Count -gt 1)
    {
        # Both Credentials and BuiltInAccount were set - throw
        New-InvalidArgumentException `
            -Message ($script:localizedData.OnlyOneParameterCanBeSpecified `
                -f 'Credential', 'BuiltInAccount') `
            -ArgumentName 'BuiltInAccount'
    }

    $userName, $password = Get-UserNameAndPassword @getUserNameAndPasswordArgs

    # If the user account needs to be changed add it to the arguments
    if (($null -ne $userName) -and `
        -not (Test-UserName -ServiceWmi $ServiceWmi -Username $userName))
    {
        # A specific user account was passed so set log on as a service policy
        if ($PSBoundParameters.ContainsKey('Credential'))
        {
            Set-LogOnAsServicePolicy -Username $userName
        }

        $changeArgs += @{
            StartName = $userName
            StartPassword = $password
        }
    }

    # The desktop interact flag was passed to set that value
    if ($PSBoundParameters.ContainsKey('DesktopInteract') -and `
        ($DesktopInteract -ne $ServiceWmi.DesktopInteract))
    {
        $changeArgs.DesktopInteract = $DesktopInteract
    }

    if ($changeArgs.Count -gt 0)
    {
        $ret = Invoke-CimMethod `
            -InputObject $ServiceWmi `
            -MethodName 'Change' `
            -Arguments $changeArgs

        if ($ret.ReturnValue -ne 0)
        {
            $innerMessage = ($script:localizedData.MethodFailed `
                -f 'Change', 'Win32_Service',$ret.ReturnValue)
            $errorMessage = ($script:localizedData.ErrorChangingProperty `
                -f 'Credential', $innerMessage)

            New-InvalidArgumentException `
                -Message $errorMessage `
                -ArgumentName 'Credential'
        }
    }
} # function Write-CredentialProperty

<#
    .SYNOPSIS
        Writes binary path if not already correctly set. Logs errors.
        returns false if the path is already set and true if it was not set.
 
    .PARAMETER ServiceWmi
        The WMI service of which to set the path
 
    .PARAMETER Path
        The Path to set for the service. Optional.
         
#>

function Write-BinaryProperty
{
    [OutputType([System.Boolean])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $ServiceWmi,

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

    if ($ServiceWmi.PathName -eq $Path)
    {
        return $false
    }

    $changeServiceArguments = @{ PathName = $Path }

    $changeResult = Invoke-CimMethod `
        -InputObject $serviceWmi `
        -MethodName 'Change' `
        -Arguments $changeServiceArguments

    if ($changeResult.ReturnValue -ne 0)
    {
        $innerMessage = ($script:localizedData.MethodFailed `
            -f 'Change', 'Win32_Service', $changeResult.ReturnValue)
        $errorMessage = ($script:localizedData.ErrorChangingProperty `
            -f 'Binary Path', $innerMessage)

        New-InvalidArgumentException `
            -Message $errorMessage `
            -ArgumentName 'Path'
    }

    return $true
} # function Write-BinaryProperty

<#
    .SYNOPSIS
        Returns true if the service's StartName matches $UserName
 
    .PARAMETER ServiceWmi
        The WMI service of which to check the username.
 
    .PARAMETER UserName
        The username of the user to compare the one in the WMI object with.
#>

function Test-UserName
{
    [OutputType([System.Boolean])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $ServiceWmi,

        [String]
        $UserName
    )

    return  ((Resolve-UserName -UserName $ServiceWmi.StartName) -ieq $UserName)
} # function Test-UserName

<#
    .SYNOPSIS
        If BuiltInAccount is provided, this will return the resolved username from BuiltInAccount.
        If Credential is provided, this will return the resolved username and password from Credential.
        If nothing is provided, this will return null for both username and password.
        If both parameters are provided the username from BuiltInAccount is returned.
 
    .PARAMETER BuiltInAccount
        The built in account to extract the username from. Optional
 
    .PARAMETER Credential
        The Credential to extract the username and password from. Optional.
 
    .OUTPUTS
        A tuple containing: [String] Username, [String] Password.
#>

function Get-UserNameAndPassword
{
    [CmdletBinding()]
    param
    (
        [System.String]
        [ValidateSet('LocalSystem', 'LocalService', 'NetworkService')]
        $BuiltInAccount,

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

    if ($PSBoundParameters.ContainsKey('BuiltInAccount'))
    {
        return (Resolve-UserName -UserName $BuiltInAccount.ToString()), $null
    }

    if ($PSBoundParameters.ContainsKey('Credential'))
    {
        return (Resolve-UserName -UserName $Credential.UserName), `
                $Credential.GetNetworkCredential().Password
    }

    return $null, $null
} # function Get-UserNameAndPassword

<#
    .SYNOPSIS
        Deletes a service
 
    .PARAMETER Name
        The name of the service to delete.
 
    .PARAMETER TerminateTimeout
        The number of milliseconds to wait for the service to be removed.
        Optional. Default value is 3000.
#>

function Remove-Service
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $Name,

        [ValidateNotNullOrEmpty()]
        [Int]
        $TerminateTimeout = 30000
    )

    # Delete the service
    & 'sc.exe' 'delete' "$Name"

    # Wait for the service to be deleted
    $serviceDeletedSuccessfully = $false
    $start = [DateTime]::Now

    while (-not $serviceDeletedSuccessfully -and `
          ([DateTime]::Now - $start).TotalMilliseconds -lt $TerminateTimeout)
    {
        if (-not (Test-ServiceExists -Name $Name))
        {
            $serviceDeletedSuccessfully = $true
            break
        }

        # The service was not deleted so wait a second and try again (unless TerminateTimeout is hit)
        Start-Sleep -Seconds 1
        Write-Verbose -Message ($script:localizedData.TryDeleteAgain)
    }

    if ($serviceDeletedSuccessfully)
    {
        Write-Verbose -Message ($script:localizedData.ServiceDeletedSuccessfully -f $Name)
    }
    else
    {
        # Service was not deleted
        New-InvalidOperationException `
            -Message ($script:localizedData.ErrorDeletingService -f $Name)
    }
} # function Remove-Service

<#
    .SYNOPSIS
        Starts a service if it is not already running.
 
    .PARAMETER Name
        The name of the service to start.
 
    .PARAMETER StartupTimeout
        The amount of time to wait for the service to start.
#>

function Start-ServiceResource
{
    [CmdletBinding(SupportsShouldProcess = $true)]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [String]
        $Name,

        [Parameter(Mandatory = $true)]
        [uint32]
        $StartupTimeout
    )

    $service = Get-ServiceResource -Name $Name

    if ($service.Status -eq [System.ServiceProcess.ServiceControllerStatus]::Running)
    {
        Write-Verbose -Message ($script:localizedData.ServiceAlreadyStarted -f $service.Name)
        return
    }

    if ($PSCmdlet.ShouldProcess($Name, $script:localizedData.StartServiceWhatIf))
    {
        try
        {
            $service.Start()
            $waitTimeSpan = New-Object `
                -TypeName TimeSpan `
                -ArgumentList ($StartupTimeout * 10000)
            $service.WaitForStatus([System.ServiceProcess.ServiceControllerStatus]::Running, `
                                    $waitTimeSpan)
        }
        catch
        {
            $servicePath = (Get-CimInstance -Class win32_service |
                Where-Object { $_.Name -eq $Name }).PathName
            $errorMessage = ($script:localizedData.ErrorStartingService `
                -f $service.Name, $servicePath, $_.Exception.Message)
            New-InvalidOperationException -Message $errorMessage
        }

        Write-Verbose -Message ($script:localizedData.ServiceStarted -f $service.Name)
    }
} # function Start-ServiceResource

<#
    .SYNOPSIS
        Stops a service if it is not already stopped.
 
    .PARAMETER Name
        The name of the service to stop.
 
    .PARAMETER TerminateTimeout
        The amount of time to wait for the service to stop.
#>

function Stop-ServiceResource
{
    [CmdletBinding(SupportsShouldProcess = $true)]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [String]
        $Name,

        [Parameter(Mandatory = $true)]
        [uint32]
        $TerminateTimeout
    )

    $service = Get-ServiceResource -Name $Name

    if ($service.Status -eq [System.ServiceProcess.ServiceControllerStatus]::Stopped)
    {
        Write-Verbose -Message ($script:localizedData.ServiceAlreadyStopped -f $service.Name)
        return
    }

    if ($PSCmdlet.ShouldProcess($Name, $script:localizedData.StopServiceWhatIf))
    {
        try
        {
            $service.Stop()
            $waitTimeSpan = New-Object `
                -TypeName TimeSpan `
                -ArgumentList ($TerminateTimeout * 10000)
            $service.WaitForStatus([System.ServiceProcess.ServiceControllerStatus]::Stopped, `
                                    $waitTimeSpan)
        }
        catch
        {
            Write-Verbose -Message ($script:localizedData.ErrorStoppingService `
                -f $service.Name, $_.Exception.Message)
            throw $_
        }

        Write-Verbose -Message ($script:localizedData.ServiceStopped -f $service.Name)
    }
} # function Stop-ServiceResource

<#
    .SYNOPSIS
        Converts the username returned from a Win32_service object to the format
        expected by this resource.
 
    .PARAMETER UserName
        The Username to convert.
#>


function Resolve-UserName
{
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [String]
        $UserName
    )

    switch ($Username)
    {
        'NetworkService'
        {
            return 'NT Authority\NetworkService'
        }
        'LocalService'
        {
            return 'NT Authority\LocalService'
        }
        'LocalSystem'
        {
            return '.\LocalSystem'
        }
        default
        {
            if ($UserName.IndexOf('\') -eq -1)
            {
                return '.\' + $UserName
            }
        }
    }

    return $UserName
} # function Resolve-UserName

<#
    .SYNOPSIS
        Tests if a service with the given name exists
 
    .PARAMETER Name
        The name of the service to test for.
#>

function Test-ServiceExists
{
    [OutputType([Boolean])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Name
    )

    $service = Get-Service -Name $Name -ErrorAction SilentlyContinue
    return ($null -ne $service)

} # function Test-ServiceExists

<#
    .SYNOPSIS
        Compares a path to the existing service path.
        Returns true when the given path is the same as the existing service path, false otherwise.
 
    .PARAMETER Name
        The name of the existing service for which to check the path.
 
    .PARAMETER Path
        The path to check against.
#>

function Compare-ServicePath
{
    [OutputType([Boolean])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Name,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Path
    )

    $existingServicePath = (Get-CimInstance -Class win32_service |
        Where-Object { $_.Name -eq $Name }).PathName
    $stringCompareResult = [String]::Compare($Path, $existingServicePath, `
                            [System.Globalization.CultureInfo]::CurrentUICulture)

    return ($stringCompareResult -eq 0)
} # function Compare-ServicePath

<#
    .SYNOPSIS
        Retrieves the service with the given name.
 
    .PARAMETER Name
        The name of the service to retrieve
#>

function Get-ServiceResource
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Name
    )

    $service = Get-Service -Name $Name -ErrorAction Ignore
    if ($null -eq $service)
    {
        New-InvalidArgumentException `
            -Message ($script:localizedData.ServiceNotFound -f $Name) `
            -ArgumentName 'Name'
    }

    return $service

} # function Get-ServiceResource

<#
    .SYNOPSIS
        Grants log on as service right to the given user
 
    .PARAMETER UserName
        The name of the user to grant log on as a service right to
#>

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

    $logOnAsServiceText = @"
        namespace LogOnAsServiceHelper
        {
            using Microsoft.Win32.SafeHandles;
            using System;
            using System.Runtime.ConstrainedExecution;
            using System.Runtime.InteropServices;
            using System.Security;
 
            public class NativeMethods
            {
                #region constants
                // from ntlsa.h
                private const int POLICY_LOOKUP_NAMES = 0x00000800;
                private const int POLICY_CREATE_ACCOUNT = 0x00000010;
                private const uint ACCOUNT_ADJUST_SYSTEM_ACCESS = 0x00000008;
                private const uint ACCOUNT_VIEW = 0x00000001;
                private const uint SECURITY_ACCESS_SERVICE_LOGON = 0x00000010;
 
                // from LsaUtils.h
                private const uint STATUS_OBJECT_NAME_NOT_FOUND = 0xC0000034;
 
                // from lmcons.h
                private const int UNLEN = 256;
                private const int DNLEN = 15;
 
                // Extra characteres for "\","@" etc.
                private const int EXTRA_LENGTH = 3;
                #endregion constants
 
                #region interop structures
                /// <summary>
                /// Used to open a policy, but not containing anything meaqningful
                /// </summary>
                [StructLayout(LayoutKind.Sequential)]
                private struct LSA_OBJECT_ATTRIBUTES
                {
                    public UInt32 Length;
                    public IntPtr RootDirectory;
                    public IntPtr ObjectName;
                    public UInt32 Attributes;
                    public IntPtr SecurityDescriptor;
                    public IntPtr SecurityQualityOfService;
 
                    public void Initialize()
                    {
                        this.Length = 0;
                        this.RootDirectory = IntPtr.Zero;
                        this.ObjectName = IntPtr.Zero;
                        this.Attributes = 0;
                        this.SecurityDescriptor = IntPtr.Zero;
                        this.SecurityQualityOfService = IntPtr.Zero;
                    }
                }
 
                /// <summary>
                /// LSA string
                /// </summary>
                [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
                private struct LSA_UNICODE_STRING
                {
                    internal ushort Length;
                    internal ushort MaximumLength;
                    [MarshalAs(UnmanagedType.LPWStr)]
                    internal string Buffer;
 
                    internal void Set(string src)
                    {
                        this.Buffer = src;
                        this.Length = (ushort)(src.Length * sizeof(char));
                        this.MaximumLength = (ushort)(this.Length + sizeof(char));
                    }
                }
 
                /// <summary>
                /// Structure used as the last parameter for LSALookupNames
                /// </summary>
                [StructLayout(LayoutKind.Sequential)]
                private struct LSA_TRANSLATED_SID2
                {
                    public uint Use;
                    public IntPtr SID;
                    public int DomainIndex;
                    public uint Flags;
                };
                #endregion interop structures
 
                #region safe handles
                /// <summary>
                /// Handle for LSA objects including Policy and Account
                /// </summary>
                private class LsaSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
                {
                    [DllImport("advapi32.dll")]
                    private static extern uint LsaClose(IntPtr ObjectHandle);
 
                    /// <summary>
                    /// Prevents a default instance of the LsaPolicySafeHAndle class from being created.
                    /// </summary>
                    private LsaSafeHandle(): base(true)
                    {
                    }
 
                    /// <summary>
                    /// Calls NativeMethods.CloseHandle(handle)
                    /// </summary>
                    /// <returns>the return of NativeMethods.CloseHandle(handle)</returns>
                    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
                    protected override bool ReleaseHandle()
                    {
                        long returnValue = LsaSafeHandle.LsaClose(this.handle);
                        return returnValue != 0;
 
                    }
                }
 
                /// <summary>
                /// Handle for IntPtrs returned from Lsa calls that have to be freed with
                /// LsaFreeMemory
                /// </summary>
                private class SafeLsaMemoryHandle : SafeHandleZeroOrMinusOneIsInvalid
                {
                    [DllImport("advapi32")]
                    internal static extern int LsaFreeMemory(IntPtr Buffer);
 
                    private SafeLsaMemoryHandle() : base(true) { }
 
                    private SafeLsaMemoryHandle(IntPtr handle)
                        : base(true)
                    {
                        SetHandle(handle);
                    }
 
                    private static SafeLsaMemoryHandle InvalidHandle
                    {
                        get { return new SafeLsaMemoryHandle(IntPtr.Zero); }
                    }
 
                    override protected bool ReleaseHandle()
                    {
                        return SafeLsaMemoryHandle.LsaFreeMemory(handle) == 0;
                    }
 
                    internal IntPtr Memory
                    {
                        get
                        {
                            return this.handle;
                        }
                    }
                }
                #endregion safe handles
 
                #region interop function declarations
                /// <summary>
                /// Opens LSA Policy
                /// </summary>
                [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
                private static extern uint LsaOpenPolicy(
                    IntPtr SystemName,
                    ref LSA_OBJECT_ATTRIBUTES ObjectAttributes,
                    uint DesiredAccess,
                    out LsaSafeHandle PolicyHandle
                );
 
                /// <summary>
                /// Convert the name into a SID which is used in remaining calls
                /// </summary>
                [DllImport("advapi32", CharSet = CharSet.Unicode, SetLastError = true), SuppressUnmanagedCodeSecurityAttribute]
                private static extern uint LsaLookupNames2(
                    LsaSafeHandle PolicyHandle,
                    uint Flags,
                    uint Count,
                    LSA_UNICODE_STRING[] Names,
                    out SafeLsaMemoryHandle ReferencedDomains,
                    out SafeLsaMemoryHandle Sids
                );
 
                /// <summary>
                /// Opens the LSA account corresponding to the user's SID
                /// </summary>
                [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
                private static extern uint LsaOpenAccount(
                    LsaSafeHandle PolicyHandle,
                    IntPtr Sid,
                    uint Access,
                    out LsaSafeHandle AccountHandle);
 
                /// <summary>
                /// Creates an LSA account corresponding to the user's SID
                /// </summary>
                [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
                private static extern uint LsaCreateAccount(
                    LsaSafeHandle PolicyHandle,
                    IntPtr Sid,
                    uint Access,
                    out LsaSafeHandle AccountHandle);
 
                /// <summary>
                /// Gets the LSA Account access
                /// </summary>
                [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
                private static extern uint LsaGetSystemAccessAccount(
                    LsaSafeHandle AccountHandle,
                    out uint SystemAccess);
 
                /// <summary>
                /// Sets the LSA Account access
                /// </summary>
                [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
                private static extern uint LsaSetSystemAccessAccount(
                    LsaSafeHandle AccountHandle,
                    uint SystemAccess);
                #endregion interop function declarations
 
                /// <summary>
                /// Sets the Log On As A Service Policy for <paramref name="userName"/>, if not already set.
                /// </summary>
                /// <param name="userName">the user name we want to allow logging on as a service</param>
                /// <exception cref="ArgumentNullException">If the <paramref name="userName"/> is null or empty.</exception>
                /// <exception cref="InvalidOperationException">In the following cases:
                /// Failure opening the LSA Policy.
                /// The <paramref name="userName"/> is too large.
                /// Failure looking up the user name.
                /// Failure opening LSA account (other than account not found).
                /// Failure creating LSA account.
                /// Failure getting LSA account policy access.
                /// Failure setting LSA account policy access.
                /// </exception>
                public static void SetLogOnAsServicePolicy(string userName)
                {
                    if (String.IsNullOrEmpty(userName))
                    {
                        throw new ArgumentNullException("userName");
                    }
 
                    LSA_OBJECT_ATTRIBUTES objectAttributes = new LSA_OBJECT_ATTRIBUTES();
                    objectAttributes.Initialize();
 
                    // All handles are delcared in advance so they can be closed on finally
                    LsaSafeHandle policyHandle = null;
                    SafeLsaMemoryHandle referencedDomains = null;
                    SafeLsaMemoryHandle sids = null;
                    LsaSafeHandle accountHandle = null;
 
                    try
                    {
                        uint status = LsaOpenPolicy(
                            IntPtr.Zero,
                            ref objectAttributes,
                            POLICY_LOOKUP_NAMES | POLICY_CREATE_ACCOUNT,
                            out policyHandle);
 
                        if (status != 0)
                        {
                            throw new InvalidOperationException("CannotOpenPolicyErrorMessage");
                        }
 
                        // Unicode strings have a maximum length of 32KB. We don't want to create
                        // LSA strings with more than that. User lengths are much smaller so this check
                        // ensures userName's length is useful
                        if (userName.Length > UNLEN + DNLEN + EXTRA_LENGTH)
                        {
                            throw new InvalidOperationException("UserNameTooLongErrorMessage");
                        }
 
                        LSA_UNICODE_STRING lsaUserName = new LSA_UNICODE_STRING();
                        lsaUserName.Set(userName);
 
                        LSA_UNICODE_STRING[] names = new LSA_UNICODE_STRING[1];
                        names[0].Set(userName);
 
                        status = LsaLookupNames2(
                            policyHandle,
                            0,
                            1,
                            new LSA_UNICODE_STRING[] { lsaUserName },
                            out referencedDomains,
                            out sids);
 
                        if (status != 0)
                        {
                            throw new InvalidOperationException("CannotLookupNamesErrorMessage");
                        }
 
                        LSA_TRANSLATED_SID2 sid = (LSA_TRANSLATED_SID2)Marshal.PtrToStructure(sids.Memory, typeof(LSA_TRANSLATED_SID2));
 
                        status = LsaOpenAccount(policyHandle,
                                            sid.SID,
                                            ACCOUNT_VIEW | ACCOUNT_ADJUST_SYSTEM_ACCESS,
                                            out accountHandle);
 
                        uint currentAccess = 0;
 
                        if (status == 0)
                        {
                            status = LsaGetSystemAccessAccount(accountHandle, out currentAccess);
 
                            if (status != 0)
                            {
                                throw new InvalidOperationException("CannotGetAccountAccessErrorMessage");
                            }
 
                        }
                        else if (status == STATUS_OBJECT_NAME_NOT_FOUND)
                        {
                            status = LsaCreateAccount(
                                policyHandle,
                                sid.SID,
                                ACCOUNT_ADJUST_SYSTEM_ACCESS,
                                out accountHandle);
 
                            if (status != 0)
                            {
                                throw new InvalidOperationException("CannotCreateAccountAccessErrorMessage");
                            }
                        }
                        else
                        {
                            throw new InvalidOperationException("CannotOpenAccountErrorMessage");
                        }
 
                        if ((currentAccess & SECURITY_ACCESS_SERVICE_LOGON) == 0)
                        {
                            status = LsaSetSystemAccessAccount(
                                accountHandle,
                                currentAccess | SECURITY_ACCESS_SERVICE_LOGON);
                            if (status != 0)
                            {
                                throw new InvalidOperationException("CannotSetAccountAccessErrorMessage");
                            }
                        }
                    }
                    finally
                    {
                        if (policyHandle != null) { policyHandle.Close(); }
                        if (referencedDomains != null) { referencedDomains.Close(); }
                        if (sids != null) { sids.Close(); }
                        if (accountHandle != null) { accountHandle.Close(); }
                    }
                }
            }
        }
"@


    try
    {
        $null = [LogOnAsServiceHelper.NativeMethods]
    }
    catch
    {
        $logOnAsServiceText = $logOnAsServiceText.Replace('CannotOpenPolicyErrorMessage', `
            $script:localizedData.CannotOpenPolicyErrorMessage)
        $logOnAsServiceText = $logOnAsServiceText.Replace('UserNameTooLongErrorMessage', `
            $script:localizedData.UserNameTooLongErrorMessage)
        $logOnAsServiceText = $logOnAsServiceText.Replace('CannotLookupNamesErrorMessage', `
            $script:localizedData.CannotLookupNamesErrorMessage)
        $logOnAsServiceText = $logOnAsServiceText.Replace('CannotOpenAccountErrorMessage', `
            $script:localizedData.CannotOpenAccountErrorMessage)
        $logOnAsServiceText = $logOnAsServiceText.Replace('CannotCreateAccountAccessErrorMessage', `
            $script:localizedData.CannotCreateAccountAccessErrorMessage)
        $logOnAsServiceText = $logOnAsServiceText.Replace('CannotGetAccountAccessErrorMessage', `
            $script:localizedData.CannotGetAccountAccessErrorMessage)
        $logOnAsServiceText = $logOnAsServiceText.Replace('CannotSetAccountAccessErrorMessage', `
            $script:localizedData.CannotSetAccountAccessErrorMessage)
        $null = Add-Type $logOnAsServiceText -PassThru -Debug:$false
    }

    if ($UserName.StartsWith('.\'))
    {
        $UserName = $UserName.Substring(2)
    }

    try
    {
        [LogOnAsServiceHelper.NativeMethods]::SetLogOnAsServicePolicy($UserName)
    }
    catch
    {
        $message = ($script:localizedData.ErrorSettingLogOnAsServiceRightsForUser `
            -f $UserName, $_.Exception.Message)
        New-InvalidOperationException -Message $message
    }
} # function Set-LogOnAsServicePolicy

Export-ModuleMember -Function *-TargetResource