2 PowerShell modules (Invoke-CommandWithCredential and Invoke-ScriptBlockWithCredential) for invoking commands using specified credentials. One referenced module (Get-CallerPreference)for propagating verbosity etc. One DSC module (cProcessWithCredentials) for executing a process with credentials.
Last active
October 31, 2015 20:05
-
-
Save tg2k/bc5cfc5087501f7664a4 to your computer and use it in GitHub Desktop.
cProcessWithCredentials DSC resource and supporting modules
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# Module manifest for module 'cProcessWithCredentials' | |
# | |
# Generated by: Todd C. Gleason | |
# | |
# Generated on: 9/18/2015 | |
# | |
@{ | |
# Script module or binary module file associated with this manifest. | |
RootModule = 'cProcessWithCredentials' | |
# Version number of this module. | |
ModuleVersion = '1.0' | |
# ID used to uniquely identify this module | |
GUID = 'a6d77483-544a-47e8-9a71-3117f9a54132' | |
# Author of this module | |
Author = 'Todd C. Gleason' | |
# Company or vendor of this module | |
CompanyName = '' | |
# Copyright statement for this module | |
Copyright = '(c) 2015 Todd C. Gleason. All rights reserved.' | |
# Description of the functionality provided by this module | |
Description = 'Module with DSC Resource for ProcessWithCredentials' | |
# Minimum version of the Windows PowerShell engine required by this module | |
PowerShellVersion = '4.0' | |
# Name of the Windows PowerShell host required by this module | |
# PowerShellHostName = '' | |
# Minimum version of the Windows PowerShell host required by this module | |
# PowerShellHostVersion = '' | |
# Minimum version of Microsoft .NET Framework required by this module | |
# DotNetFrameworkVersion = '' | |
# Minimum version of the common language runtime (CLR) required by this module | |
# CLRVersion = '' | |
# Processor architecture (None, X86, Amd64) required by this module | |
# ProcessorArchitecture = '' | |
# Modules that must be imported into the global environment prior to importing this module | |
# RequiredModules = @() | |
# Assemblies that must be loaded prior to importing this module | |
# RequiredAssemblies = @() | |
# Script files (.ps1) that are run in the caller's environment prior to importing this module. | |
# ScriptsToProcess = @() | |
# Type files (.ps1xml) to be loaded when importing this module | |
# TypesToProcess = @() | |
# Format files (.ps1xml) to be loaded when importing this module | |
# FormatsToProcess = @() | |
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess | |
# NestedModules = @() | |
# Functions to export from this module | |
FunctionsToExport = '*.TargetResource' | |
# Cmdlets to export from this module | |
CmdletsToExport = '*' | |
# Variables to export from this module | |
VariablesToExport = '*' | |
# Aliases to export from this module | |
AliasesToExport = '*' | |
# List of all modules packaged with this module | |
# ModuleList = @() | |
# List of all files packaged with this module | |
# FileList = @() | |
# Private data to pass to the module specified in RootModule/ModuleToProcess | |
# PrivateData = '' | |
# HelpInfo URI of this module | |
# HelpInfoURI = '' | |
# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. | |
# DefaultCommandPrefix = '' | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Import-Module Invoke-CommandWithCredential | |
Import-Module Get-CallerPreference | |
function Get-TargetResource | |
{ | |
[CmdletBinding()] | |
[OutputType([System.Collections.Hashtable])] | |
param | |
( | |
[parameter(Mandatory = $true)] | |
[System.String] | |
$Name | |
) | |
#Write-Verbose "Use this cmdlet to deliver information about command processing." | |
#Write-Debug "Use this cmdlet to write debug information while troubleshooting." | |
<# | |
$returnValue = @{ | |
Name = [System.String] | |
Credential = [System.Management.Automation.PSCredential] | |
Path = [System.String] | |
Arguments = [System.String] | |
WorkingDirectory = [System.String] | |
} | |
$returnValue | |
#> | |
} | |
function Set-TargetResource | |
{ | |
[CmdletBinding()] | |
param | |
( | |
[parameter(Mandatory = $true)] | |
[System.String] | |
$Name, | |
[System.Management.Automation.PSCredential] | |
$Credential, | |
[System.String] | |
$Path, | |
[System.String] | |
$Arguments, | |
[System.String] | |
$WorkingDirectory | |
) | |
#Write-Verbose "Use this cmdlet to deliver information about command processing." | |
#Write-Debug "Use this cmdlet to write debug information while troubleshooting." | |
# Must manually inherit state such as verbosity | |
# See https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d | |
Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState | |
Invoke-CommandWithCredential $Credential $Path $Arguments $WorkingDirectory | |
#Include this line if the resource requires a system reboot. | |
#$global:DSCMachineStatus = 1 | |
} | |
function Test-TargetResource | |
{ | |
[CmdletBinding()] | |
[OutputType([System.Boolean])] | |
param | |
( | |
[parameter(Mandatory = $true)] | |
[System.String] | |
$Name, | |
[System.Management.Automation.PSCredential] | |
$Credential, | |
[System.String] | |
$Path, | |
[System.String] | |
$Arguments, | |
[System.String] | |
$WorkingDirectory | |
) | |
#Write-Verbose "Use this cmdlet to deliver information about command processing." | |
#Write-Debug "Use this cmdlet to write debug information while troubleshooting." | |
<# | |
$result = [System.Boolean] | |
$result | |
#> | |
# Always require the Set to run | |
$false | |
} | |
Export-ModuleMember -Function *-TargetResource | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[ClassVersion("1.0.0.0"), FriendlyName("cProcessWithCredentials")] | |
class cProcessWithCredentials : OMI_BaseResource | |
{ | |
[Key] String Name; | |
[Write, EmbeddedInstance("MSFT_Credential"), Description("Credential to run the MFS commands under")] String Credential; | |
[Write, Description("The path to the executable")] String Path; | |
[Write, Description("The arguments to the executable")] String Arguments; | |
[Write, Description("The working directory")] String WorkingDirectory; | |
}; | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#requires -Version 2.0 | |
function Get-CallerPreference | |
{ | |
<# | |
.Synopsis | |
Fetches "Preference" variable values from the caller's scope. | |
.DESCRIPTION | |
Script module functions do not automatically inherit their caller's variables, but they can be | |
obtained through the $PSCmdlet variable in Advanced Functions. This function is a helper function | |
for any script module Advanced Function; by passing in the values of $ExecutionContext.SessionState | |
and $PSCmdlet, Get-CallerPreference will set the caller's preference variables locally. | |
.PARAMETER Cmdlet | |
The $PSCmdlet object from a script module Advanced Function. | |
.PARAMETER SessionState | |
The $ExecutionContext.SessionState object from a script module Advanced Function. This is how the | |
Get-CallerPreference function sets variables in its callers' scope, even if that caller is in a different | |
script module. | |
.PARAMETER Name | |
Optional array of parameter names to retrieve from the caller's scope. Default is to retrieve all | |
Preference variables as defined in the about_Preference_Variables help file (as of PowerShell 4.0) | |
This parameter may also specify names of variables that are not in the about_Preference_Variables | |
help file, and the function will retrieve and set those as well. | |
.EXAMPLE | |
Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState | |
Imports the default PowerShell preference variables from the caller into the local scope. | |
.EXAMPLE | |
Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -Name 'ErrorActionPreference','SomeOtherVariable' | |
Imports only the ErrorActionPreference and SomeOtherVariable variables into the local scope. | |
.EXAMPLE | |
'ErrorActionPreference','SomeOtherVariable' | Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState | |
Same as Example 2, but sends variable names to the Name parameter via pipeline input. | |
.INPUTS | |
String | |
.OUTPUTS | |
None. This function does not produce pipeline output. | |
.LINK | |
about_Preference_Variables | |
#> | |
[CmdletBinding(DefaultParameterSetName = 'AllVariables')] | |
param ( | |
[Parameter(Mandatory = $true)] | |
[ValidateScript({ $_.GetType().FullName -eq 'System.Management.Automation.PSScriptCmdlet' })] | |
$Cmdlet, | |
[Parameter(Mandatory = $true)] | |
[System.Management.Automation.SessionState] | |
$SessionState, | |
[Parameter(ParameterSetName = 'Filtered', ValueFromPipeline = $true)] | |
[string[]] | |
$Name | |
) | |
begin | |
{ | |
$filterHash = @{} | |
} | |
process | |
{ | |
if ($null -ne $Name) | |
{ | |
foreach ($string in $Name) | |
{ | |
$filterHash[$string] = $true | |
} | |
} | |
} | |
end | |
{ | |
# List of preference variables taken from the about_Preference_Variables help file in PowerShell version 4.0 | |
$vars = @{ | |
'ErrorView' = $null | |
'FormatEnumerationLimit' = $null | |
'LogCommandHealthEvent' = $null | |
'LogCommandLifecycleEvent' = $null | |
'LogEngineHealthEvent' = $null | |
'LogEngineLifecycleEvent' = $null | |
'LogProviderHealthEvent' = $null | |
'LogProviderLifecycleEvent' = $null | |
'MaximumAliasCount' = $null | |
'MaximumDriveCount' = $null | |
'MaximumErrorCount' = $null | |
'MaximumFunctionCount' = $null | |
'MaximumHistoryCount' = $null | |
'MaximumVariableCount' = $null | |
'OFS' = $null | |
'OutputEncoding' = $null | |
'ProgressPreference' = $null | |
'PSDefaultParameterValues' = $null | |
'PSEmailServer' = $null | |
'PSModuleAutoLoadingPreference' = $null | |
'PSSessionApplicationName' = $null | |
'PSSessionConfigurationName' = $null | |
'PSSessionOption' = $null | |
'ErrorActionPreference' = 'ErrorAction' | |
'DebugPreference' = 'Debug' | |
'ConfirmPreference' = 'Confirm' | |
'WhatIfPreference' = 'WhatIf' | |
'VerbosePreference' = 'Verbose' | |
'WarningPreference' = 'WarningAction' | |
} | |
foreach ($entry in $vars.GetEnumerator()) | |
{ | |
if (([string]::IsNullOrEmpty($entry.Value) -or -not $Cmdlet.MyInvocation.BoundParameters.ContainsKey($entry.Value)) -and | |
($PSCmdlet.ParameterSetName -eq 'AllVariables' -or $filterHash.ContainsKey($entry.Name))) | |
{ | |
$variable = $Cmdlet.SessionState.PSVariable.Get($entry.Key) | |
if ($null -ne $variable) | |
{ | |
if ($SessionState -eq $ExecutionContext.SessionState) | |
{ | |
Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false | |
} | |
else | |
{ | |
$SessionState.PSVariable.Set($variable.Name, $variable.Value) | |
} | |
} | |
} | |
} | |
if ($PSCmdlet.ParameterSetName -eq 'Filtered') | |
{ | |
foreach ($varName in $filterHash.Keys) | |
{ | |
if (-not $vars.ContainsKey($varName)) | |
{ | |
$variable = $Cmdlet.SessionState.PSVariable.Get($varName) | |
if ($null -ne $variable) | |
{ | |
if ($SessionState -eq $ExecutionContext.SessionState) | |
{ | |
Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false | |
} | |
else | |
{ | |
$SessionState.PSVariable.Set($variable.Name, $variable.Value) | |
} | |
} | |
} | |
} | |
} | |
} # end | |
} # function Get-CallerPreference |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Import-Module Get-CallerPreference | |
function Invoke-CommandWithCredential { | |
<# | |
.SYNOPSIS | |
Invokes a command with the specified credential. | |
.DESCRIPTION | |
The Invoke-CommandWithCredential function calls Invoke-ScriptBlockWithCredential | |
(which uses LogonUser and impersonation) and then executes | |
the specified command as the specified user. | |
It is useful from within DSC because DSC runs as the system, and using the typical means to | |
execute new processes results in failures described in | |
http://stackoverflow.com/questions/32573385/problems-starting-processes-with-credentials | |
.PARAMETER credential | |
The credential to run under | |
.PARAMETER filename | |
The full path of the file to run | |
.PARAMETER cmdArgs | |
Optional: command line arguments. | |
.PARAMETER workingDirectory | |
Optional: Working directory. If not specified, the path of the filename is used. | |
.EXAMPLE | |
Invoke-CommandWithCredential $(Get-Credential) "c:\windows\system32\attrib.exe" "-a c:\MyTempFile.txt" | |
#> | |
[CmdletBinding()] | |
param ( | |
[parameter(Mandatory=$true)] | |
[PSCredential] $credential, | |
[parameter(Mandatory=$true)] | |
[string]$filename, | |
[string]$cmdArgs, | |
[string]$workingDirectory | |
) | |
PROCESS { | |
# Must manually inherit state such as verbosity | |
# See https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d | |
Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState | |
$output = RunAndCapture $credential $filename $cmdArgs $workingDirectory | |
return $output | |
} | |
} | |
function RunAndCapture([PSCredential] $credential, $filename, $cmdArgs, $workingDirectory) | |
{ | |
$userName = Split-Path $credential.UserName -Leaf | |
$domain = Split-Path $credential.UserName | |
Write-Debug "RunAndCapture(): Invoking with domain $domain and user $userName" | |
Write-Debug "RunAndCapture(): Invoking $filename in directory $workingDirectory with args $cmdArgs" | |
[String] $stdout = $null | |
[String] $stderr = $null | |
# Two alternatives; in my experience LaunchCommand_AsUser worked better | |
$exitCode = [PsInvoke.NativeMethods.Win32]::LaunchCommand_AsUser($filename, $cmdArgs, $workingDirectory, $domain, $userName, $credential.GetNetworkCredential().Password, [Ref] $stdout, [Ref] $stderr) | |
#$exitCode = [PsInvoke.NativeMethods.Win32]::LaunchCommand_WithLogon($filename, $cmdArgs, $workingDirectory, $domain, $userName, $credential.GetNetworkCredential().Password, [Ref] $stdout, [Ref] $stderr) | |
Write-Verbose ("exit code: " + $exitCode) | |
if (-not [string]::IsNullOrEmpty($stdout)) | |
{ | |
Write-Verbose ("Output: " + $stdout) | |
} | |
if (-not [string]::IsNullOrEmpty($stderr)) | |
{ | |
Write-Verbose ("Errors: " + $stderr) | |
} | |
return @{ | |
ExitCode = $exitCode | |
stdout = $stdout | |
stderr = $stderr | |
} | |
} | |
$Win32File = | |
@' | |
using System; | |
using System.IO; | |
using System.IO.Pipes; | |
using System.Runtime.InteropServices; | |
using System.Text; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using Microsoft.Win32.SafeHandles; | |
namespace PsInvoke.NativeMethods | |
{ | |
// from http://blogs.msdn.com/b/alejacma/archive/2007/12/20/how-to-call-createprocesswithlogonw-createprocessasuser-in-net.aspx | |
public class Win32 | |
{ | |
#region Constants | |
const UInt32 INFINITE = 0xFFFFFFFF; | |
const UInt32 WAIT_FAILED = 0xFFFFFFFF; | |
#endregion | |
#region Enumerations | |
[Flags] | |
public enum LogonType | |
{ | |
LOGON32_LOGON_INTERACTIVE = 2, | |
LOGON32_LOGON_NETWORK = 3, | |
LOGON32_LOGON_BATCH = 4, | |
LOGON32_LOGON_SERVICE = 5, | |
LOGON32_LOGON_UNLOCK = 7, | |
LOGON32_LOGON_NETWORK_CLEARTEXT = 8, | |
LOGON32_LOGON_NEW_CREDENTIALS = 9 | |
} | |
[Flags] | |
public enum LogonProvider | |
{ | |
LOGON32_PROVIDER_DEFAULT = 0, | |
LOGON32_PROVIDER_WINNT35, | |
LOGON32_PROVIDER_WINNT40, | |
LOGON32_PROVIDER_WINNT50 | |
} | |
// http://pinvoke.net/default.aspx/advapi32.CreateProcessWithLogonW | |
[Flags] | |
public enum CreationFlags | |
{ | |
CREATE_SUSPENDED = 0x00000004, | |
CREATE_NEW_CONSOLE = 0x00000010, | |
CREATE_NEW_PROCESS_GROUP = 0x00000200, | |
CREATE_UNICODE_ENVIRONMENT = 0x00000400, | |
CREATE_SEPARATE_WOW_VDM = 0x00000800, | |
CREATE_DEFAULT_ERROR_MODE = 0x04000000, | |
} | |
// http://pinvoke.net/default.aspx/advapi32.CreateProcessWithLogonW | |
[Flags] | |
public enum LogonFlags | |
{ | |
NONE = 0x0, | |
LOGON_WITH_PROFILE = 0x00000001, | |
LOGON_NETCREDENTIALS_ONLY = 0x00000002 | |
} | |
// http://pinvoke.net/default.aspx/Enums/STARTF.html | |
// was incomplete, added more from | |
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms686331%28v=vs.85%29.aspx | |
[Flags] | |
public enum STARTF : uint | |
{ | |
STARTF_USESHOWWINDOW = 0x00000001, | |
STARTF_USESIZE = 0x00000002, | |
STARTF_USEPOSITION = 0x00000004, | |
STARTF_USECOUNTCHARS = 0x00000008, | |
STARTF_USEFILLATTRIBUTE = 0x00000010, | |
STARTF_RUNFULLSCREEN = 0x00000020, // ignored for non-x86 platforms | |
STARTF_FORCEONFEEDBACK = 0x00000040, | |
STARTF_FORCEOFFFEEDBACK = 0x00000080, | |
STARTF_USESTDHANDLES = 0x00000100, | |
STARTF_USEHOTKEY = 0x00000200, | |
STARTF_TITLEISLINKNAME = 0x00000800, | |
STARTF_TITLEISAPPID = 0x00001000, | |
STARTF_PREVENTPINNING = 0x00002000, | |
STARTF_UNTRUSTEDSOURCE = 0x00008000, | |
} | |
#endregion | |
#region Structs | |
[StructLayout(LayoutKind.Sequential)] | |
public struct STARTUPINFO | |
{ | |
public Int32 cb; | |
public String lpReserved; | |
public String lpDesktop; | |
public String lpTitle; | |
public Int32 dwX; | |
public Int32 dwY; | |
public Int32 dwXSize; | |
public Int32 dwYSize; | |
public Int32 dwXCountChars; | |
public Int32 dwYCountChars; | |
public Int32 dwFillAttribute; | |
[MarshalAs(UnmanagedType.I4)] | |
public STARTF dwFlags; | |
public Int16 wShowWindow; | |
public Int16 cbReserved2; | |
public IntPtr lpReserved2; | |
public SafeFileHandle stdInput; | |
public SafeFileHandle stdOutput; | |
public SafeFileHandle stdError; | |
//public IntPtr hStdInput; | |
//public IntPtr hStdOutput; | |
//public IntPtr hStdError; | |
} | |
[StructLayout(LayoutKind.Sequential)] | |
public struct PROCESS_INFORMATION | |
{ | |
public IntPtr hProcess; | |
public IntPtr hThread; | |
public Int32 dwProcessId; | |
public Int32 dwThreadId; | |
} | |
[StructLayout(LayoutKind.Sequential)] | |
public class SECURITY_ATTRIBUTES | |
{ | |
public int nLength; | |
public IntPtr lpSecurityDescriptor; | |
public bool bInheritHandle; | |
public SECURITY_ATTRIBUTES() | |
{ | |
nLength = 12; | |
lpSecurityDescriptor = IntPtr.Zero; | |
} | |
} | |
#endregion | |
#region P/Invoke Functions | |
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] | |
public static extern Boolean LogonUser | |
( | |
String lpszUserName, | |
String lpszDomain, | |
String lpszPassword, | |
LogonType dwLogonType, | |
LogonProvider dwLogonProvider, | |
out IntPtr phToken | |
); | |
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] | |
public static extern Boolean CreateProcessAsUserW | |
( | |
IntPtr hToken, | |
String lpApplicationName, | |
String lpCommandLine, | |
IntPtr lpProcessAttributes, | |
IntPtr lpThreadAttributes, | |
Boolean bInheritHandles, | |
Int32 dwCreationFlags, | |
IntPtr lpEnvironment, | |
String lpCurrentDirectory, | |
ref STARTUPINFO lpStartupInfo, | |
out PROCESS_INFORMATION lpProcessInformation | |
); | |
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] | |
public static extern Boolean CreateProcessWithLogonW | |
( | |
String lpszUsername, | |
String lpszDomain, | |
String lpszPassword, | |
[MarshalAs(UnmanagedType.U4)] | |
LogonFlags dwLogonFlags, | |
String applicationName, | |
String commandLine, | |
Int32 creationFlags, | |
IntPtr environment, | |
String currentDirectory, | |
ref STARTUPINFO sui, | |
out PROCESS_INFORMATION processInfo | |
); | |
[DllImport("kernel32.dll", SetLastError = true)] | |
public static extern UInt32 WaitForSingleObject | |
( | |
IntPtr hHandle, | |
UInt32 dwMilliseconds | |
); | |
[DllImport("kernel32", SetLastError = true)] | |
public static extern Boolean CloseHandle(IntPtr handle); | |
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] | |
public static extern bool CreatePipe(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, | |
SECURITY_ATTRIBUTES lpPipeAttributes, int nSize); | |
[DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)] | |
public static extern bool DuplicateHandle(IntPtr hSourceProcessHandle, SafeHandle hSourceHandle, | |
IntPtr hTargetProcess, out SafeFileHandle targetHandle, int dwDesiredAccess, | |
bool bInheritHandle, int dwOptions); | |
[DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)] | |
public static extern IntPtr GetCurrentProcess(); | |
// http://www.pinvoke.net/default.aspx/kernel32.getexitcodeprocess | |
[DllImport("kernel32.dll", SetLastError = true)] | |
[return: MarshalAs(UnmanagedType.Bool)] | |
static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode); | |
#endregion | |
#region Helpers | |
public static void CreatePipeWithSecurityAttributes(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, | |
SECURITY_ATTRIBUTES lpPipeAttributes, int nSize) | |
{ | |
hReadPipe = null; | |
if ((!CreatePipe(out hReadPipe, out hWritePipe, lpPipeAttributes, nSize) || hReadPipe.IsInvalid) || hWritePipe.IsInvalid) | |
throw new Exception(); | |
} | |
public static void CreatePipe(out SafeFileHandle parentHandle, out SafeFileHandle childHandle, bool parentInputs) | |
{ | |
SECURITY_ATTRIBUTES lpPipeAttributes = new SECURITY_ATTRIBUTES(); | |
lpPipeAttributes.bInheritHandle = true; | |
SafeFileHandle hWritePipe = null; | |
try | |
{ | |
if (parentInputs) | |
CreatePipeWithSecurityAttributes(out childHandle, out hWritePipe, lpPipeAttributes, 0); | |
else | |
CreatePipeWithSecurityAttributes(out hWritePipe, out childHandle, lpPipeAttributes, 0); | |
if ( | |
!DuplicateHandle(GetCurrentProcess(), hWritePipe, GetCurrentProcess(), out parentHandle, 0, false, 2)) | |
throw new Exception(); | |
} | |
finally | |
{ | |
if ((hWritePipe != null) && !hWritePipe.IsInvalid) | |
{ | |
hWritePipe.Close(); | |
} | |
} | |
} | |
private class AsyncTextStreamReaderAdapter | |
{ | |
private Stream _stream; | |
private byte[] _buffer = new byte[0x2000]; | |
private StringBuilder _sb = new StringBuilder(); | |
ManualResetEvent _completedEvent = new ManualResetEvent(false); | |
private IAsyncResult _ar; | |
public AsyncTextStreamReaderAdapter(Stream stream) | |
{ | |
_stream = stream; | |
GetNextChunk(); | |
} | |
private void GetNextChunk() | |
{ | |
_ar = _stream.BeginRead(_buffer, 0, _buffer.Length, ReadCallback, null); | |
} | |
private void ReadCallback(IAsyncResult ar) | |
{ | |
int bytesRead = _stream.EndRead(ar); | |
if (bytesRead == 0) | |
{ | |
// completed | |
_completedEvent.Set(); | |
System.Diagnostics.Debug.WriteLine("Completion on read callback"); | |
return; | |
} | |
string str = Console.OutputEncoding.GetString(_buffer, 0, bytesRead); | |
_sb.Append(str); | |
System.Diagnostics.Debug.WriteLine("Read: " + str); | |
GetNextChunk(); | |
} | |
public void Wait() | |
{ | |
_completedEvent.WaitOne(); | |
} | |
public string Result | |
{ | |
get | |
{ | |
_ar.AsyncWaitHandle.WaitOne(); | |
Wait(); | |
return _sb.ToString(); | |
} | |
} | |
} | |
private const int _bufferSize = 0x2000; | |
public static string QuoteIfNeeded(string s) | |
{ | |
if (s.Contains(" ")) | |
{ | |
return string.Format("\"{0}\"", s); | |
} | |
else | |
{ | |
return s; | |
} | |
} | |
#endregion | |
#region Public Interfaces | |
public static uint LaunchCommand_WithLogon(string applicationName, string commandLine, string workingDir, string domain, string userid, string password, out string stdout, out string stderr) | |
{ | |
// Variables | |
PROCESS_INFORMATION processInfo = new PROCESS_INFORMATION(); | |
STARTUPINFO startInfo = new STARTUPINFO(); | |
bool bResult = false; | |
UInt32 uiResultWait = WAIT_FAILED; | |
// null out empty strings | |
if (workingDir == String.Empty) | |
workingDir = null; | |
// some of the pipes implementation from | |
// http://stackoverflow.com/questions/1973376/how-to-get-standard-output-from-createprocesswithlogonw | |
SafeFileHandle inputHandle = null; | |
SafeFileHandle outputHandle = null; | |
SafeFileHandle errorHandle = null; | |
CreatePipe(out inputHandle, out startInfo.stdInput, true); | |
CreatePipe(out outputHandle, out startInfo.stdOutput, false); | |
CreatePipe(out errorHandle, out startInfo.stdError, false); | |
startInfo.cb = Marshal.SizeOf(startInfo); | |
// get a lot of 0xc0000142 errors when using this | |
// not sure if it has anything to do with | |
// http://deshack.net/windows-how-to-solve-application-error-0xc0000142-and-0xc0000005/ | |
// in which you need to remove things using autoruns and/or registry | |
//startInfo.lpDesktop = "winsta0\\default"; | |
startInfo.dwFlags = STARTF.STARTF_USESTDHANDLES; | |
try | |
{ | |
// Create process | |
bResult = CreateProcessWithLogonW( | |
userid, | |
domain, | |
password, | |
LogonFlags.LOGON_WITH_PROFILE, | |
// regarding command line argument processing: | |
// it seems that even if you specify the application name, | |
// you must still include it in the command line; otherwise it seems that | |
// the first argument gets stripped out (perhaps Windows wants to consume that one as well | |
// as being argv[0] or whatnot). | |
applicationName, | |
//commandLine, | |
string.Format("{0} {1}", QuoteIfNeeded(applicationName), commandLine), | |
0, | |
IntPtr.Zero, | |
workingDir, | |
ref startInfo, | |
out processInfo | |
); | |
if (!bResult) { throw new Exception("CreateProcessWithLogonW error #" + Marshal.GetLastWin32Error().ToString()); } | |
// deal with the pipes | |
// (could use TPL and ReadToEndAsync() but this didn't work for me) | |
StreamWriter standardInput = new StreamWriter(new FileStream(inputHandle, FileAccess.Write, _bufferSize, false), Console.InputEncoding, _bufferSize); | |
standardInput.AutoFlush = true; | |
var stdoutStream = new FileStream(outputHandle, FileAccess.Read, _bufferSize, false); | |
var stderrStream = new FileStream(errorHandle, FileAccess.Read, _bufferSize, false); | |
AsyncTextStreamReaderAdapter stdoutAdapter = new AsyncTextStreamReaderAdapter(stdoutStream); | |
AsyncTextStreamReaderAdapter stderrAdapter = new AsyncTextStreamReaderAdapter(stderrStream); | |
// Wait for process to end | |
uiResultWait = WaitForSingleObject(processInfo.hProcess, INFINITE); | |
if (uiResultWait == WAIT_FAILED) { throw new Exception("WaitForSingleObject error #" + Marshal.GetLastWin32Error()); } | |
// finish with the pipes | |
standardInput.Close(); | |
// per http://www.databaseforum.info/25/581868.aspx must close the handles sent to the child process | |
startInfo.stdInput.Close(); | |
startInfo.stdOutput.Close(); | |
startInfo.stdError.Close(); | |
stderr = stderrAdapter.Result; | |
stdout = stdoutAdapter.Result; | |
uint exitCode; | |
if (!GetExitCodeProcess(processInfo.hProcess, out exitCode)) | |
{ | |
throw new Exception("GetExitCodeProcess error #" + Marshal.GetLastWin32Error()); | |
} | |
return exitCode; | |
} | |
finally | |
{ | |
// Close all handles | |
CloseHandle(processInfo.hProcess); | |
CloseHandle(processInfo.hThread); | |
} | |
} | |
// handle creation: | |
// http://stackoverflow.com/questions/1973376/how-to-get-standard-output-from-createprocesswithlogonw | |
public static uint LaunchCommand_AsUser(string applicationName, string commandLine, string workingDir, string domain, string userid, string password, out string stdout, out string stderr) | |
{ | |
// Variables | |
PROCESS_INFORMATION processInfo = new PROCESS_INFORMATION(); | |
STARTUPINFO startInfo = new STARTUPINFO(); | |
Boolean bResult = false; | |
IntPtr hToken = IntPtr.Zero; | |
UInt32 uiResultWait = WAIT_FAILED; | |
// null out empty strings | |
if (workingDir == String.Empty) | |
workingDir = null; | |
// some of the pipes implementation from | |
// http://stackoverflow.com/questions/1973376/how-to-get-standard-output-from-createprocesswithlogonw | |
SafeFileHandle inputHandle = null; | |
SafeFileHandle outputHandle = null; | |
SafeFileHandle errorHandle = null; | |
CreatePipe(out inputHandle, out startInfo.stdInput, true); | |
CreatePipe(out outputHandle, out startInfo.stdOutput, false); | |
CreatePipe(out errorHandle, out startInfo.stdError, false); | |
try | |
{ | |
LogonType logonType; | |
if (Environment.UserInteractive) | |
{ | |
logonType = LogonType.LOGON32_LOGON_INTERACTIVE; | |
} | |
else | |
{ | |
logonType = LogonType.LOGON32_LOGON_NEW_CREDENTIALS; | |
} | |
// Logon user | |
bResult = Win32.LogonUser( | |
userid, | |
domain, | |
password, | |
//Win32.LogonType.LOGON32_LOGON_INTERACTIVE, | |
logonType, | |
Win32.LogonProvider.LOGON32_PROVIDER_DEFAULT, | |
out hToken | |
); | |
if (!bResult) { throw new Exception("Logon error #" + Marshal.GetLastWin32Error()); } | |
// Create process | |
startInfo.cb = Marshal.SizeOf(startInfo); | |
// this caused problems with CreateProcessWithLogonW(); not sure about with CreateProcessAsUserW() | |
//startInfo.lpDesktop = "winsta0\\default"; | |
startInfo.dwFlags = STARTF.STARTF_USESTDHANDLES; | |
bResult = Win32.CreateProcessAsUserW( | |
hToken, | |
// regarding command line argument processing: | |
// it seems that even if you specify the application name, | |
// you must still include it in the command line; otherwise it seems that | |
// the first argument gets stripped out (perhaps Windows wants to consume that one as well | |
// as being argv[0] or whatnot). | |
applicationName, | |
//commandLine, | |
string.Format("{0} {1}", QuoteIfNeeded(applicationName), commandLine), | |
IntPtr.Zero, | |
IntPtr.Zero, | |
// inherit handles must be set in order to redirect handles | |
// see STARTF_USESTDHANDLES | |
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms686331%28v=vs.85%29.aspx | |
true, // false, | |
0, | |
IntPtr.Zero, | |
workingDir, | |
ref startInfo, | |
out processInfo | |
); | |
if (!bResult) { throw new Exception("CreateProcessAsUser error #" + Marshal.GetLastWin32Error()); } | |
// deal with the pipes | |
// (could use TPL and ReadToEndAsync() but this didn't work for me) | |
StreamWriter standardInput = new StreamWriter(new FileStream(inputHandle, FileAccess.Write, _bufferSize, false), Console.InputEncoding, _bufferSize); | |
standardInput.AutoFlush = true; | |
var stdoutStream = new FileStream(outputHandle, FileAccess.Read, _bufferSize, false); | |
var stderrStream = new FileStream(errorHandle, FileAccess.Read, _bufferSize, false); | |
AsyncTextStreamReaderAdapter stdoutAdapter = new AsyncTextStreamReaderAdapter(stdoutStream); | |
AsyncTextStreamReaderAdapter stderrAdapter = new AsyncTextStreamReaderAdapter(stderrStream); | |
// Wait for process to end | |
uiResultWait = WaitForSingleObject(processInfo.hProcess, INFINITE); | |
if (uiResultWait == WAIT_FAILED) { throw new Exception("WaitForSingleObject error #" + Marshal.GetLastWin32Error()); } | |
// finish with the pipes | |
standardInput.Close(); | |
// per http://www.databaseforum.info/25/581868.aspx must close the handles sent to the child process | |
startInfo.stdInput.Close(); | |
startInfo.stdOutput.Close(); | |
startInfo.stdError.Close(); | |
stderr = stderrAdapter.Result; | |
stdout = stdoutAdapter.Result; | |
uint exitCode; | |
if (!GetExitCodeProcess(processInfo.hProcess, out exitCode)) | |
{ | |
throw new Exception("GetExitCodeProcess error #" + Marshal.GetLastWin32Error()); | |
} | |
return exitCode; | |
} | |
finally | |
{ | |
// Close all handles | |
CloseHandle(hToken); | |
CloseHandle(processInfo.hProcess); | |
CloseHandle(processInfo.hThread); | |
} | |
} | |
#endregion | |
} | |
} | |
'@ | |
$Win32 = Add-Type -TypeDefinition $Win32File -PassThru |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Import-Module Get-CallerPreference | |
function Invoke-ScriptBlockWithCredential { | |
<# | |
.SYNOPSIS | |
Invokes a scriptblock with the specified credential. | |
.DESCRIPTION | |
The Invoke-ScriptBlockWithCredential function uses LogonUser and impersonation and then executes | |
the specified script block as the specified user. | |
It is useful from within DSC because DSC runs as the system, and using the typical means to | |
execute new processes etc. results in failures described in | |
http://stackoverflow.com/questions/32573385/problems-starting-processes-with-credentials | |
.PARAMETER credential | |
The credential to run under | |
.PARAMETER filename | |
The full path of the file to run | |
.PARAMETER cmdArgs | |
The command line arguments to run. | |
.EXAMPLE | |
Invoke-ScriptBlockWithCredential $(Get-Credential) { dir } | |
#> | |
[CmdletBinding()] | |
param ( | |
[parameter(Mandatory=$true)] | |
[PSCredential] $credential, | |
[parameter(Mandatory=$true)] | |
[ScriptBlock] $scriptBlock | |
) | |
PROCESS { | |
# Must manually inherit state such as verbosity | |
# See https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d | |
Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState | |
# References: | |
# https://gist.github.com/idavis/856603 | |
# http://poshcode.org/1856 | |
$userName = Split-Path $credential.UserName -Leaf | |
$domain = Split-Path $credential.UserName | |
Write-Verbose "Invoking with domain $domain and user $userName" | |
try | |
{ | |
# #define LOGON32_PROVIDER_DEFAULT 0 | |
# #define LOGON32_PROVIDER_WINNT50 3 | |
$Logon32ProviderDefault = 3 | |
# for reference (examples I saw used 9): | |
#define LOGON32_LOGON_INTERACTIVE 2 | |
#define LOGON32_LOGON_NETWORK 3 | |
#define LOGON32_LOGON_BATCH 4 | |
#define LOGON32_LOGON_SERVICE 5 | |
#define LOGON32_LOGON_NEW_CREDENTIALS 9 | |
$Logon32LogonInteractive = 3 | |
$tokenHandle = [IntPtr]::Zero | |
$userName = Split-Path $credential.UserName -Leaf | |
$domain = Split-Path $credential.UserName | |
$unmanagedString = [IntPtr]::Zero; | |
$success = $false | |
try | |
{ | |
Write-Debug "Logon interactive: $Logon32LogonInteractive; logon provider: $Logon32ProviderDefault" | |
# note LogonUser prototype; could possibly use IntPtr as well | |
#$unmanagedString = [System.Runtime.InteropServices.Marshal]::SecureStringToGlobalAllocUnicode($credential.Password); | |
#$success = $AdvApi32::LogonUser($userName, $domain, $unmanagedString, $Logon32LogonInteractive, $Logon32ProviderDefault, [Ref] $tokenHandle) | |
$success = $AdvApi32::LogonUser($userName, $domain, $credential.GetNetworkCredential().Password, $Logon32LogonInteractive, $Logon32ProviderDefault, [Ref] $tokenHandle) | |
} | |
finally | |
{ | |
[System.Runtime.InteropServices.Marshal]::ZeroFreeGlobalAllocUnicode($unmanagedString); | |
} | |
if (!$success ) | |
{ | |
$retVal = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() | |
Write-Verbose "LogonUser was unsuccessful. Error code: $retVal" | |
Write-Error "LogonUser was unsuccessful. Error code: $retVal" | |
return | |
} | |
Write-Debug "LogonUser was successful." | |
Write-Debug "Value of Windows NT token: $tokenHandle" | |
$identityName = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name | |
Write-Debug "Current Identity: $identityName" | |
$newIdentity = New-Object System.Security.Principal.WindowsIdentity( $tokenHandle ) | |
$context = $newIdentity.Impersonate() | |
$newIdentityName = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name | |
Write-Debug "Impersonated: $newIdentityName" | |
Write-Debug "Executing custom script" | |
$output = & $scriptBlock | |
Write-Verbose ("Exit code: " + $output['ExitCode']) | |
Write-Verbose ("Output: " + $output['stdout']) | |
Write-Verbose ("Errors: " + $output['stderr']) | |
return $output | |
} | |
#catch [System.Exception] | |
#{ | |
# Write-Verbose $_.Exception.ToString() | |
#} | |
finally | |
{ | |
if ( $context -ne $null ) | |
{ | |
$context.Undo() | |
} | |
if ( $tokenHandle -ne [System.IntPtr]::Zero ) | |
{ | |
$Kernel32::CloseHandle( $tokenHandle ) | |
} | |
} | |
} | |
} | |
# for lpszPassword, could also use IntPtr | |
$logonUserSignature = | |
@' | |
[DllImport( "advapi32.dll" )] | |
public static extern bool LogonUser( String lpszUserName, | |
String lpszDomain, | |
String lpszPassword, | |
int dwLogonType, | |
int dwLogonProvider, | |
ref IntPtr phToken ); | |
'@ | |
$AdvApi32 = Add-Type -MemberDefinition $logonUserSignature -Name "AdvApi32" -Namespace "PsInvoke.NativeMethods" -PassThru | |
$closeHandleSignature = | |
@' | |
[DllImport( "kernel32.dll", CharSet = CharSet.Auto )] | |
public static extern bool CloseHandle( IntPtr handle ); | |
'@ | |
$Kernel32 = Add-Type -MemberDefinition $closeHandleSignature -Name "Kernel32" -Namespace "PsInvoke.NativeMethods" -PassThru |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment