SharedResources/Src/InstallPrimaryHeadNode/xPSDesiredStateConfiguration/DSCResources/MSFT_xServiceResource/MSFT_xServiceResource.psm1 (1,516 lines of code) (raw):
# 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 ()
#region localizeddata
if (Test-Path "${PSScriptRoot}\${PSUICulture}")
{
Import-LocalizedData `
-BindingVariable LocalizedData `
-Filename MSFT_xServiceResource.strings.psd1 `
-BaseDirectory "${PSScriptRoot}\${PSUICulture}"
}
else
{
#fallback to en-US
Import-LocalizedData `
-BindingVariable LocalizedData `
-Filename MSFT_xServiceResource.strings.psd1 `
-BaseDirectory "${PSScriptRoot}\en-US"
}
#endregion
<#
.SYNOPSIS
Get the current status of a service.
.PARAMETER name
Indicates the service name. 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([Hashtable])]
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Name
)
if (Test-ServiceExist -Name $Name -ErrorAction SilentlyContinue)
{
$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
{
return @{
Name = $service.Name
Ensure = 'Absent'
}
}
} # function Get-TargetResource
<#
.SYNOPSIS
Tests if a service needs to be created, changed or removed.
.PARAMETER name
Indicates the service name. Note that sometimes this is different from the display name.
You can get a list ofthe services and their current state with the Get-Service cmdlet. Key.
.PARAMETER Ensure
Ensures that the service is present or absent. 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
The service can create or communicate with a window on the desktop. Must be false for services
not running as LocalSystem. Optional. Defaults to False.
.PARAMETER State
Indicates the state you want to ensure for the service. Optional. Defaults to 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.
.PARAMETER TerminateTimeout
The time to wait for the service to stop in milliseconds. Optional.
#>
function Test-TargetResource
{
[OutputType([Boolean])]
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Name,
[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")]
[String]
$State = "Running",
[ValidateSet("Present", "Absent")]
[String]
$Ensure = "Present",
[ValidateNotNullOrEmpty()]
[String]
$Path,
[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 will conflict with the current state
Test-StartupType -Name $Name -StartupType $StartupType -State $State
} # if
$serviceExists = Test-ServiceExist -Name $Name -ErrorAction SilentlyContinue
if ($Ensure -eq 'Absent')
{
return -not $serviceExists
} # if
if (-not $serviceExists)
{
return $false
} # if
$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 ($LocalizedData.TestBinaryPathMismatch `
-f $serviceWmi.Name, $serviceWmi.PathName, $Path)
return $false
} # if
# Check the optional parameters
if ($PSBoundParameters.ContainsKey('DisplayName') `
-and ($DisplayName -ne $serviceWmi.DisplayName))
{
Write-Verbose -Message ($LocalizedData.ParameterMismatch `
-f 'DisplayName',$serviceWmi.DisplayName,$DisplayName)
return $false
} # if
if ($PSBoundParameters.ContainsKey('Description') `
-and ($Description -ne $serviceWmi.Description))
{
Write-Verbose -Message ($LocalizedData.ParameterMismatch `
-f 'Description',$serviceWmi.Description,$Description)
return $false
} # if
# update the service dependencies if required
if ($PSBoundParameters.ContainsKey('Dependencies') `
-and (@(Compare-Object `
-ReferenceObject $service.ServicesDependedOn `
-DifferenceObject $Dependencies).Count -gt 0))
{
Write-Verbose -Message ($LocalizedData.ParameterMismatch `
-f 'Dependencies',($service.ServicesDependedOn -join ','),($Dependencies -join ','))
return $false
} # if
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
if($PSBoundParameters.ContainsKey("Credential"))
{
$null = $getUserNameAndPasswordArgs.Add("Credential",$Credential)
} # if
$userName,$password = Get-UserNameAndPassword @getUserNameAndPasswordArgs
if($null -ne $userName `
-and -not (Test-UserName -ServiceWmi $serviceWmi -Username $userName))
{
Write-Verbose -Message ($LocalizedData.TestUserNameMismatch `
-f $serviceWmi.Name,$serviceWmi.StartName,$userName)
return $false
} # if
if ($PSBoundParameters.ContainsKey("DesktopInteract") `
-and $serviceWmi.DesktopInteract -ne $DesktopInteract)
{
Write-Verbose -Message ($LocalizedData.TestDesktopInteractMismatch `
-f $serviceWmi.Name,$serviceWmi.DesktopInteract,$DesktopInteract)
return $false
} # if
if ($PSBoundParameters.ContainsKey("StartupType") `
-and $serviceWmi.StartMode -ine (ConvertTo-StartModeString -StartupType $StartupType))
{
Write-Verbose -Message ($LocalizedData.TestStartupTypeMismatch `
-f $serviceWmi.Name,$serviceWmi.StartMode,$StartupType)
return $false
} # if
} # if
if ($State -ne $service.Status)
{
Write-Verbose -Message ($LocalizedData.TestStateMismatch `
-f $serviceWmi.Name, $service.Status, $State)
return $false
} # if
return $true
} # function Test-TargetResource
<#
.SYNOPSIS
Creates, updates or removes a service.
.PARAMETER name
Indicates the service name. Note that sometimes this is different from the display name.
You can get a list ofthe services and their current state with the Get-Service cmdlet. Key.
.PARAMETER Ensure
Ensures that the service is present or absent. 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
The service can create or communicate with a window on the desktop. Must be false for services
not running as LocalSystem. Optional. Defaults to False.
.PARAMETER State
Indicates the state you want to ensure for the service. Optional. Defaults to 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.
.PARAMETER TerminateTimeout
The time to wait for the service to stop in milliseconds. Optional.
#>
function Set-TargetResource
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Name,
[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")]
[String]
$State = "Running",
[ValidateSet("Present", "Absent")]
[String]
$Ensure = "Present",
[ValidateNotNullOrEmpty()]
[String]
$Path,
[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 will conflict with the current state
Test-StartupType -Name $Name -StartupType $StartupType -State $State
} # if
$serviceExists = Test-ServiceExist -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
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 ($LocalizedData.ServiceExecutablePathChangeNotSupported)
} # if
}
elseif ($PSBoundParameters.ContainsKey("Path") -and -not $serviceExists)
{
$argumentsToNewService = @{}
$argumentsToNewService.Add("Name", $Name)
$argumentsToNewService.Add("BinaryPathName", $Path)
try
{
New-Service @argumentsToNewService
}
catch
{
Write-Verbose -Message ($LocalizedData.TestStartupTypeMismatch `
-f $argumentsToNewService["Name"], $_.Exception.Message)
throw $_
} # try
}
elseif (-not $PSBoundParameters.ContainsKey("Path") -and -not $serviceExists)
{
New-InvalidArgumentError `
-ErrorId "ServiceDoesNotExistPathMissingError" `
-ErrorMessage ($LocalizedData.ServiceDoesNotExistPathMissingError -f $Name)
} # if
# Update the parameters of the service
$writeWritePropertiesArguments = @{
Name = $Name
}
if ($PSBoundParameters.ContainsKey('Path'))
{
$writeWritePropertiesArguments['Path'] = $Path
} # if
if ($PSBoundParameters.ContainsKey('StartupType'))
{
$writeWritePropertiesArguments['StartupType'] = $StartupType
} # if
if ($PSBoundParameters.ContainsKey('BuiltInAccount'))
{
$writeWritePropertiesArguments['BuiltInAccount'] = $BuiltInAccount
} # if
if ($PSBoundParameters.ContainsKey('Credential'))
{
$writeWritePropertiesArguments['Credential'] = $Credential
} # if
if ($PSBoundParameters.ContainsKey('DesktopInteract'))
{
$writeWritePropertiesArguments['DesktopInteract'] = $DesktopInteract
} # if
if ($PSBoundParameters.ContainsKey('DisplayName'))
{
$writeWritePropertiesArguments['DisplayName'] = $DisplayName
} # if
if ($PSBoundParameters.ContainsKey('Description'))
{
$writeWritePropertiesArguments['Description'] = $Description
} # if
if ($PSBoundParameters.ContainsKey('Dependencies'))
{
$writeWritePropertiesArguments['Dependencies'] = $Dependencies
} # if
$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 ($LocalizedData.ServiceNeedsRestartMessage -f
$Name)
Stop-ServiceResource -Name $Name -TerminateTimeout $TerminateTimeout
} # if
Start-ServiceResource $Name -StartupTimeout $StartupTimeout
} # if
} # function Set-TargetResource
<#
.SYNOPSIS
Tests if the given StartupType with valid with the given State parameter for 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.
#>
function Test-StartupType
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Name,
[Parameter(Mandatory = $true)]
[ValidateSet("Automatic", "Manual", "Disabled")]
[String]
$StartupType,
[ValidateSet("Running", "Stopped")]
[String]
$State = "Running"
)
if ($State -eq "Stopped")
{
if ($StartupType -eq "Automatic")
{
# State = Stopped conflicts with Automatic or Delayed
New-InvalidArgumentError `
-ErrorId "CannotStopServiceSetToStartAutomatically" `
-ErrorMessage ($LocalizedData.CannotStopServiceSetToStartAutomatically -f $Name)
} # if
}
else
{
if ($StartupType -eq "Disabled")
{
# State = Running conflicts with Disabled
New-InvalidArgumentError `
-ErrorId "CannotStartAndDisable" `
-ErrorMessage ($LocalizedData.CannotStartAndDisable -f $Name)
} # if
} # if
} # 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'
} # if
return $StartupType
} # function ConvertTo-StartModeString
<#
.SYNOPSIS
Converts the StartupType string returned in a Win32_Service object to the format
expected by this resource.
.PARAMETER StartupType
The StartupType string to convert.
#>
function ConvertTo-StartupTypeString
{
param
(
[Parameter(Mandatory = $true)]
[ValidateSet('Auto', 'Manual', 'Disabled')]
$StartMode
)
if ($StartMode -eq 'Auto')
{
return "Automatic"
} # if
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 for which to set the StartupType
.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, $LocalizedData.SetStartupTypeWhatIf))
{
$changeServiceArguments = @{
StartMode = $StartupType
}
$changeResult = Invoke-CimMethod `
-InputObject $Win32ServiceObject `
-MethodName Change `
-Arguments $changeServiceArguments
if ($changeResult.ReturnValue -ne 0)
{
$innerMessage = ($LocalizedData.MethodFailed `
-f "Change", "Win32_Service", $changeResult.ReturnValue)
$errorMessage = ($LocalizedData.ErrorChangingProperty `
-f "StartupType", $innerMessage)
New-InvalidArgumentError `
-ErrorId "ChangeStartupTypeFailed" `
-ErrorMessage $errorMessage
}
}
} # function Set-ServiceStartMode
<#
.SYNOPSIS
Writes all write properties if not already correctly set, logging errors and respecting whatif
#>
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)
} # if
# update misc service properties
$serviceprops = @{}
if ($PSBoundParameters.ContainsKey('DisplayName') `
-and ($DisplayName -ne $serviceWmi.DisplayName))
{
$serviceprops += @{ DisplayName = $DisplayName }
} # if
if ($PSBoundParameters.ContainsKey('Description') `
-and ($Description -ne $serviceWmi.Description))
{
$serviceprops += @{ Description = $Description }
} # if
if ($serviceprops.count -gt 0)
{
$null = Set-Service `
-Name $Name `
@ServiceProps
} # if
# update the service dependencies if required
if ($PSBoundParameters.ContainsKey('Dependencies') `
-and (@(Compare-Object `
-ReferenceObject $service.ServicesDependedOn `
-DifferenceObject $Dependencies).Count -gt 0))
{
$changeServiceArguments = @{ ServiceDependencies = $Dependencies }
$changeResult = Invoke-CimMethod `
-InputObject $serviceWmi `
-MethodName Change `
-Arguments $changeServiceArguments
if ($changeResult.ReturnValue -ne 0)
{
$innerMessage = ($LocalizedData.MethodFailed `
-f "Change", "Win32_Service", $changeResult.ReturnValue)
$errorMessage = ($LocalizedData.ErrorChangingProperty `
-f "Dependencies", $innerMessage)
New-InvalidArgumentError `
-ErrorId "ChangeDependenciesFailed" `
-ErrorMessage $errorMessage
} # if
} # if
# 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
if($PSBoundParameters.ContainsKey("Credential"))
{
$null = $writeCredentialPropertiesArguments.Add("Credential",$Credential)
} # if
if($PSBoundParameters.ContainsKey("DesktopInteract"))
{
$null = $writeCredentialPropertiesArguments.Add("DesktopInteract",$DesktopInteract)
} # if
Write-CredentialProperty @writeCredentialPropertiesArguments
} # if
# Update startup type
if($PSBoundParameters.ContainsKey("StartupType"))
{
Set-ServiceStartMode -Win32ServiceObject $serviceWmi -StartupType $StartupType
} # if
# Return restart status
return $requiresRestart
} # function Write-WriteProperty
<#
.SYNOPSIS
Writes credential properties if not already correctly set, logging errors and respecting whatif
#>
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
} # if
# 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
if($PSBoundParameters.ContainsKey("Credential"))
{
$null = $getUserNameAndPasswordArgs.Add("Credential",$Credential)
} # if
if($getUserNameAndPasswordArgs.Count -gt 1)
{
# Both credentials and buildinaccount were set - throw
New-InvalidArgumentError `
-ErrorId "OnlyCredentialOrBuiltInAccount" `
-ErrorMessage ($LocalizedData.OnlyOneParameterCanBeSpecified `
-f "Credential","BuiltInAccount")
} # if
$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
} # if
$changeArgs += @{
StartName = $userName
StartPassword = $password
}
} # if
# The desktop interact flag was passed to set that value
if($PSBoundParameters.ContainsKey("DesktopInteract") `
-and ($DesktopInteract -ne $ServiceWmi.DesktopInteract))
{
$changeArgs.DesktopInteract = $DesktopInteract
} # if
if ($changeArgs.Count -gt 0)
{
$ret = Invoke-CimMethod `
-InputObject $ServiceWmi `
-MethodName Change `
-Arguments $changeArgs
if($ret.ReturnValue -ne 0)
{
$innerMessage = ($LocalizedData.MethodFailed `
-f "Change","Win32_Service",$ret.ReturnValue)
$errorMessage = ($LocalizedData.ErrorChangingProperty `
-f "Credential",$innerMessage)
New-InvalidArgumentError `
-ErrorId "ChangeCredentialFailed" `
-ErrorMessage $errorMessage
} # if
} # if
} # function Write-CredentialProperty
<#
.SYNOPSIS
Writes binary path if not already correctly set, logging errors.
#>
function Write-BinaryProperty
{
[OutputType([System.Boolean])]
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$ServiceWmi,
[System.String]
[ValidateNotNullOrEmpty()]
$Path
)
if($ServiceWmi.PathName -eq $Path)
{
return $false
} # if
$changeServiceArguments = @{ PathName = $Path }
$changeResult = Invoke-CimMethod `
-InputObject $serviceWmi `
-MethodName Change `
-Arguments $changeServiceArguments
if ($changeResult.ReturnValue -ne 0)
{
$innerMessage = ($LocalizedData.MethodFailed `
-f "Change", "Win32_Service", $changeResult.ReturnValue)
$errorMessage = ($LocalizedData.ErrorChangingProperty `
-f "Binary Path", $innerMessage)
New-InvalidArgumentError `
-ErrorId "ChangeBinaryPathFailed" `
-ErrorMessage $errorMessage
} # if
return $true
} # function Write-BinaryProperty
<#
.SYNOPSIS
Returns true if the service's StartName matches $UserName
.PARAMETER ServiceWmi
The Service object pulled from WMI for the service.
.PARAMETER UserName
The username of the user to compare the one in the WMI object with.
#>
function Test-UserName
{
param
(
$ServiceWmi,
[string]
$UserName
)
return (Resolve-UserName -UserName $ServiceWmi.StartName) -ieq $UserName
} # function Test-UserName
<#
.SYNOPSIS
Retrieves username and password out of the BuiltInAccount and Credential parameters
.PARAMETER BuiltInAccount
If passed the username will contain the resolved username for the built-in account.
.PARAMETER Credential
The Credential to extract the username from.
.OUTPUTS
A tuple containing: Username,Password
#>
function Get-UserNameAndPassword
{
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.
#>
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-ServiceExist -Name $Name))
{
# The service has been deleted OK
$serviceDeletedSuccessfully = $true
break
} # if
# The service wasn't deleted so wait a second and try again (unless TerminateTimeout is hit)
Start-Sleep -Seconds 1
Write-Verbose -Message ($LocalizedData.TryDeleteAgain)
} # while
if ($serviceDeletedSuccessfully)
{
# Service was deleted OK
Write-Verbose -Message ($LocalizedData.ServiceDeletedSuccessfully -f $Name)
}
else
{
# Service was not deleted
New-InvalidArgumentError `
-ErrorId "ErrorDeletingService" `
-ErrorMessage ($LocalizedData.ErrorDeletingService -f $Name)
} # if
} # 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 ($LocalizedData.ServiceAlreadyStarted -f $service.Name)
return
} # if
if ($PSCmdlet.ShouldProcess($Name, $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 = ($LocalizedData.ErrorStartingService -f `
$service.Name, $servicePath, $_.Exception.Message)
New-InvalidArgumentError `
-ErrorId "ErrorStartingService" `
-ErrorMessage $errorMessage
} # try
Write-Verbose -Message ($LocalizedData.ServiceStarted -f $service.Name)
} # if
} # 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 ($LocalizedData.ServiceAlreadyStopped -f $service.Name)
return
}
if ($PSCmdlet.ShouldProcess($Name, $LocalizedData.StopServiceWhatIf))
{
try
{
$service.Stop()
$waitTimeSpan = New-Object `
-TypeName TimeSpan `
-ArgumentList ($TerminateTimeout * 10000)
$service.WaitForStatus([System.ServiceProcess.ServiceControllerStatus]::Stopped,`
$waitTimeSpan)
}
catch
{
Write-Verbose -Message ($LocalizedData.ErrorStoppingService `
-f $service.Name, $_.Exception.Message)
throw $_
}
Write-Verbose -Message ($LocalizedData.ServiceStopped -f $service.Name)
}
} # function Stop-ServiceResource
<#
.SYNOPSIS
Converts the username returned in a Win32_service object to the format
expected by this resource.
.PARAMETER UserName
The Username to convert.
#>
function Resolve-UserName
{
param
(
[String]
$UserName
)
switch ($Username)
{
'NetworkService'
{
return "NT Authority\NetworkService"
} # 'NetworkService'
'LocalService'
{
return "NT Authority\LocalService"
} # 'LocalService'
'LocalSystem'
{
return ".\LocalSystem"
} # 'LocalSystem'
default
{
if ($UserName.IndexOf("\") -eq -1)
{
return ".\" + $UserName
} # if
} # default
} # switch
return $UserName
} # function Resolve-UserName
<#
.SYNOPSIS
Throws an argument error.
.PARAMETER ErrorId
The error id to assign to the custom exception.
.PARAMETER ErrorMessage
The error message to assign to the custom exception.
#>
function New-InvalidArgumentError
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[System.String]
$ErrorId,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[System.String]
$ErrorMessage
)
$errorCategory=[System.Management.Automation.ErrorCategory]::InvalidArgument
$exception = New-Object `
-TypeName System.ArgumentException `
-ArgumentList $ErrorMessage
$errorRecord = New-Object `
-TypeName System.Management.Automation.ErrorRecord `
-ArgumentList $exception, $ErrorId, $errorCategory, $null
throw $errorRecord
} # function New-InvalidArgumentError
<#
.SYNOPSIS
Tests if a service with the given name exists
.PARAMETER Name
The name of the service to test for.
#>
function Test-ServiceExist
{
[OutputType([Boolean])]
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Name
)
$service = Get-Service -Name $Name -ErrorAction SilentlyContinue
return $null -ne $service
} # function Test-ServiceExist
<#
.SYNOPSIS
Compares a path to the existing service path.
Returns true when the given path is same as the existing service path.
.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-InvalidArgumentError `
-ErrorId "ServiceNotFound" `
-ErrorMessage ($LocalizedData.ServiceNotFound -f $Name)
} # if
return $service
} # function Get-ServiceResource
<#
.SYNOPSIS
Grants log on as service right to the given user
#>
function Set-LogOnAsServicePolicy
{
param
(
[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",`
$LocalizedData.CannotOpenPolicyErrorMessage)
$logOnAsServiceText = $logOnAsServiceText.Replace("UserNameTooLongErrorMessage",`
$LocalizedData.UserNameTooLongErrorMessage)
$logOnAsServiceText = $logOnAsServiceText.Replace("CannotLookupNamesErrorMessage",`
$LocalizedData.CannotLookupNamesErrorMessage)
$logOnAsServiceText = $logOnAsServiceText.Replace("CannotOpenAccountErrorMessage",`
$LocalizedData.CannotOpenAccountErrorMessage)
$logOnAsServiceText = $logOnAsServiceText.Replace("CannotCreateAccountAccessErrorMessage",`
$LocalizedData.CannotCreateAccountAccessErrorMessage)
$logOnAsServiceText = $logOnAsServiceText.Replace("CannotGetAccountAccessErrorMessage",`
$LocalizedData.CannotGetAccountAccessErrorMessage)
$logOnAsServiceText = $logOnAsServiceText.Replace("CannotSetAccountAccessErrorMessage",`
$LocalizedData.CannotSetAccountAccessErrorMessage)
$null = Add-Type $logOnAsServiceText -PassThru -Debug:$false
} # try
if($UserName.StartsWith(".\"))
{
$UserName = $UserName.Substring(2)
} # if
try
{
[LogOnAsServiceHelper.NativeMethods]::SetLogOnAsServicePolicy($UserName)
}
catch
{
$message = ($LocalizedData.ErrorSettingLogOnAsServiceRightsForUser `
-f $UserName,$_.Exception.Message)
New-InvalidArgumentError -ErrorId "ErrorSettingLogOnAsServiceRightsForUser" -ErrorMessage $message
} # try
} # function Set-LogOnAsServicePolicy
Export-ModuleMember -Function *-TargetResource