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 () #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 |