diff --git a/changelogs/fragments/win_become-passwordless.yaml b/changelogs/fragments/win_become-passwordless.yaml new file mode 100644 index 00000000000..dd35eb9fcca --- /dev/null +++ b/changelogs/fragments/win_become-passwordless.yaml @@ -0,0 +1,3 @@ +minor_changes: +- windows become - Add support for passwordless become. +- windows become - Moved to shared C# util so modules can utilise the code. diff --git a/docs/docsite/rst/user_guide/become.rst b/docs/docsite/rst/user_guide/become.rst index ef5b6cc1959..ccfd6c00e84 100644 --- a/docs/docsite/rst/user_guide/become.rst +++ b/docs/docsite/rst/user_guide/become.rst @@ -523,6 +523,42 @@ Because local service accounts do not have passwords, the ``ansible_become_password`` parameter is not required and is ignored if specified. +Become without setting a Password +--------------------------------- + +As of Ansible 2.8, ``become`` can be used to become a local or domain account +without requiring a password for that account. For this method to work, the +following requirements must be met: + +* The connection user has the ``SeDebugPrivilege`` privilege assigned +* The connection user is part of the ``BUILTIN\Administrators`` group +* The ``become_user`` has either the ``SeBatchLogonRight`` or ``SeNetworkLogonRight`` user right + +Using become without a password is achieved in one of two different methods: + +* Duplicating an existing logon session's token if the account is already logged on +* Using S4U to generate a logon token that is valid on the remote host only + +In the first scenario, the become process is spawned from another logon of that +user account. This could be an existing RDP logon, console logon, but this is +not guaranteed to occur all the time. This is similar to the +``Run only when user is logged on`` option for a Scheduled Task. + +In the case where another logon of the become account does not exist, S4U is +used to create a new logon and run the module through that. This is similar to +the ``Run whether user is logged on or not`` with the ``Do not store password`` +option for a Scheduled Task. In this scenario, the become process will not be +able to access any network resources like a normal WinRM process. + +To make a distinction between using become with no password and becoming an +account that has no password make sure to keep ``ansible_become_pass`` as +undefined or set ``ansible_become_pass:``. + +.. Note:: Because there are no guarantees an existing token will exist for a + user when Ansible runs, there's a high change the become process will only + have access to local resources. Use become with a password if the task needs + to access network resources + Accounts without a Password --------------------------- @@ -530,8 +566,7 @@ Accounts without a Password Ansible can be used to become an account that does not have a password (like the ``Guest`` account). To become an account without a password, set up the -variables like normal but either do not define ``ansible_become_pass`` or set -``ansible_become_pass: ''``. +variables like normal but set ``ansible_become_pass: ''``. Before become can work on an account like this, the local policy `Accounts: Limit local account use of blank passwords to console logon only `_ diff --git a/lib/ansible/executor/powershell/become_wrapper.ps1 b/lib/ansible/executor/powershell/become_wrapper.ps1 index e585f94fff9..043db18a26b 100644 --- a/lib/ansible/executor/powershell/become_wrapper.ps1 +++ b/lib/ansible/executor/powershell/become_wrapper.ps1 @@ -5,6 +5,7 @@ param( [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Payload ) +#Requires -Module Ansible.ModuleUtils.AddType #AnsibleRequires -CSharpUtil Ansible.Become $ErrorActionPreference = "Stop" @@ -74,18 +75,24 @@ Function Get-BecomeFlags($flags) { } Write-AnsibleLog "INFO - loading C# become code" "become_wrapper" -$become_def = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.csharp_utils["Ansible.Become"])) +$add_type_b64 = $Payload.powershell_modules["Ansible.ModuleUtils.AddType"] +$add_type = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($add_type_b64)) +New-Module -Name Ansible.ModuleUtils.AddType -ScriptBlock ([ScriptBlock]::Create($add_type)) | Import-Module > $null -# set the TMP env var to _ansible_remote_tmp to ensure the tmp binaries are -# compiled to that location $new_tmp = [System.Environment]::ExpandEnvironmentVariables($Payload.module_args["_ansible_remote_tmp"]) -$old_tmp = $env:TMP -$env:TMP = $new_tmp -Add-Type -TypeDefinition $become_def -Debug:$false -$env:TMP = $old_tmp +$become_def = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.csharp_utils["Ansible.Become"])) +$process_def = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.csharp_utils["Ansible.Process"])) +Add-CSharpType -References $become_def, $process_def -TempPath $new_tmp -IncludeDebugInfo $username = $Payload.become_user $password = $Payload.become_password +# We need to set password to the value of NullString so a null password is preserved when crossing the .NET +# boundary. If we pass $null it will automatically be converted to "" and we need to keep the distinction for +# accounts that don't have a password and when someone wants to become without knowing the password. +if ($null -eq $password) { + $password = [NullString]::Value +} + try { $logon_type, $logon_flags = Get-BecomeFlags -flags $Payload.become_flags } catch { @@ -109,7 +116,7 @@ $bootstrap_wrapper = { &$exec_wrapper } $exec_command = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($bootstrap_wrapper.ToString())) -$lp_command_line = New-Object System.Text.StringBuilder @("powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $exec_command") +$lp_command_line = "powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $exec_command" $lp_current_directory = $env:SystemRoot # TODO: should this be set to the become user's profile dir? # pop the become_wrapper action so we don't get stuck in a loop @@ -124,8 +131,8 @@ $exec_wrapper += "`0`0`0`0" + $payload_json try { Write-AnsibleLog "INFO - starting become process '$lp_command_line'" "become_wrapper" - $result = [Ansible.Become.BecomeUtil]::RunAsUser($username, $password, $lp_command_line, - $lp_current_directory, $exec_wrapper, $logon_flags, $logon_type) + $result = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($username, $password, $logon_flags, $logon_type, + $null, $lp_command_line, $lp_current_directory, $null, $exec_wrapper) Write-AnsibleLog "INFO - become process complete with rc: $($result.ExitCode)" "become_wrapper" $stdout = $result.StandardOut try { diff --git a/lib/ansible/module_utils/csharp/Ansible.Become.cs b/lib/ansible/module_utils/csharp/Ansible.Become.cs index 3a8a8527680..7e0359d57ba 100644 --- a/lib/ansible/module_utils/csharp/Ansible.Become.cs +++ b/lib/ansible/module_utils/csharp/Ansible.Become.cs @@ -1,107 +1,364 @@ using Microsoft.Win32.SafeHandles; using System; +using System.Collections; using System.Collections.Generic; -using System.Diagnostics; using System.IO; -using System.Linq; +using System.Runtime.ConstrainedExecution; using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Security.Principal; using System.Text; -using System.Threading; - -// TODO: make some classes/structs private/internal before the next release +using Ansible.Process; +using System.Linq; namespace Ansible.Become { - [StructLayout(LayoutKind.Sequential)] - public class SECURITY_ATTRIBUTES + internal class NativeHelpers { - public int nLength; - public IntPtr lpSecurityDescriptor; - public bool bInheritHandle = false; - public SECURITY_ATTRIBUTES() + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct KERB_S4U_LOGON { - nLength = Marshal.SizeOf(this); + public UInt32 MessageType; + public UInt32 Flags; + public LSA_UNICODE_STRING ClientUpn; + public LSA_UNICODE_STRING ClientRealm; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + public struct LSA_STRING + { + public UInt16 Length; + public UInt16 MaximumLength; + [MarshalAs(UnmanagedType.LPStr)] public string Buffer; + + public static implicit operator string(LSA_STRING s) + { + return s.Buffer; + } + + public static implicit operator LSA_STRING(string s) + { + if (s == null) + s = ""; + + LSA_STRING lsaStr = new LSA_STRING + { + Buffer = s, + Length = (UInt16)s.Length, + MaximumLength = (UInt16)(s.Length + 1), + }; + return lsaStr; + } + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct LSA_UNICODE_STRING + { + public UInt16 Length; + public UInt16 MaximumLength; + public IntPtr Buffer; + } + + [StructLayout(LayoutKind.Sequential)] + public struct LUID + { + public UInt32 LowPart; + public Int32 HighPart; + + public static explicit operator UInt64(LUID l) + { + return (UInt64)((UInt64)l.HighPart << 32) | (UInt64)l.LowPart; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct LUID_AND_ATTRIBUTES + { + public LUID Luid; + public UInt32 Attributes; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SECURITY_LOGON_SESSION_DATA + { + public UInt32 Size; + public LUID LogonId; + public LSA_UNICODE_STRING UserName; + public LSA_UNICODE_STRING LogonDomain; + public LSA_UNICODE_STRING AuthenticationPackage; + public SECURITY_LOGON_TYPE LogonType; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SID_AND_ATTRIBUTES + { + public IntPtr Sid; + public int Attributes; + } + + [StructLayout(LayoutKind.Sequential)] + public struct TOKEN_PRIVILEGES + { + public UInt32 PrivilegeCount; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] + public LUID_AND_ATTRIBUTES[] Privileges; + } + + [StructLayout(LayoutKind.Sequential)] + public struct TOKEN_SOURCE + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] public char[] SourceName; + public LUID SourceIdentifier; + } + + [StructLayout(LayoutKind.Sequential)] + public struct TOKEN_STATISTICS + { + public LUID TokenId; + public LUID AuthenticationId; + } + + [StructLayout(LayoutKind.Sequential)] + public struct TOKEN_USER + { + public SID_AND_ATTRIBUTES User; + } + + public enum LogonProvider + { + LOGON32_PROVIDER_DEFAULT = 0, + } + + [Flags] + public enum ProcessAccessFlags : uint + { + PROCESS_QUERY_INFORMATION = 0x00000400, + } + + public enum SECURITY_IMPERSONATION_LEVEL + { + SecurityImpersonation, + } + + public enum SECURITY_LOGON_TYPE + { + System = 0, // Used only by the Sytem account + Interactive = 2, + Network, + Batch, + Service, + Proxy, + Unlock, + NetworkCleartext, + NewCredentials, + RemoteInteractive, + CachedInteractive, + CachedRemoteInteractive, + CachedUnlock + } + + public enum TOKEN_TYPE + { + TokenPrimary = 1, + TokenImpersonation + } + + public enum TokenElevationType + { + TokenElevationTypeDefault = 1, + TokenElevationTypeFull, + TokenElevationTypeLimited + } + + public enum TokenInformationClass + { + TokenUser = 1, + TokenPrivileges = 3, + TokenStatistics = 10, + TokenElevationType = 18, + TokenLinkedToken = 19, } } - [StructLayout(LayoutKind.Sequential)] - public class STARTUPINFO + internal class NativeMethods { - public Int32 cb; - public IntPtr lpReserved; - public IntPtr lpDesktop; - public IntPtr lpTitle; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 28)] - public byte[] _data1; - public Int32 dwFlags; - public Int16 wShowWindow; - public Int16 cbReserved2; - public IntPtr lpReserved2; - public SafeFileHandle hStdInput; - public SafeFileHandle hStdOutput; - public SafeFileHandle hStdError; - public STARTUPINFO() + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool AllocateLocallyUniqueId( + out NativeHelpers.LUID Luid); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool CloseHandle( + IntPtr hObject); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CreateProcessWithTokenW( + SafeNativeHandle hToken, + LogonFlags dwLogonFlags, + [MarshalAs(UnmanagedType.LPWStr)] string lpApplicationName, + StringBuilder lpCommandLine, + Process.NativeHelpers.ProcessCreationFlags dwCreationFlags, + SafeMemoryBuffer lpEnvironment, + [MarshalAs(UnmanagedType.LPWStr)] string lpCurrentDirectory, + Process.NativeHelpers.STARTUPINFOEX lpStartupInfo, + out Process.NativeHelpers.PROCESS_INFORMATION lpProcessInformation); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool DuplicateTokenEx( + SafeNativeHandle hExistingToken, + TokenAccessLevels dwDesiredAccess, + IntPtr lpTokenAttributes, + NativeHelpers.SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, + NativeHelpers.TOKEN_TYPE TokenType, + out SafeNativeHandle phNewToken); + + [DllImport("kernel32.dll")] + public static extern UInt32 GetCurrentThreadId(); + + [DllImport("user32.dll", SetLastError = true)] + public static extern NoopSafeHandle GetProcessWindowStation(); + + [DllImport("user32.dll", SetLastError = true)] + public static extern NoopSafeHandle GetThreadDesktop( + UInt32 dwThreadId); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool GetTokenInformation( + SafeNativeHandle TokenHandle, + NativeHelpers.TokenInformationClass TokenInformationClass, + SafeMemoryBuffer TokenInformation, + UInt32 TokenInformationLength, + out UInt32 ReturnLength); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool ImpersonateLoggedOnUser( + SafeNativeHandle hToken); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool LogonUserW( + string lpszUsername, + string lpszDomain, + string lpszPassword, + LogonType dwLogonType, + NativeHelpers.LogonProvider dwLogonProvider, + out SafeNativeHandle phToken); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool LookupPrivilegeNameW( + string lpSystemName, + ref NativeHelpers.LUID lpLuid, + StringBuilder lpName, + ref UInt32 cchName); + + [DllImport("secur32.dll", SetLastError = true)] + public static extern UInt32 LsaDeregisterLogonProcess( + IntPtr LsaHandle); + + [DllImport("secur32.dll", SetLastError = true)] + public static extern UInt32 LsaEnumerateLogonSessions( + out UInt32 LogonSessionCount, + out SafeLsaMemoryBuffer LogonSessionList); + + [DllImport("secur32.dll", SetLastError = true)] + public static extern UInt32 LsaFreeReturnBuffer( + IntPtr Buffer); + + [DllImport("secur32.dll", SetLastError = true)] + public static extern UInt32 LsaGetLogonSessionData( + IntPtr LogonId, + out SafeLsaMemoryBuffer ppLogonSessionData); + + [DllImport("secur32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern UInt32 LsaLogonUser( + SafeLsaHandle LsaHandle, + NativeHelpers.LSA_STRING OriginName, + LogonType LogonType, + UInt32 AuthenticationPackage, + IntPtr AuthenticationInformation, + UInt32 AuthenticationInformationLength, + IntPtr LocalGroups, + NativeHelpers.TOKEN_SOURCE SourceContext, + out SafeLsaMemoryBuffer ProfileBuffer, + out UInt32 ProfileBufferLength, + out NativeHelpers.LUID LogonId, + out SafeNativeHandle Token, + out IntPtr Quotas, + out UInt32 SubStatus); + + [DllImport("secur32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern UInt32 LsaLookupAuthenticationPackage( + SafeLsaHandle LsaHandle, + NativeHelpers.LSA_STRING PackageName, + out UInt32 AuthenticationPackage); + + [DllImport("advapi32.dll")] + public static extern UInt32 LsaNtStatusToWinError( + UInt32 Status); + + [DllImport("secur32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern UInt32 LsaRegisterLogonProcess( + NativeHelpers.LSA_STRING LogonProcessName, + out SafeLsaHandle LsaHandle, + out IntPtr SecurityMode); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern SafeNativeHandle OpenProcess( + NativeHelpers.ProcessAccessFlags dwDesiredAccess, + bool bInheritHandle, + UInt32 dwProcessId); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool OpenProcessToken( + SafeNativeHandle ProcessHandle, + TokenAccessLevels DesiredAccess, + out SafeNativeHandle TokenHandle); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool RevertToSelf(); + } + + internal class SafeLsaHandle : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeLsaHandle() : base(true) { } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() { - cb = Marshal.SizeOf(this); + UInt32 res = NativeMethods.LsaDeregisterLogonProcess(handle); + return res == 0; } } - [StructLayout(LayoutKind.Sequential)] - public class STARTUPINFOEX + internal class SafeLsaMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid { - public STARTUPINFO startupInfo; - public IntPtr lpAttributeList; - public STARTUPINFOEX() + public SafeLsaMemoryBuffer() : base(true) { } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() { - startupInfo = new STARTUPINFO(); - startupInfo.cb = Marshal.SizeOf(this); + UInt32 res = NativeMethods.LsaFreeReturnBuffer(handle); + return res == 0; } } - [StructLayout(LayoutKind.Sequential)] - public struct PROCESS_INFORMATION + internal class SafeNativeHandle : SafeHandleZeroOrMinusOneIsInvalid { - public IntPtr hProcess; - public IntPtr hThread; - public int dwProcessId; - public int dwThreadId; + public SafeNativeHandle() : base(true) { } + public SafeNativeHandle(IntPtr handle) : base(true) { this.handle = handle; } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + return NativeMethods.CloseHandle(handle); + } } - [StructLayout(LayoutKind.Sequential)] - public struct SID_AND_ATTRIBUTES + internal class NoopSafeHandle : SafeHandle { - public IntPtr Sid; - public int Attributes; - } + public NoopSafeHandle() : base(IntPtr.Zero, false) { } + public override bool IsInvalid { get { return false; } } - public struct TOKEN_USER - { - public SID_AND_ATTRIBUTES User; - } - - [Flags] - public enum StartupInfoFlags : uint - { - USESTDHANDLES = 0x00000100 - } - - [Flags] - public enum CreationFlags : uint - { - CREATE_BREAKAWAY_FROM_JOB = 0x01000000, - CREATE_DEFAULT_ERROR_MODE = 0x04000000, - CREATE_NEW_CONSOLE = 0x00000010, - CREATE_SUSPENDED = 0x00000004, - CREATE_UNICODE_ENVIRONMENT = 0x00000400, - EXTENDED_STARTUPINFO_PRESENT = 0x00080000 - } - - public enum HandleFlags : uint - { - None = 0, - INHERIT = 1 + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() { return true; } } [Flags] @@ -122,569 +379,489 @@ namespace Ansible.Become LOGON32_LOGON_NEW_CREDENTIALS = 9 } - public enum LogonProvider - { - LOGON32_PROVIDER_DEFAULT = 0, - } - - public enum TokenInformationClass - { - TokenUser = 1, - TokenType = 8, - TokenImpersonationLevel = 9, - TokenElevationType = 18, - TokenLinkedToken = 19, - } - - public enum TokenElevationType - { - TokenElevationTypeDefault = 1, - TokenElevationTypeFull, - TokenElevationTypeLimited - } - - [Flags] - public enum ProcessAccessFlags : uint - { - PROCESS_QUERY_INFORMATION = 0x00000400, - } - - public enum SECURITY_IMPERSONATION_LEVEL - { - SecurityImpersonation, - } - - public enum TOKEN_TYPE - { - TokenPrimary = 1, - TokenImpersonation - } - - class NativeWaitHandle : WaitHandle - { - public NativeWaitHandle(IntPtr handle) - { - this.SafeWaitHandle = new SafeWaitHandle(handle, false); - } - } - - public class Win32Exception : System.ComponentModel.Win32Exception - { - private string _msg; - public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { } - public Win32Exception(int errorCode, string message) : base(errorCode) - { - _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode); - } - public override string Message { get { return _msg; } } - public static explicit operator Win32Exception(string message) { return new Win32Exception(message); } - } - - public class CommandResult - { - public string StandardOut { get; internal set; } - public string StandardError { get; internal set; } - public uint ExitCode { get; internal set; } - } - public class BecomeUtil { - [DllImport("advapi32.dll", SetLastError = true)] - private static extern bool LogonUser( - string lpszUsername, - string lpszDomain, - string lpszPassword, - LogonType dwLogonType, - LogonProvider dwLogonProvider, - out IntPtr phToken); - - [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern bool CreateProcessWithTokenW( - IntPtr hToken, - LogonFlags dwLogonFlags, - [MarshalAs(UnmanagedType.LPTStr)] - string lpApplicationName, - StringBuilder lpCommandLine, - CreationFlags dwCreationFlags, - IntPtr lpEnvironment, - [MarshalAs(UnmanagedType.LPTStr)] - string lpCurrentDirectory, - STARTUPINFOEX lpStartupInfo, - out PROCESS_INFORMATION lpProcessInformation); - - [DllImport("kernel32.dll")] - private static extern bool CreatePipe( - out SafeFileHandle hReadPipe, - out SafeFileHandle hWritePipe, - SECURITY_ATTRIBUTES lpPipeAttributes, - uint nSize); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool SetHandleInformation( - SafeFileHandle hObject, - HandleFlags dwMask, - int dwFlags); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool GetExitCodeProcess( - IntPtr hProcess, - out uint lpExitCode); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool CloseHandle( - IntPtr hObject); - - [DllImport("user32.dll", SetLastError = true)] - private static extern IntPtr GetProcessWindowStation(); - - [DllImport("user32.dll", SetLastError = true)] - private static extern IntPtr GetThreadDesktop( - int dwThreadId); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern int GetCurrentThreadId(); - - [DllImport("advapi32.dll", SetLastError = true)] - private static extern bool GetTokenInformation( - IntPtr TokenHandle, - TokenInformationClass TokenInformationClass, - IntPtr TokenInformation, - uint TokenInformationLength, - out uint ReturnLength); - - [DllImport("psapi.dll", SetLastError = true)] - private static extern bool EnumProcesses( - [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U4)] - [In][Out] IntPtr[] processIds, - uint cb, - [MarshalAs(UnmanagedType.U4)] - out uint pBytesReturned); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern IntPtr OpenProcess( - ProcessAccessFlags processAccess, - bool bInheritHandle, - IntPtr processId); - - [DllImport("advapi32.dll", SetLastError = true)] - private static extern bool OpenProcessToken( - IntPtr ProcessHandle, - TokenAccessLevels DesiredAccess, - out IntPtr TokenHandle); - - [DllImport("advapi32.dll", SetLastError = true)] - private static extern bool ConvertSidToStringSidW( - IntPtr pSID, - [MarshalAs(UnmanagedType.LPTStr)] - out string StringSid); - - [DllImport("advapi32", SetLastError = true)] - private static extern bool DuplicateTokenEx( - IntPtr hExistingToken, - TokenAccessLevels dwDesiredAccess, - IntPtr lpTokenAttributes, - SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, - TOKEN_TYPE TokenType, - out IntPtr phNewToken); - - [DllImport("advapi32.dll", SetLastError = true)] - private static extern bool ImpersonateLoggedOnUser( - IntPtr hToken); - - [DllImport("advapi32.dll", SetLastError = true)] - private static extern bool RevertToSelf(); - - public static CommandResult RunAsUser(string username, string password, string lpCommandLine, - string lpCurrentDirectory, string stdinInput, LogonFlags logonFlags, LogonType logonType) + private static List SERVICE_SIDS = new List() { - SecurityIdentifier account = null; - if (logonType != LogonType.LOGON32_LOGON_NEW_CREDENTIALS) - { - account = GetBecomeSid(username); - } + "S-1-5-18", // NT AUTHORITY\SYSTEM + "S-1-5-19", // NT AUTHORITY\LocalService + "S-1-5-20" // NT AUTHORITY\NetworkService + }; + private static int WINDOWS_STATION_ALL_ACCESS = 0x000F037F; + private static int DESKTOP_RIGHTS_ALL_ACCESS = 0x000F01FF; - STARTUPINFOEX si = new STARTUPINFOEX(); - si.startupInfo.dwFlags = (int)StartupInfoFlags.USESTDHANDLES; - - SECURITY_ATTRIBUTES pipesec = new SECURITY_ATTRIBUTES(); - pipesec.bInheritHandle = true; - - // Create the stdout, stderr and stdin pipes used in the process and add to the startupInfo - SafeFileHandle stdout_read, stdout_write, stderr_read, stderr_write, stdin_read, stdin_write; - if (!CreatePipe(out stdout_read, out stdout_write, pipesec, 0)) - throw new Win32Exception("STDOUT pipe setup failed"); - if (!SetHandleInformation(stdout_read, HandleFlags.INHERIT, 0)) - throw new Win32Exception("STDOUT pipe handle setup failed"); - - if (!CreatePipe(out stderr_read, out stderr_write, pipesec, 0)) - throw new Win32Exception("STDERR pipe setup failed"); - if (!SetHandleInformation(stderr_read, HandleFlags.INHERIT, 0)) - throw new Win32Exception("STDERR pipe handle setup failed"); - - if (!CreatePipe(out stdin_read, out stdin_write, pipesec, 0)) - throw new Win32Exception("STDIN pipe setup failed"); - if (!SetHandleInformation(stdin_write, HandleFlags.INHERIT, 0)) - throw new Win32Exception("STDIN pipe handle setup failed"); - - si.startupInfo.hStdOutput = stdout_write; - si.startupInfo.hStdError = stderr_write; - si.startupInfo.hStdInput = stdin_read; - - // Setup the stdin buffer - UTF8Encoding utf8_encoding = new UTF8Encoding(false); - FileStream stdin_fs = new FileStream(stdin_write, FileAccess.Write, 32768); - StreamWriter stdin = new StreamWriter(stdin_fs, utf8_encoding, 32768); - - // Create the environment block if set - IntPtr lpEnvironment = IntPtr.Zero; - - CreationFlags startup_flags = CreationFlags.CREATE_UNICODE_ENVIRONMENT; - - PROCESS_INFORMATION pi = new PROCESS_INFORMATION(); - - // Get the user tokens to try running processes with - List tokens = GetUserTokens(account, username, password, logonType); - - bool launch_success = false; - foreach (IntPtr token in tokens) - { - if (CreateProcessWithTokenW( - token, - logonFlags, - null, - new StringBuilder(lpCommandLine), - startup_flags, - lpEnvironment, - lpCurrentDirectory, - si, - out pi)) - { - launch_success = true; - break; - } - } - - if (!launch_success) - throw new Win32Exception("Failed to start become process"); - - CommandResult result = new CommandResult(); - // Setup the output buffers and get stdout/stderr - FileStream stdout_fs = new FileStream(stdout_read, FileAccess.Read, 4096); - StreamReader stdout = new StreamReader(stdout_fs, utf8_encoding, true, 4096); - stdout_write.Close(); - - FileStream stderr_fs = new FileStream(stderr_read, FileAccess.Read, 4096); - StreamReader stderr = new StreamReader(stderr_fs, utf8_encoding, true, 4096); - stderr_write.Close(); - - stdin.WriteLine(stdinInput); - stdin.Close(); - - string stdout_str, stderr_str = null; - GetProcessOutput(stdout, stderr, out stdout_str, out stderr_str); - UInt32 rc = GetProcessExitCode(pi.hProcess); - - result.StandardOut = stdout_str; - result.StandardError = stderr_str; - result.ExitCode = rc; - - return result; + public static Result CreateProcessAsUser(string username, string password, string command) + { + return CreateProcessAsUser(username, password, LogonFlags.LOGON_WITH_PROFILE, LogonType.LOGON32_LOGON_INTERACTIVE, + null, command, null, null, ""); } - private static SecurityIdentifier GetBecomeSid(string username) + public static Result CreateProcessAsUser(string username, string password, LogonFlags logonFlags, LogonType logonType, + string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, IDictionary environment, + string stdin) { - NTAccount account = new NTAccount(username); - try + byte[] stdinBytes; + if (String.IsNullOrEmpty(stdin)) + stdinBytes = new byte[0]; + else { - SecurityIdentifier security_identifier = (SecurityIdentifier)account.Translate(typeof(SecurityIdentifier)); - return security_identifier; - } - catch (IdentityNotMappedException ex) - { - throw new Exception(String.Format("Unable to find become user {0}: {1}", username, ex.Message)); + if (!stdin.EndsWith(Environment.NewLine)) + stdin += Environment.NewLine; + stdinBytes = new UTF8Encoding(false).GetBytes(stdin); } + return CreateProcessAsUser(username, password, logonFlags, logonType, lpApplicationName, lpCommandLine, + lpCurrentDirectory, environment, stdinBytes); } - private static List GetUserTokens(SecurityIdentifier account, string username, string password, LogonType logonType) + /// + /// Creates a process as another user account. This method will attempt to run as another user with the + /// highest possible permissions available. The main privilege required is the SeDebugPrivilege, without + /// this privilege you can only run as a local or domain user if the username and password is specified. + /// + /// The username of the runas user + /// The password of the runas user + /// LogonFlags to control how to logon a user when the password is specified + /// Controls what type of logon is used, this only applies when the password is specified + /// The name of the executable or batch file to executable + /// The command line to execute, typically this includes lpApplication as the first argument + /// The full path to the current directory for the process, null will have the same cwd as the calling process + /// A dictionary of key/value pairs to define the new process environment + /// Bytes sent to the stdin pipe + /// Ansible.Process.Result object that contains the command output and return code + public static Result CreateProcessAsUser(string username, string password, LogonFlags logonFlags, LogonType logonType, + string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, IDictionary environment, byte[] stdin) { - List tokens = new List(); - List service_sids = new List() - { - "S-1-5-18", // NT AUTHORITY\SYSTEM - "S-1-5-19", // NT AUTHORITY\LocalService - "S-1-5-20" // NT AUTHORITY\NetworkService - }; + // While we use STARTUPINFOEX having EXTENDED_STARTUPINFO_PRESENT causes a parameter validation error + Process.NativeHelpers.ProcessCreationFlags creationFlags = Process.NativeHelpers.ProcessCreationFlags.CREATE_UNICODE_ENVIRONMENT; + Process.NativeHelpers.PROCESS_INFORMATION pi = new Process.NativeHelpers.PROCESS_INFORMATION(); + Process.NativeHelpers.STARTUPINFOEX si = new Process.NativeHelpers.STARTUPINFOEX(); + si.startupInfo.dwFlags = Process.NativeHelpers.StartupInfoFlags.USESTDHANDLES; - IntPtr hSystemToken = IntPtr.Zero; - string account_sid = ""; - if (logonType != LogonType.LOGON32_LOGON_NEW_CREDENTIALS) - { - GrantAccessToWindowStationAndDesktop(account); - // Try to get SYSTEM token handle so we can impersonate to get full admin token - hSystemToken = GetSystemUserHandle(); - account_sid = account.ToString(); - } - bool impersonated = false; + SafeFileHandle stdoutRead, stdoutWrite, stderrRead, stderrWrite, stdinRead, stdinWrite; + ProcessUtil.CreateStdioPipes(si, out stdoutRead, out stdoutWrite, out stderrRead, out stderrWrite, + out stdinRead, out stdinWrite); + FileStream stdinStream = new FileStream(stdinWrite, FileAccess.Write); - try + // $null from PowerShell ends up as an empty string, we need to convert back as an empty string doesn't + // make sense for these parameters + if (lpApplicationName == "") + lpApplicationName = null; + + if (lpCurrentDirectory == "") + lpCurrentDirectory = null; + + using (SafeMemoryBuffer lpEnvironment = ProcessUtil.CreateEnvironmentPointer(environment)) { - IntPtr hSystemTokenDup = IntPtr.Zero; - if (hSystemToken == IntPtr.Zero && service_sids.Contains(account_sid)) + // A user may have 2 tokens, 1 limited and 1 elevated. GetUserToken will try and get both but we will + // only find out if the elevated token is valid when running here. + List userTokens = GetUserTokens(username, password, logonType); + + bool launchSuccess = false; + StringBuilder commandLine = new StringBuilder(lpCommandLine); + foreach (SafeNativeHandle token in userTokens) { - // We need the SYSTEM token if we want to become one of those accounts, fail here - throw new Win32Exception("Failed to get token for NT AUTHORITY\\SYSTEM"); - } - else if (hSystemToken != IntPtr.Zero) - { - // We have the token, need to duplicate and impersonate - bool dupResult = DuplicateTokenEx( - hSystemToken, - TokenAccessLevels.MaximumAllowed, - IntPtr.Zero, - SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, - TOKEN_TYPE.TokenPrimary, - out hSystemTokenDup); - int lastError = Marshal.GetLastWin32Error(); - CloseHandle(hSystemToken); - - if (!dupResult && service_sids.Contains(account_sid)) - throw new Win32Exception(lastError, "Failed to duplicate token for NT AUTHORITY\\SYSTEM"); - else if (dupResult && account_sid != "S-1-5-18") + if (NativeMethods.CreateProcessWithTokenW(token, logonFlags, lpApplicationName, commandLine, + creationFlags, lpEnvironment, lpCurrentDirectory, si, out pi)) { - if (ImpersonateLoggedOnUser(hSystemTokenDup)) - impersonated = true; - else if (service_sids.Contains(account_sid)) - throw new Win32Exception("Failed to impersonate as SYSTEM account"); + launchSuccess = true; + break; } - // If SYSTEM impersonation failed but we're trying to become a regular user, just proceed; - // might get a limited token in UAC-enabled cases, but better than nothing... } - string domain = null; + if (!launchSuccess) + throw new Win32Exception("CreateProcessWithTokenW() failed"); + } + return ProcessUtil.WaitProcess(stdoutRead, stdoutWrite, stderrRead, stderrWrite, stdinStream, stdin, pi.hProcess); + } - if (service_sids.Contains(account_sid)) + private static List GetUserTokens(string username, string password, LogonType logonType) + { + List userTokens = new List(); + + SafeNativeHandle systemToken = null; + bool impersonated = false; + string becomeSid = username; + if (logonType != LogonType.LOGON32_LOGON_NEW_CREDENTIALS) + { + // If prefixed with .\, we are becoming a local account, strip the prefix + if (username.StartsWith(".\\")) + username = username.Substring(2); + + NTAccount account = new NTAccount(username); + becomeSid = ((SecurityIdentifier)account.Translate(typeof(SecurityIdentifier))).Value; + + // Grant access to the current Windows Station and Desktop to the become user + GrantAccessToWindowStationAndDesktop(account); + + // Try and impersonate a SYSTEM token, we need a SYSTEM token to either become a well known service + // account or have administrative rights on the become access token. + systemToken = GetPrimaryTokenForUser(new SecurityIdentifier("S-1-5-18"), new List() { "SeTcbPrivilege" }); + if (systemToken != null) + impersonated = NativeMethods.ImpersonateLoggedOnUser(systemToken); + } + + // We require impersonation if becoming a service sid or becoming a user without a password + if (!impersonated && (SERVICE_SIDS.Contains(becomeSid) || String.IsNullOrEmpty(password))) + throw new Exception("Failed to get token for NT AUTHORITY\\SYSTEM required for become as a service account or an account without a password"); + + try + { + if (becomeSid == "S-1-5-18") + userTokens.Add(systemToken); + // Cannot use String.IsEmptyOrNull() as an empty string is an account that doesn't have a pass. + // We only use S4U if no password was defined or it was null + else if (!SERVICE_SIDS.Contains(becomeSid) && password == null && logonType != LogonType.LOGON32_LOGON_NEW_CREDENTIALS) { - // We're using a well-known service account, do a service logon instead of the actual flag set - logonType = LogonType.LOGON32_LOGON_SERVICE; - domain = "NT AUTHORITY"; - password = null; - switch (account_sid) + // If no password was specified, try and duplicate an existing token for that user or use S4U to + // generate one without network credentials + SecurityIdentifier sid = new SecurityIdentifier(becomeSid); + SafeNativeHandle becomeToken = GetPrimaryTokenForUser(sid); + if (becomeToken != null) { - case "S-1-5-18": - tokens.Add(hSystemTokenDup); - return tokens; - case "S-1-5-19": - username = "LocalService"; - break; - case "S-1-5-20": - username = "NetworkService"; - break; + userTokens.Add(GetElevatedToken(becomeToken)); + userTokens.Add(becomeToken); + } + else + { + becomeToken = GetS4UTokenForUser(sid, logonType); + userTokens.Add(becomeToken); } } else { - // We are trying to become a local or domain account - if (username.Contains(@"\")) + string domain = null; + switch (becomeSid) { - var user_split = username.Split(Convert.ToChar(@"\")); - domain = user_split[0]; - username = user_split[1]; + case "S-1-5-19": + logonType = LogonType.LOGON32_LOGON_SERVICE; + domain = "NT AUTHORITY"; + username = "LocalService"; + break; + case "S-1-5-20": + logonType = LogonType.LOGON32_LOGON_SERVICE; + domain = "NT AUTHORITY"; + username = "NetworkService"; + break; + default: + // Trying to become a local or domain account + if (username.Contains(@"\")) + { + string[] userSplit = username.Split(new char[1] { '\\' }, 2); + domain = userSplit[0]; + username = userSplit[1]; + } + else if (!username.Contains("@")) + domain = "."; + break; } - else if (username.Contains("@")) - domain = null; - else - domain = "."; - } - IntPtr hToken = IntPtr.Zero; - if (!LogonUser( - username, - domain, - password, - logonType, - LogonProvider.LOGON32_PROVIDER_DEFAULT, - out hToken)) - { - throw new Win32Exception("LogonUser failed"); - } + SafeNativeHandle hToken; + if (!NativeMethods.LogonUserW(username, domain, password, logonType, + NativeHelpers.LogonProvider.LOGON32_PROVIDER_DEFAULT, out hToken)) + { + throw new Win32Exception("LogonUserW() failed"); + } - if (!service_sids.Contains(account_sid)) - { - // Try and get the elevated token for local/domain account - IntPtr hTokenElevated = GetElevatedToken(hToken); - tokens.Add(hTokenElevated); + // Get the elevated token for a local/domain accounts only + if (!SERVICE_SIDS.Contains(becomeSid)) + userTokens.Add(GetElevatedToken(hToken)); + userTokens.Add(hToken); } - - // add the original token as a fallback - tokens.Add(hToken); } finally { if (impersonated) - RevertToSelf(); + NativeMethods.RevertToSelf(); } - return tokens; + return userTokens; } - private static IntPtr GetSystemUserHandle() + private static SafeNativeHandle GetPrimaryTokenForUser(SecurityIdentifier sid, List requiredPrivileges = null) { - uint array_byte_size = 1024 * sizeof(uint); - IntPtr[] pids = new IntPtr[1024]; - uint bytes_copied; + NativeHelpers.ProcessAccessFlags accessFlags = NativeHelpers.ProcessAccessFlags.PROCESS_QUERY_INFORMATION; + // According to CreateProcessWithTokenW we require a token with + // TOKEN_QUERY, TOKEN_DUPLICATE and TOKEN_ASSIGN_PRIMARY + // Also add in TOKEN_IMPERSONATE so we can get an impersonated token + TokenAccessLevels dwAccess = TokenAccessLevels.Query | + TokenAccessLevels.Duplicate | + TokenAccessLevels.AssignPrimary | + TokenAccessLevels.Impersonate; - if (!EnumProcesses(pids, array_byte_size, out bytes_copied)) + foreach (System.Diagnostics.Process process in System.Diagnostics.Process.GetProcesses()) { - throw new Win32Exception("Failed to enumerate processes"); - } - // TODO: Handle if bytes_copied is larger than the array size and rerun EnumProcesses with larger array - uint num_processes = bytes_copied / sizeof(uint); - - for (uint i = 0; i < num_processes; i++) - { - IntPtr hProcess = OpenProcess(ProcessAccessFlags.PROCESS_QUERY_INFORMATION, false, pids[i]); - if (hProcess != IntPtr.Zero) + using (process) { - IntPtr hToken = IntPtr.Zero; - // According to CreateProcessWithTokenW we require a token with - // TOKEN_QUERY, TOKEN_DUPLICATE and TOKEN_ASSIGN_PRIMARY - // Also add in TOKEN_IMPERSONATE so we can get an impersontated token - TokenAccessLevels desired_access = TokenAccessLevels.Query | - TokenAccessLevels.Duplicate | - TokenAccessLevels.AssignPrimary | - TokenAccessLevels.Impersonate; - - if (OpenProcessToken(hProcess, desired_access, out hToken)) + using (SafeNativeHandle hProcess = NativeMethods.OpenProcess(accessFlags, false, (UInt32)process.Id)) { - string sid = GetTokenUserSID(hToken); - if (sid == "S-1-5-18") + if (hProcess.IsInvalid) + continue; + + SafeNativeHandle hToken; + NativeMethods.OpenProcessToken(hProcess, dwAccess, out hToken); + if (hToken.IsInvalid) + continue; + + using (hToken) { - CloseHandle(hProcess); - return hToken; + if (!sid.Equals(GetTokenUserSID(hToken))) + continue; + + // Filter out any Network logon tokens, using become with that is useless when S4U + // can give us a Batch logon + NativeHelpers.SECURITY_LOGON_TYPE tokenLogonType = GetTokenLogonType(hToken); + if (tokenLogonType == NativeHelpers.SECURITY_LOGON_TYPE.Network) + continue; + + // Check that the required privileges are on the token + if (requiredPrivileges != null) + { + List actualPrivileges = GetTokenPrivileges(hToken); + int missing = requiredPrivileges.Where(x => !actualPrivileges.Contains(x)).Count(); + if (missing > 0) + continue; + } + + SafeNativeHandle dupToken; + if (!NativeMethods.DuplicateTokenEx(hToken, TokenAccessLevels.MaximumAllowed, + IntPtr.Zero, NativeHelpers.SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, + NativeHelpers.TOKEN_TYPE.TokenPrimary, out dupToken)) + { + continue; + } + + return dupToken; } } - - CloseHandle(hToken); } - CloseHandle(hProcess); } - return IntPtr.Zero; + return null; } - private static string GetTokenUserSID(IntPtr hToken) + private static SafeNativeHandle GetS4UTokenForUser(SecurityIdentifier sid, LogonType logonType) { - uint token_length; - string sid; + NTAccount becomeAccount = (NTAccount)sid.Translate(typeof(NTAccount)); + string[] userSplit = becomeAccount.Value.Split(new char[1] { '\\' }, 2); + string domainName = userSplit[0]; + string username = userSplit[1]; + bool domainUser = domainName.ToLowerInvariant() != Environment.MachineName.ToLowerInvariant(); - if (!GetTokenInformation(hToken, TokenInformationClass.TokenUser, IntPtr.Zero, 0, out token_length)) + NativeHelpers.LSA_STRING logonProcessName = "ansible"; + SafeLsaHandle lsaHandle; + IntPtr securityMode; + UInt32 res = NativeMethods.LsaRegisterLogonProcess(logonProcessName, out lsaHandle, out securityMode); + if (res != 0) + throw new Win32Exception((int)NativeMethods.LsaNtStatusToWinError(res), "LsaRegisterLogonProcess() failed"); + + using (lsaHandle) { - int last_err = Marshal.GetLastWin32Error(); - if (last_err != 122) // ERROR_INSUFFICIENT_BUFFER - throw new Win32Exception(last_err, "Failed to get TokenUser length"); - } + NativeHelpers.LSA_STRING packageName = domainUser ? "Kerberos" : "MICROSOFT_AUTHENTICATION_PACKAGE_V1_0"; + UInt32 authPackage; + res = NativeMethods.LsaLookupAuthenticationPackage(lsaHandle, packageName, out authPackage); + if (res != 0) + throw new Win32Exception((int)NativeMethods.LsaNtStatusToWinError(res), + String.Format("LsaLookupAuthenticationPackage({0}) failed", (string)packageName)); - IntPtr token_information = Marshal.AllocHGlobal((int)token_length); - try - { - if (!GetTokenInformation(hToken, TokenInformationClass.TokenUser, token_information, token_length, out token_length)) - throw new Win32Exception("Failed to get TokenUser information"); + int usernameLength = username.Length * sizeof(char); + int domainLength = domainName.Length * sizeof(char); + int authInfoLength = (Marshal.SizeOf(typeof(NativeHelpers.KERB_S4U_LOGON)) + usernameLength + domainLength); + IntPtr authInfo = Marshal.AllocHGlobal((int)authInfoLength); + try + { + IntPtr usernamePtr = IntPtr.Add(authInfo, Marshal.SizeOf(typeof(NativeHelpers.KERB_S4U_LOGON))); + IntPtr domainPtr = IntPtr.Add(usernamePtr, usernameLength); - TOKEN_USER token_user = (TOKEN_USER)Marshal.PtrToStructure(token_information, typeof(TOKEN_USER)); + // KERB_S4U_LOGON has the same structure as MSV1_0_S4U_LOGON (local accounts) + NativeHelpers.KERB_S4U_LOGON s4uLogon = new NativeHelpers.KERB_S4U_LOGON + { + MessageType = 12, // KerbS4ULogon + Flags = 0, + ClientUpn = new NativeHelpers.LSA_UNICODE_STRING + { + Length = (UInt16)usernameLength, + MaximumLength = (UInt16)usernameLength, + Buffer = usernamePtr, + }, + ClientRealm = new NativeHelpers.LSA_UNICODE_STRING + { + Length = (UInt16)domainLength, + MaximumLength = (UInt16)domainLength, + Buffer = domainPtr, + }, + }; + Marshal.StructureToPtr(s4uLogon, authInfo, false); + Marshal.Copy(username.ToCharArray(), 0, usernamePtr, username.Length); + Marshal.Copy(domainName.ToCharArray(), 0, domainPtr, domainName.Length); - if (!ConvertSidToStringSidW(token_user.User.Sid, out sid)) - throw new Win32Exception("Failed to get user SID"); - } - finally - { - Marshal.FreeHGlobal(token_information); - } + NativeHelpers.LUID sourceLuid; + if (!NativeMethods.AllocateLocallyUniqueId(out sourceLuid)) + throw new Win32Exception("AllocateLocallyUniqueId() failed"); - return sid; - } + NativeHelpers.TOKEN_SOURCE tokenSource = new NativeHelpers.TOKEN_SOURCE + { + SourceName = "ansible\0".ToCharArray(), + SourceIdentifier = sourceLuid, + }; - private static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr) - { - var sowait = new EventWaitHandle(false, EventResetMode.ManualReset); - var sewait = new EventWaitHandle(false, EventResetMode.ManualReset); - string so = null, se = null; - ThreadPool.QueueUserWorkItem((s) => - { - so = stdoutStream.ReadToEnd(); - sowait.Set(); - }); - ThreadPool.QueueUserWorkItem((s) => - { - se = stderrStream.ReadToEnd(); - sewait.Set(); - }); - foreach (var wh in new WaitHandle[] { sowait, sewait }) - wh.WaitOne(); - stdout = so; - stderr = se; - } + // Only Batch or Network will work with S4U, prefer Batch but use Network if asked + LogonType lsaLogonType = logonType == LogonType.LOGON32_LOGON_NETWORK + ? LogonType.LOGON32_LOGON_NETWORK + : LogonType.LOGON32_LOGON_BATCH; + SafeLsaMemoryBuffer profileBuffer; + UInt32 profileBufferLength; + NativeHelpers.LUID logonId; + SafeNativeHandle hToken; + IntPtr quotas; + UInt32 subStatus; - private static uint GetProcessExitCode(IntPtr processHandle) - { - new NativeWaitHandle(processHandle).WaitOne(); - uint exitCode; - if (!GetExitCodeProcess(processHandle, out exitCode)) - throw new Win32Exception("Error getting process exit code"); - return exitCode; - } + res = NativeMethods.LsaLogonUser(lsaHandle, logonProcessName, lsaLogonType, authPackage, + authInfo, (UInt32)authInfoLength, IntPtr.Zero, tokenSource, out profileBuffer, out profileBufferLength, + out logonId, out hToken, out quotas, out subStatus); + if (res != 0) + throw new Win32Exception((int)NativeMethods.LsaNtStatusToWinError(res), + String.Format("LsaLogonUser() failed with substatus {0}", subStatus)); - private static IntPtr GetElevatedToken(IntPtr hToken) - { - uint requestedLength; - - IntPtr pTokenInfo = Marshal.AllocHGlobal(sizeof(int)); - - try - { - if (!GetTokenInformation(hToken, TokenInformationClass.TokenElevationType, pTokenInfo, sizeof(int), out requestedLength)) - throw new Win32Exception("Unable to get TokenElevationType"); - - var tet = (TokenElevationType)Marshal.ReadInt32(pTokenInfo); - - // we already have the best token we can get, just use it - if (tet != TokenElevationType.TokenElevationTypeLimited) + profileBuffer.Dispose(); return hToken; - - GetTokenInformation(hToken, TokenInformationClass.TokenLinkedToken, IntPtr.Zero, 0, out requestedLength); - - IntPtr pLinkedToken = Marshal.AllocHGlobal((int)requestedLength); - - if (!GetTokenInformation(hToken, TokenInformationClass.TokenLinkedToken, pLinkedToken, requestedLength, out requestedLength)) - throw new Win32Exception("Unable to get linked token"); - - IntPtr linkedToken = Marshal.ReadIntPtr(pLinkedToken); - - Marshal.FreeHGlobal(pLinkedToken); - - return linkedToken; + } + finally + { + Marshal.FreeHGlobal(authInfo); + } } - finally + } + + private static SafeNativeHandle GetElevatedToken(SafeNativeHandle hToken) + { + // First determine if the current token is a limited token + using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, NativeHelpers.TokenInformationClass.TokenElevationType)) { - Marshal.FreeHGlobal(pTokenInfo); + NativeHelpers.TokenElevationType tet = (NativeHelpers.TokenElevationType)Marshal.ReadInt32(tokenInfo.DangerousGetHandle()); + // We already have the best token we can get, just use it + if (tet != NativeHelpers.TokenElevationType.TokenElevationTypeLimited) + return hToken; + } + + // We have a limited token, get the linked elevated token + using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, NativeHelpers.TokenInformationClass.TokenLinkedToken)) + return new SafeNativeHandle(Marshal.ReadIntPtr(tokenInfo.DangerousGetHandle())); + } + + private static List GetTokenPrivileges(SafeNativeHandle hToken) + { + using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, NativeHelpers.TokenInformationClass.TokenPrivileges)) + { + NativeHelpers.TOKEN_PRIVILEGES tokenPrivileges = (NativeHelpers.TOKEN_PRIVILEGES)Marshal.PtrToStructure( + tokenInfo.DangerousGetHandle(), typeof(NativeHelpers.TOKEN_PRIVILEGES)); + + NativeHelpers.LUID_AND_ATTRIBUTES[] luidAndAttributes = new NativeHelpers.LUID_AND_ATTRIBUTES[tokenPrivileges.PrivilegeCount]; + PtrToStructureArray(luidAndAttributes, IntPtr.Add(tokenInfo.DangerousGetHandle(), Marshal.SizeOf(tokenPrivileges.PrivilegeCount))); + + return luidAndAttributes.Select(x => GetPrivilegeName(x.Luid)).ToList(); } } - private static void GrantAccessToWindowStationAndDesktop(SecurityIdentifier account) + private static SecurityIdentifier GetTokenUserSID(SafeNativeHandle hToken) { - const int WindowStationAllAccess = 0x000f037f; - GrantAccess(account, GetProcessWindowStation(), WindowStationAllAccess); - const int DesktopRightsAllAccess = 0x000f01ff; - GrantAccess(account, GetThreadDesktop(GetCurrentThreadId()), DesktopRightsAllAccess); + using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, NativeHelpers.TokenInformationClass.TokenUser)) + { + NativeHelpers.TOKEN_USER tokenUser = (NativeHelpers.TOKEN_USER)Marshal.PtrToStructure(tokenInfo.DangerousGetHandle(), + typeof(NativeHelpers.TOKEN_USER)); + return new SecurityIdentifier(tokenUser.User.Sid); + } } - private static void GrantAccess(SecurityIdentifier account, IntPtr handle, int accessMask) + private static NativeHelpers.SECURITY_LOGON_TYPE GetTokenLogonType(SafeNativeHandle hToken) { - SafeHandle safeHandle = new NoopSafeHandle(handle); - GenericSecurity security = - new GenericSecurity(false, ResourceType.WindowObject, safeHandle, AccessControlSections.Access); - security.AddAccessRule( - new GenericAccessRule(account, accessMask, AccessControlType.Allow)); - security.Persist(safeHandle, AccessControlSections.Access); + UInt64 tokenLuidId; + using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, NativeHelpers.TokenInformationClass.TokenStatistics)) + { + NativeHelpers.TOKEN_STATISTICS stats = (NativeHelpers.TOKEN_STATISTICS)Marshal.PtrToStructure( + tokenInfo.DangerousGetHandle(), typeof(NativeHelpers.TOKEN_STATISTICS)); + tokenLuidId = (UInt64)stats.AuthenticationId; + } + + // Default to Network, if we weren't able to get the actual type treat it as an error and assume + // we don't want to run a process with the token + NativeHelpers.SECURITY_LOGON_TYPE logonType = NativeHelpers.SECURITY_LOGON_TYPE.Network; + UInt32 sessionCount; + SafeLsaMemoryBuffer sessionPtr; + UInt32 res = NativeMethods.LsaEnumerateLogonSessions(out sessionCount, out sessionPtr); + if (res != 0) + throw new Win32Exception((int)NativeMethods.LsaNtStatusToWinError(res), "LsaEnumerateLogonSession() failed"); + using (sessionPtr) + { + for (IntPtr p = sessionPtr.DangerousGetHandle(); + p != IntPtr.Add(sessionPtr.DangerousGetHandle(), (int)(IntPtr.Size * sessionCount)); + p = IntPtr.Add(p, Marshal.SizeOf(typeof(NativeHelpers.LUID)))) + { + SafeLsaMemoryBuffer sessionDataPtr; + res = NativeMethods.LsaGetLogonSessionData(p, out sessionDataPtr); + if (res != 0) + continue; + + using (sessionDataPtr) + { + NativeHelpers.SECURITY_LOGON_SESSION_DATA sessionData = (NativeHelpers.SECURITY_LOGON_SESSION_DATA)Marshal.PtrToStructure( + sessionDataPtr.DangerousGetHandle(), typeof(NativeHelpers.SECURITY_LOGON_SESSION_DATA)); + UInt64 sessionId = (UInt64)sessionData.LogonId; + if (sessionId == tokenLuidId) + { + logonType = sessionData.LogonType; + break; + } + } + } + } + + return logonType; + } + + private static SafeMemoryBuffer GetTokenInformation(SafeNativeHandle hToken, NativeHelpers.TokenInformationClass tokenClass) + { + UInt32 tokenLength; + bool res = NativeMethods.GetTokenInformation(hToken, tokenClass, new SafeMemoryBuffer(IntPtr.Zero), 0, out tokenLength); + if (!res && tokenLength == 0) // res will be false due to insufficient buffer size, we ignore if we got the buffer length + throw new Win32Exception(String.Format("GetTokenInformation({0}) failed to get buffer length", tokenClass.ToString())); + + SafeMemoryBuffer tokenInfo = new SafeMemoryBuffer((int)tokenLength); + if (!NativeMethods.GetTokenInformation(hToken, tokenClass, tokenInfo, tokenLength, out tokenLength)) + throw new Win32Exception(String.Format("GetTokenInformation({0}) failed", tokenClass.ToString())); + + return tokenInfo; + } + + private static string GetPrivilegeName(NativeHelpers.LUID luid) + { + UInt32 nameLen = 0; + NativeMethods.LookupPrivilegeNameW(null, ref luid, null, ref nameLen); + + StringBuilder name = new StringBuilder((int)(nameLen + 1)); + if (!NativeMethods.LookupPrivilegeNameW(null, ref luid, name, ref nameLen)) + throw new Win32Exception("LookupPrivilegeNameW() failed"); + + return name.ToString(); + } + + private static void PtrToStructureArray(T[] array, IntPtr ptr) + { + IntPtr ptrOffset = ptr; + for (int i = 0; i < array.Length; i++, ptrOffset = IntPtr.Add(ptrOffset, Marshal.SizeOf(typeof(T)))) + array[i] = (T)Marshal.PtrToStructure(ptrOffset, typeof(T)); + } + + private static void GrantAccessToWindowStationAndDesktop(IdentityReference account) + { + GrantAccess(account, NativeMethods.GetProcessWindowStation(), WINDOWS_STATION_ALL_ACCESS); + GrantAccess(account, NativeMethods.GetThreadDesktop(NativeMethods.GetCurrentThreadId()), DESKTOP_RIGHTS_ALL_ACCESS); + } + + private static void GrantAccess(IdentityReference account, NoopSafeHandle handle, int accessMask) + { + GenericSecurity security = new GenericSecurity(false, ResourceType.WindowObject, handle, AccessControlSections.Access); + security.AddAccessRule(new GenericAccessRule(account, accessMask, AccessControlType.Allow)); + security.Persist(handle, AccessControlSections.Access); } private class GenericSecurity : NativeObjectSecurity @@ -704,13 +881,6 @@ namespace Ansible.Become public override Type AuditRuleType { get { return typeof(AuditRule); } } } - private class NoopSafeHandle : SafeHandle - { - public NoopSafeHandle(IntPtr handle) : base(handle, false) { } - public override bool IsInvalid { get { return false; } } - protected override bool ReleaseHandle() { return true; } - } - private class GenericAccessRule : AccessRule { public GenericAccessRule(IdentityReference identity, int accessMask, AccessControlType type) : diff --git a/lib/ansible/module_utils/csharp/Ansible.Process.cs b/lib/ansible/module_utils/csharp/Ansible.Process.cs new file mode 100644 index 00000000000..0ea20b0c093 --- /dev/null +++ b/lib/ansible/module_utils/csharp/Ansible.Process.cs @@ -0,0 +1,445 @@ +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections; +using System.IO; +using System.Linq; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; + +namespace Ansible.Process +{ + internal class NativeHelpers + { + [StructLayout(LayoutKind.Sequential)] + public class SECURITY_ATTRIBUTES + { + public UInt32 nLength; + public IntPtr lpSecurityDescriptor; + public bool bInheritHandle = false; + public SECURITY_ATTRIBUTES() + { + nLength = (UInt32)Marshal.SizeOf(this); + } + } + + [StructLayout(LayoutKind.Sequential)] + public class STARTUPINFO + { + public UInt32 cb; + public IntPtr lpReserved; + [MarshalAs(UnmanagedType.LPWStr)] public string lpDesktop; + [MarshalAs(UnmanagedType.LPWStr)] public string lpTitle; + public UInt32 dwX; + public UInt32 dwY; + public UInt32 dwXSize; + public UInt32 dwYSize; + public UInt32 dwXCountChars; + public UInt32 dwYCountChars; + public UInt32 dwFillAttribute; + public StartupInfoFlags dwFlags; + public UInt16 wShowWindow; + public UInt16 cbReserved2; + public IntPtr lpReserved2; + public SafeFileHandle hStdInput; + public SafeFileHandle hStdOutput; + public SafeFileHandle hStdError; + public STARTUPINFO() + { + cb = (UInt32)Marshal.SizeOf(this); + } + } + + [StructLayout(LayoutKind.Sequential)] + public class STARTUPINFOEX + { + public STARTUPINFO startupInfo; + public IntPtr lpAttributeList; + public STARTUPINFOEX() + { + startupInfo = new STARTUPINFO(); + startupInfo.cb = (UInt32)Marshal.SizeOf(this); + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public int dwProcessId; + public int dwThreadId; + } + + [Flags] + public enum ProcessCreationFlags : uint + { + CREATE_NEW_CONSOLE = 0x00000010, + CREATE_UNICODE_ENVIRONMENT = 0x00000400, + EXTENDED_STARTUPINFO_PRESENT = 0x00080000 + } + + [Flags] + public enum StartupInfoFlags : uint + { + USESTDHANDLES = 0x00000100 + } + + [Flags] + public enum HandleFlags : uint + { + None = 0, + INHERIT = 1 + } + } + + internal class NativeMethods + { + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool AllocConsole(); + + [DllImport("shell32.dll", SetLastError = true)] + public static extern SafeMemoryBuffer CommandLineToArgvW( + [MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, + out int pNumArgs); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool CreatePipe( + out SafeFileHandle hReadPipe, + out SafeFileHandle hWritePipe, + NativeHelpers.SECURITY_ATTRIBUTES lpPipeAttributes, + UInt32 nSize); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CreateProcessW( + [MarshalAs(UnmanagedType.LPWStr)] string lpApplicationName, + StringBuilder lpCommandLine, + IntPtr lpProcessAttributes, + IntPtr lpThreadAttributes, + bool bInheritHandles, + NativeHelpers.ProcessCreationFlags dwCreationFlags, + SafeMemoryBuffer lpEnvironment, + [MarshalAs(UnmanagedType.LPWStr)] string lpCurrentDirectory, + NativeHelpers.STARTUPINFOEX lpStartupInfo, + out NativeHelpers.PROCESS_INFORMATION lpProcessInformation); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool FreeConsole(); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr GetConsoleWindow(); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool GetExitCodeProcess( + SafeWaitHandle hProcess, + out UInt32 lpExitCode); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern uint SearchPathW( + [MarshalAs(UnmanagedType.LPWStr)] string lpPath, + [MarshalAs(UnmanagedType.LPWStr)] string lpFileName, + [MarshalAs(UnmanagedType.LPWStr)] string lpExtension, + UInt32 nBufferLength, + [MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpBuffer, + out IntPtr lpFilePart); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool SetConsoleCP( + UInt32 wCodePageID); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool SetConsoleOutputCP( + UInt32 wCodePageID); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool SetHandleInformation( + SafeFileHandle hObject, + NativeHelpers.HandleFlags dwMask, + NativeHelpers.HandleFlags dwFlags); + + [DllImport("kernel32.dll")] + public static extern UInt32 WaitForSingleObject( + SafeWaitHandle hHandle, + UInt32 dwMilliseconds); + } + + internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeMemoryBuffer() : base(true) { } + public SafeMemoryBuffer(int cb) : base(true) + { + base.SetHandle(Marshal.AllocHGlobal(cb)); + } + public SafeMemoryBuffer(IntPtr handle) : base(true) + { + base.SetHandle(handle); + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + Marshal.FreeHGlobal(handle); + return true; + } + } + + public class Win32Exception : System.ComponentModel.Win32Exception + { + private string _msg; + + public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { } + public Win32Exception(int errorCode, string message) : base(errorCode) + { + _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode); + } + + public override string Message { get { return _msg; } } + public static explicit operator Win32Exception(string message) { return new Win32Exception(message); } + } + + public class Result + { + public string StandardOut { get; internal set; } + public string StandardError { get; internal set; } + public uint ExitCode { get; internal set; } + } + + public class ProcessUtil + { + /// + /// Parses a command line string into an argv array according to the Windows rules + /// + /// The command line to parse + /// An array of arguments interpreted by Windows + public static string[] ParseCommandLine(string lpCommandLine) + { + int numArgs; + using (SafeMemoryBuffer buf = NativeMethods.CommandLineToArgvW(lpCommandLine, out numArgs)) + { + if (buf.IsInvalid) + throw new Win32Exception("Error parsing command line"); + IntPtr[] strptrs = new IntPtr[numArgs]; + Marshal.Copy(buf.DangerousGetHandle(), strptrs, 0, numArgs); + return strptrs.Select(s => Marshal.PtrToStringUni(s)).ToArray(); + } + } + + /// + /// Searches the path for the executable specified. Will throw a Win32Exception if the file is not found. + /// + /// The executable to search for + /// The full path of the executable to search for + public static string SearchPath(string lpFileName) + { + StringBuilder sbOut = new StringBuilder(0); + IntPtr filePartOut = IntPtr.Zero; + UInt32 res = NativeMethods.SearchPathW(null, lpFileName, null, (UInt32)sbOut.Capacity, sbOut, out filePartOut); + if (res == 0) + { + int lastErr = Marshal.GetLastWin32Error(); + if (lastErr == 2) // ERROR_FILE_NOT_FOUND + throw new FileNotFoundException(String.Format("Could not find file '{0}'.", lpFileName)); + else + throw new Win32Exception(String.Format("SearchPathW({0}) failed to get buffer length", lpFileName)); + } + + sbOut.EnsureCapacity((int)res); + if (NativeMethods.SearchPathW(null, lpFileName, null, (UInt32)sbOut.Capacity, sbOut, out filePartOut) == 0) + throw new Win32Exception(String.Format("SearchPathW({0}) failed", lpFileName)); + + return sbOut.ToString(); + } + + public static Result CreateProcess(string command) + { + return CreateProcess(null, command, null, null, String.Empty); + } + + public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, + IDictionary environment) + { + return CreateProcess(lpApplicationName, lpCommandLine, lpCurrentDirectory, environment, String.Empty); + } + + public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, + IDictionary environment, string stdin) + { + byte[] stdinBytes; + if (String.IsNullOrEmpty(stdin)) + stdinBytes = new byte[0]; + else + { + if (!stdin.EndsWith(Environment.NewLine)) + stdin += Environment.NewLine; + stdinBytes = new UTF8Encoding(false).GetBytes(stdin); + } + return CreateProcess(lpApplicationName, lpCommandLine, lpCurrentDirectory, environment, stdinBytes); + } + + /// + /// Creates a process based on the CreateProcess API call. + /// + /// The name of the executable or batch file to execute + /// The command line to execute, typically this includes lpApplication as the first argument + /// The full path to the current directory for the process, null will have the same cwd as the calling process + /// A dictionary of key/value pairs to define the new process environment + /// A byte array to send over the stdin pipe + /// Result object that contains the command output and return code + public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, + IDictionary environment, byte[] stdin) + { + NativeHelpers.ProcessCreationFlags creationFlags = NativeHelpers.ProcessCreationFlags.CREATE_UNICODE_ENVIRONMENT | + NativeHelpers.ProcessCreationFlags.EXTENDED_STARTUPINFO_PRESENT; + NativeHelpers.PROCESS_INFORMATION pi = new NativeHelpers.PROCESS_INFORMATION(); + NativeHelpers.STARTUPINFOEX si = new NativeHelpers.STARTUPINFOEX(); + si.startupInfo.dwFlags = NativeHelpers.StartupInfoFlags.USESTDHANDLES; + + SafeFileHandle stdoutRead, stdoutWrite, stderrRead, stderrWrite, stdinRead, stdinWrite; + CreateStdioPipes(si, out stdoutRead, out stdoutWrite, out stderrRead, out stderrWrite, out stdinRead, + out stdinWrite); + FileStream stdinStream = new FileStream(stdinWrite, FileAccess.Write); + + // $null from PowerShell ends up as an empty string, we need to convert back as an empty string doesn't + // make sense for these parameters + if (lpApplicationName == "") + lpApplicationName = null; + + if (lpCurrentDirectory == "") + lpCurrentDirectory = null; + + using (SafeMemoryBuffer lpEnvironment = CreateEnvironmentPointer(environment)) + { + // Create console with utf-8 CP if no existing console is present + bool isConsole = false; + if (NativeMethods.GetConsoleWindow() == IntPtr.Zero) + { + isConsole = NativeMethods.AllocConsole(); + + // Set console input/output codepage to UTF-8 + NativeMethods.SetConsoleCP(65001); + NativeMethods.SetConsoleOutputCP(65001); + } + + try + { + StringBuilder commandLine = new StringBuilder(lpCommandLine); + if (!NativeMethods.CreateProcessW(lpApplicationName, commandLine, IntPtr.Zero, IntPtr.Zero, + true, creationFlags, lpEnvironment, lpCurrentDirectory, si, out pi)) + { + throw new Win32Exception("CreateProcessW() failed"); + } + } + finally + { + if (isConsole) + NativeMethods.FreeConsole(); + } + } + + return WaitProcess(stdoutRead, stdoutWrite, stderrRead, stderrWrite, stdinStream, stdin, pi.hProcess); + } + + internal static void CreateStdioPipes(NativeHelpers.STARTUPINFOEX si, out SafeFileHandle stdoutRead, + out SafeFileHandle stdoutWrite, out SafeFileHandle stderrRead, out SafeFileHandle stderrWrite, + out SafeFileHandle stdinRead, out SafeFileHandle stdinWrite) + { + NativeHelpers.SECURITY_ATTRIBUTES pipesec = new NativeHelpers.SECURITY_ATTRIBUTES(); + pipesec.bInheritHandle = true; + + if (!NativeMethods.CreatePipe(out stdoutRead, out stdoutWrite, pipesec, 0)) + throw new Win32Exception("STDOUT pipe setup failed"); + if (!NativeMethods.SetHandleInformation(stdoutRead, NativeHelpers.HandleFlags.INHERIT, 0)) + throw new Win32Exception("STDOUT pipe handle setup failed"); + + if (!NativeMethods.CreatePipe(out stderrRead, out stderrWrite, pipesec, 0)) + throw new Win32Exception("STDERR pipe setup failed"); + if (!NativeMethods.SetHandleInformation(stderrRead, NativeHelpers.HandleFlags.INHERIT, 0)) + throw new Win32Exception("STDERR pipe handle setup failed"); + + if (!NativeMethods.CreatePipe(out stdinRead, out stdinWrite, pipesec, 0)) + throw new Win32Exception("STDIN pipe setup failed"); + if (!NativeMethods.SetHandleInformation(stdinWrite, NativeHelpers.HandleFlags.INHERIT, 0)) + throw new Win32Exception("STDIN pipe handle setup failed"); + + si.startupInfo.hStdOutput = stdoutWrite; + si.startupInfo.hStdError = stderrWrite; + si.startupInfo.hStdInput = stdinRead; + } + + internal static SafeMemoryBuffer CreateEnvironmentPointer(IDictionary environment) + { + IntPtr lpEnvironment = IntPtr.Zero; + if (environment != null && environment.Count > 0) + { + StringBuilder environmentString = new StringBuilder(); + foreach (DictionaryEntry kv in environment) + environmentString.AppendFormat("{0}={1}\0", kv.Key, kv.Value); + environmentString.Append('\0'); + + lpEnvironment = Marshal.StringToHGlobalUni(environmentString.ToString()); + } + return new SafeMemoryBuffer(lpEnvironment); + } + + internal static Result WaitProcess(SafeFileHandle stdoutRead, SafeFileHandle stdoutWrite, SafeFileHandle stderrRead, + SafeFileHandle stderrWrite, FileStream stdinStream, byte[] stdin, IntPtr hProcess) + { + // Setup the output buffers and get stdout/stderr + UTF8Encoding utf8Encoding = new UTF8Encoding(false); + FileStream stdoutFS = new FileStream(stdoutRead, FileAccess.Read, 4096); + StreamReader stdout = new StreamReader(stdoutFS, utf8Encoding, true, 4096); + stdoutWrite.Close(); + + FileStream stderrFS = new FileStream(stderrRead, FileAccess.Read, 4096); + StreamReader stderr = new StreamReader(stderrFS, utf8Encoding, true, 4096); + stderrWrite.Close(); + + stdinStream.Write(stdin, 0, stdin.Length); + stdinStream.Close(); + + string stdoutStr, stderrStr = null; + GetProcessOutput(stdout, stderr, out stdoutStr, out stderrStr); + UInt32 rc = GetProcessExitCode(hProcess); + + return new Result + { + StandardOut = stdoutStr, + StandardError = stderrStr, + ExitCode = rc + }; + } + + internal static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr) + { + var sowait = new EventWaitHandle(false, EventResetMode.ManualReset); + var sewait = new EventWaitHandle(false, EventResetMode.ManualReset); + string so = null, se = null; + ThreadPool.QueueUserWorkItem((s) => + { + so = stdoutStream.ReadToEnd(); + sowait.Set(); + }); + ThreadPool.QueueUserWorkItem((s) => + { + se = stderrStream.ReadToEnd(); + sewait.Set(); + }); + foreach (var wh in new WaitHandle[] { sowait, sewait }) + wh.WaitOne(); + stdout = so; + stderr = se; + } + + internal static UInt32 GetProcessExitCode(IntPtr processHandle) + { + SafeWaitHandle hProcess = new SafeWaitHandle(processHandle, true); + NativeMethods.WaitForSingleObject(hProcess, 0xFFFFFFFF); + + UInt32 exitCode; + if (!NativeMethods.GetExitCodeProcess(hProcess, out exitCode)) + throw new Win32Exception("GetExitCodeProcess() failed"); + return exitCode; + } + } +} diff --git a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 index 263ea558833..44ea3ca9698 100644 --- a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 +++ b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 @@ -1,393 +1,43 @@ # Copyright (c) 2017 Ansible Project # Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) -$process_util = @" -using Microsoft.Win32.SafeHandles; -using System; -using System.Collections; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; - -namespace Ansible -{ - [StructLayout(LayoutKind.Sequential)] - public class SECURITY_ATTRIBUTES - { - public int nLength; - public IntPtr lpSecurityDescriptor; - public bool bInheritHandle = false; - public SECURITY_ATTRIBUTES() - { - nLength = Marshal.SizeOf(this); - } - } - - [StructLayout(LayoutKind.Sequential)] - public class STARTUPINFO - { - public Int32 cb; - public IntPtr lpReserved; - public IntPtr lpDesktop; - public IntPtr lpTitle; - public Int32 dwX; - public Int32 dwY; - public Int32 dwXSize; - public Int32 dwYSize; - public Int32 dwXCountChars; - public Int32 dwYCountChars; - public Int32 dwFillAttribute; - public Int32 dwFlags; - public Int16 wShowWindow; - public Int16 cbReserved2; - public IntPtr lpReserved2; - public SafeFileHandle hStdInput; - public SafeFileHandle hStdOutput; - public SafeFileHandle hStdError; - public STARTUPINFO() - { - cb = Marshal.SizeOf(this); - } - } - - [StructLayout(LayoutKind.Sequential)] - public class STARTUPINFOEX - { - public STARTUPINFO startupInfo; - public IntPtr lpAttributeList; - public STARTUPINFOEX() - { - startupInfo = new STARTUPINFO(); - startupInfo.cb = Marshal.SizeOf(this); - } - } - - [StructLayout(LayoutKind.Sequential)] - public struct PROCESS_INFORMATION - { - public IntPtr hProcess; - public IntPtr hThread; - public int dwProcessId; - public int dwThreadId; - } - - [Flags] - public enum StartupInfoFlags : uint - { - USESTDHANDLES = 0x00000100 - } - - public enum HandleFlags : uint - { - None = 0, - INHERIT = 1 - } - - class NativeWaitHandle : WaitHandle - { - public NativeWaitHandle(IntPtr handle) - { - this.SafeWaitHandle = new SafeWaitHandle(handle, false); - } - } - - public class Win32Exception : System.ComponentModel.Win32Exception - { - private string _msg; - - public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { } - - public Win32Exception(int errorCode, string message) : base(errorCode) - { - _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode); - } - - public override string Message { get { return _msg; } } - public static explicit operator Win32Exception(string message) { return new Win32Exception(message); } - } - - public class CommandUtil - { - private static UInt32 CREATE_UNICODE_ENVIRONMENT = 0x000000400; - private static UInt32 EXTENDED_STARTUPINFO_PRESENT = 0x00080000; - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)] - public static extern bool CreateProcess( - [MarshalAs(UnmanagedType.LPWStr)] - string lpApplicationName, - StringBuilder lpCommandLine, - IntPtr lpProcessAttributes, - IntPtr lpThreadAttributes, - bool bInheritHandles, - uint dwCreationFlags, - IntPtr lpEnvironment, - [MarshalAs(UnmanagedType.LPWStr)] - string lpCurrentDirectory, - STARTUPINFOEX lpStartupInfo, - out PROCESS_INFORMATION lpProcessInformation); - - [DllImport("kernel32.dll")] - public static extern bool CreatePipe( - out SafeFileHandle hReadPipe, - out SafeFileHandle hWritePipe, - SECURITY_ATTRIBUTES lpPipeAttributes, - uint nSize); - - [DllImport("kernel32.dll", SetLastError = true)] - public static extern bool SetHandleInformation( - SafeFileHandle hObject, - HandleFlags dwMask, - int dwFlags); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool GetExitCodeProcess( - IntPtr hProcess, - out uint lpExitCode); - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern uint SearchPath( - string lpPath, - string lpFileName, - string lpExtension, - int nBufferLength, - [MarshalAs (UnmanagedType.LPTStr)] - StringBuilder lpBuffer, - out IntPtr lpFilePart); - - [DllImport("kernel32.dll", SetLastError = true)] - static extern IntPtr GetConsoleWindow(); - - [DllImport("kernel32.dll", SetLastError = true)] - static extern bool AllocConsole(); - - [DllImport("kernel32.dll", SetLastError = true)] - static extern bool FreeConsole(); - - [DllImport("kernel32.dll", SetLastError = true)] - static extern bool SetConsoleCP( - UInt32 wCodePageID); - - [DllImport("kernel32.dll", SetLastError = true)] - static extern bool SetConsoleOutputCP( - UInt32 wCodePageID); - - [DllImport("shell32.dll", SetLastError = true)] - static extern IntPtr CommandLineToArgvW( - [MarshalAs(UnmanagedType.LPWStr)] - string lpCmdLine, - out int pNumArgs); - - public static string[] ParseCommandLine(string lpCommandLine) - { - int numArgs; - IntPtr ret = CommandLineToArgvW(lpCommandLine, out numArgs); - - if (ret == IntPtr.Zero) - throw new Win32Exception("Error parsing command line"); - - IntPtr[] strptrs = new IntPtr[numArgs]; - Marshal.Copy(ret, strptrs, 0, numArgs); - string[] cmdlineParts = strptrs.Select(s => Marshal.PtrToStringUni(s)).ToArray(); - - Marshal.FreeHGlobal(ret); - - return cmdlineParts; - } - - public static string SearchPath(string lpFileName) - { - StringBuilder sbOut = new StringBuilder(1024); - IntPtr filePartOut; - - if (SearchPath(null, lpFileName, null, sbOut.Capacity, sbOut, out filePartOut) == 0) - throw new FileNotFoundException(String.Format("Could not locate the following executable {0}", lpFileName)); - - return sbOut.ToString(); - } - - public class CommandResult - { - public string StandardOut { get; internal set; } - public string StandardError { get; internal set; } - public uint ExitCode { get; internal set; } - } - - public static CommandResult RunCommand(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, string stdinInput, IDictionary environment) - { - UInt32 startup_flags = CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT; - STARTUPINFOEX si = new STARTUPINFOEX(); - si.startupInfo.dwFlags = (int)StartupInfoFlags.USESTDHANDLES; - - SECURITY_ATTRIBUTES pipesec = new SECURITY_ATTRIBUTES(); - pipesec.bInheritHandle = true; - - // Create the stdout, stderr and stdin pipes used in the process and add to the startupInfo - SafeFileHandle stdout_read, stdout_write, stderr_read, stderr_write, stdin_read, stdin_write; - if (!CreatePipe(out stdout_read, out stdout_write, pipesec, 0)) - throw new Win32Exception("STDOUT pipe setup failed"); - if (!SetHandleInformation(stdout_read, HandleFlags.INHERIT, 0)) - throw new Win32Exception("STDOUT pipe handle setup failed"); - - if (!CreatePipe(out stderr_read, out stderr_write, pipesec, 0)) - throw new Win32Exception("STDERR pipe setup failed"); - if (!SetHandleInformation(stderr_read, HandleFlags.INHERIT, 0)) - throw new Win32Exception("STDERR pipe handle setup failed"); - - if (!CreatePipe(out stdin_read, out stdin_write, pipesec, 0)) - throw new Win32Exception("STDIN pipe setup failed"); - if (!SetHandleInformation(stdin_write, HandleFlags.INHERIT, 0)) - throw new Win32Exception("STDIN pipe handle setup failed"); - - si.startupInfo.hStdOutput = stdout_write; - si.startupInfo.hStdError = stderr_write; - si.startupInfo.hStdInput = stdin_read; - - // Setup the stdin buffer - UTF8Encoding utf8_encoding = new UTF8Encoding(false); - FileStream stdin_fs = new FileStream(stdin_write, FileAccess.Write, 32768); - StreamWriter stdin = new StreamWriter(stdin_fs, utf8_encoding, 32768); - - // If lpCurrentDirectory is set to null in PS it will be an empty - // string here, we need to convert it - if (lpCurrentDirectory == "") - lpCurrentDirectory = null; - - StringBuilder environmentString = null; - - if (environment != null && environment.Count > 0) - { - environmentString = new StringBuilder(); - foreach (DictionaryEntry kv in environment) - environmentString.AppendFormat("{0}={1}\0", kv.Key, kv.Value); - environmentString.Append('\0'); - } - - // Create the environment block if set - IntPtr lpEnvironment = IntPtr.Zero; - if (environmentString != null) - lpEnvironment = Marshal.StringToHGlobalUni(environmentString.ToString()); - - // Create console if needed to be inherited by child process - bool isConsole = false; - if (GetConsoleWindow() == IntPtr.Zero) { - isConsole = AllocConsole(); - - // Set console input/output codepage to UTF-8 - SetConsoleCP(65001); - SetConsoleOutputCP(65001); - } - - // Create new process and run - StringBuilder argument_string = new StringBuilder(lpCommandLine); - PROCESS_INFORMATION pi = new PROCESS_INFORMATION(); - if (!CreateProcess( - lpApplicationName, - argument_string, - IntPtr.Zero, - IntPtr.Zero, - true, - startup_flags, - lpEnvironment, - lpCurrentDirectory, - si, - out pi)) - { - throw new Win32Exception("Failed to create new process"); - } - - // Destroy console if we created it - if (isConsole) { - FreeConsole(); - } - - // Setup the output buffers and get stdout/stderr - FileStream stdout_fs = new FileStream(stdout_read, FileAccess.Read, 4096); - StreamReader stdout = new StreamReader(stdout_fs, utf8_encoding, true, 4096); - stdout_write.Close(); - FileStream stderr_fs = new FileStream(stderr_read, FileAccess.Read, 4096); - StreamReader stderr = new StreamReader(stderr_fs, utf8_encoding, true, 4096); - stderr_write.Close(); - - stdin.WriteLine(stdinInput); - stdin.Close(); - - string stdout_str, stderr_str = null; - GetProcessOutput(stdout, stderr, out stdout_str, out stderr_str); - uint rc = GetProcessExitCode(pi.hProcess); - - return new CommandResult - { - StandardOut = stdout_str, - StandardError = stderr_str, - ExitCode = rc - }; - } - - private static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr) - { - var sowait = new EventWaitHandle(false, EventResetMode.ManualReset); - var sewait = new EventWaitHandle(false, EventResetMode.ManualReset); - string so = null, se = null; - ThreadPool.QueueUserWorkItem((s) => - { - so = stdoutStream.ReadToEnd(); - sowait.Set(); - }); - ThreadPool.QueueUserWorkItem((s) => - { - se = stderrStream.ReadToEnd(); - sewait.Set(); - }); - foreach (var wh in new WaitHandle[] { sowait, sewait }) - wh.WaitOne(); - stdout = so; - stderr = se; - } - - private static uint GetProcessExitCode(IntPtr processHandle) - { - new NativeWaitHandle(processHandle).WaitOne(); - uint exitCode; - if (!GetExitCodeProcess(processHandle, out exitCode)) - throw new Win32Exception("Error getting process exit code"); - return exitCode; - } - } -} -"@ - -$ErrorActionPreference = 'Stop' +#AnsibleRequires -CSharpUtil Ansible.Process Function Load-CommandUtils { - # makes the following static functions available - # [Ansible.CommandUtil]::ParseCommandLine(string lpCommandLine) - # [Ansible.CommandUtil]::SearchPath(string lpFileName) - # [Ansible.CommandUtil]::RunCommand(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, string stdinInput, string environmentBlock) - # - # there are also numerous P/Invoke methods that can be called if you are feeling adventurous - - # FUTURE: find a better way to get the _ansible_remote_tmp variable - $original_tmp = $env:TMP - - $remote_tmp = $original_tmp - $module_params = Get-Variable -Name complex_args -ErrorAction SilentlyContinue - if ($module_params) { - if ($module_params.Value.ContainsKey("_ansible_remote_tmp") ) { - $remote_tmp = $module_params.Value["_ansible_remote_tmp"] - $remote_tmp = [System.Environment]::ExpandEnvironmentVariables($remote_tmp) + <# + .SYNOPSIS + No-op, as the C# types are automatically loaded. + #> + Param() + $msg = "Load-CommandUtils is deprecated and no longer needed, this cmdlet will be removed in a future version" + if ((Get-Command -Name Add-DeprecationWarning -ErrorAction SilentlyContinue) -and (Get-Variable -Name result -ErrorAction SilentlyContinue)) { + Add-DeprecationWarning -obj $result.Value -message $msg -version 2.12 + } else { + $module = Get-Variable -Name module -ErrorAction SilentlyContinue + if ($null -ne $module -and $module.Value.GetType().FullName -eq "Ansible.Basic.AnsibleModule") { + $module.Value.Deprecate($msg, "2.12") } } - - $env:TMP = $remote_tmp - Add-Type -TypeDefinition $process_util - $env:TMP = $original_tmp } -Function Get-ExecutablePath($executable, $directory) { - # lpApplicationName requires the full path to a file, we need to find it - # ourselves. +Function Get-ExecutablePath { + <# + .SYNOPSIS + Get's the full path to an executable, will search the directory specified or ones in the PATH env var. + + .PARAMETER executable + [String]The executable to seach for. + + .PARAMETER directory + [String] If set, the directory to search in. + + .OUTPUT + [String] The full path the executable specified. + #> + Param( + [String]$executable, + [String]$directory = $null + ) # we need to add .exe if it doesn't have an extension already if (-not [System.IO.Path]::HasExtension($executable)) { @@ -404,21 +54,41 @@ Function Get-ExecutablePath($executable, $directory) { if ($file -ne $null) { $executable_path = $file.FullName } else { - $executable_path = [Ansible.CommandUtil]::SearchPath($executable) + $executable_path = [Ansible.Process.ProcessUtil]::SearchPath($executable) } return $executable_path } Function Run-Command { + <# + .SYNOPSIS + Run a command with the CreateProcess API and return the stdout/stderr and return code. + + .PARAMETER command + The full command, including the executable, to run. + + .PARAMETER working_directory + The working directory to set on the new process, will default to the current working dir. + + .PARAMETER stdin + A string to sent over the stdin pipe to the new process. + + .PARAMETER environment + A hashtable of key/value pairs to run with the command. If set, it will replace all other env vars. + + .OUTPUT + [Hashtable] + [String]executable - The full path to the executable that was run + [String]stdout - The stdout stream of the process + [String]stderr - The stderr stream of the process + [Int32]rc - The return code of the process + #> Param( - [string]$command, # the full command to run including the executable - [string]$working_directory = $null, # the working directory to run under, will default to the current dir - [string]$stdin = $null, # a string to send to the stdin pipe when executing the command - [hashtable]$environment = @{} # a hashtable of environment values to run the command under, this will replace all the other environment variables with these + [string]$command, + [string]$working_directory = $null, + [string]$stdin = "", + [hashtable]$environment = @{} ) - - # load the C# code we call in this function - Load-CommandUtils # need to validate the working directory if it is set if ($working_directory) { @@ -430,11 +100,11 @@ Function Run-Command { # lpApplicationName needs to be the full path to an executable, we do this # by getting the executable as the first arg and then getting the full path - $arguments = [Ansible.CommandUtil]::ParseCommandLine($command) + $arguments = [Ansible.Process.ProcessUtil]::ParseCommandLine($command) $executable = Get-ExecutablePath -executable $arguments[0] -directory $working_directory # run the command and get the results - $command_result = [Ansible.CommandUtil]::RunCommand($executable, $command, $working_directory, $stdin, $environment) + $command_result = [Ansible.Process.ProcessUtil]::CreateProcess($executable, $command, $working_directory, $environment, $stdin) return ,@{ executable = $executable @@ -445,4 +115,4 @@ Function Run-Command { } # this line must stay at the bottom to ensure all defined module parts are exported -Export-ModuleMember -Alias * -Function * -Cmdlet * +Export-ModuleMember -Function Get-ExecutablePath, Load-CommandUtils, Run-Command diff --git a/test/integration/targets/win_become/tasks/main.yml b/test/integration/targets/win_become/tasks/main.yml index 9033e6af146..7f1b6bc2049 100644 --- a/test/integration/targets/win_become/tasks/main.yml +++ b/test/integration/targets/win_become/tasks/main.yml @@ -143,57 +143,6 @@ - '"LogonUser failed" not in become_invalid_pass.msg' - '"Win32ErrorCode 1326)" not in become_invalid_pass.msg' - - name: test become with SYSTEM account - win_whoami: - become: yes - become_method: runas - become_user: SYSTEM - register: whoami_out - - - name: verify output - assert: - that: - - whoami_out.account.sid == "S-1-5-18" - - whoami_out.account.account_name == "SYSTEM" - - whoami_out.account.domain_name == "NT AUTHORITY" - - whoami_out.label.account_name == 'System Mandatory Level' - - whoami_out.label.sid == 'S-1-16-16384' - - whoami_out.logon_type == 'System' - - - name: test become with NetworkService account - win_whoami: - become: yes - become_method: runas - become_user: NetworkService - register: whoami_out - - - name: verify output - assert: - that: - - whoami_out.account.sid == "S-1-5-20" - - whoami_out.account.account_name == "NETWORK SERVICE" - - whoami_out.account.domain_name == "NT AUTHORITY" - - whoami_out.label.account_name == 'System Mandatory Level' - - whoami_out.label.sid == 'S-1-16-16384' - - whoami_out.logon_type == 'Service' - - - name: test become with LocalService account - win_whoami: - become: yes - become_method: runas - become_user: LocalService - register: whoami_out - - - name: verify output - assert: - that: - - whoami_out.account.sid == "S-1-5-19" - - whoami_out.account.account_name == "LOCAL SERVICE" - - whoami_out.account.domain_name == "NT AUTHORITY" - - whoami_out.label.account_name == 'System Mandatory Level' - - whoami_out.label.sid == 'S-1-16-16384' - - whoami_out.logon_type == 'Service' - - name: test become + async vars: *become_vars win_command: whoami @@ -228,82 +177,6 @@ register: failed_flags_invalid_flag failed_when: "failed_flags_invalid_flag.msg != \"internal error: failed to parse become_flags 'logon_flags=with_profile,invalid': become_flags logon_flags value 'invalid' is not valid, valid values are: with_profile, netcredentials_only\"" - # Server 2008 doesn't work with network and network_cleartext, there isn't really a reason why you would want this anyway - - name: check if we are running on a dinosaur, neanderthal or an OS of the modern age - win_shell: | - $version = [System.Environment]::OSVersion.Version - if ($version -lt [Version]"6.1") { - "dinosaur" - } elseif ($version -lt [Version]"6.2") { - "neanderthal" - } else { - "False" - } - register: os_version - - - name: become different types - vars: *become_vars - win_whoami: - become_flags: logon_type={{item.type}} - register: become_logon_type - when: not ((item.type == 'network' or item.type == 'network_cleartext') and os_version.stdout_lines[0] == "dinosaur") - failed_when: become_logon_type.logon_type != item.actual and become_logon_type.sid != user_limited_result.sid - with_items: - - type: interactive - actual: Interactive - - type: batch - actual: Batch - - type: network - actual: Network - - type: network_cleartext - actual: NetworkCleartext - - - name: become netcredentials with network user - vars: - ansible_become_user: fakeuser - ansible_become_password: fakepassword - ansible_become_method: runas - ansible_become: True - ansible_become_flags: logon_type=new_credentials logon_flags=netcredentials_only - win_whoami: - register: become_netcredentials - - - name: assert become netcredentials with network user - assert: - that: - # new_credentials still come up as the ansible_user so we can't test that - - become_netcredentials.label.account_name == 'High Mandatory Level' - - become_netcredentials.label.sid == 'S-1-16-12288' - - - name: become logon_flags bitwise tests when loading the profile - # Error code of 2 means no file found == no profile loaded - win_shell: | - Add-Type -Name "Native" -Namespace "Ansible" -MemberDefinition '[DllImport("Userenv.dll", SetLastError=true)]public static extern bool GetProfileType(out UInt32 pdwFlags);' - $profile_type = $null - $res = [Ansible.Native]::GetProfileType([ref]$profile_type) - if (-not $res) { - $last_err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() - if ($last_err -eq 2) { - return $false - } else { - throw [System.ComponentModel.Win32Exception]$last_err - } - } else { - return $true - } - vars: *admin_become_vars - become_flags: logon_flags={{item.flags}} - register: become_logon_flags - failed_when: become_logon_flags.stdout_lines[0]|bool != item.actual - when: os_version.stdout_lines[0] not in ["dinosaur", "neanderthal"] # usual suspect 2008 doesn't support the no profile flags - with_items: - - flags: - actual: False - - flags: netcredentials_only - actual: False - - flags: with_profile,netcredentials_only - actual: True - - name: echo some non ascii characters win_command: cmd.exe /c echo über den Fußgängerübergang gehen vars: *become_vars @@ -348,7 +221,7 @@ win_user: name: "{{ become_test_username }}" state: absent - + - name: ensure privileged test user is deleted win_user: name: "{{ become_test_admin_username }}" @@ -360,7 +233,7 @@ args: executable: cmd.exe when: become_test_username in profile_dir_out.stdout_lines[0] - + - name: ensure privileged test user profile is deleted # NB: have to work around powershell limitation of long filenames until win_file fixes it win_shell: rmdir /S /Q {{ admin_profile_dir_out.stdout_lines[0] }} diff --git a/test/integration/targets/win_command/tasks/main.yml b/test/integration/targets/win_command/tasks/main.yml index f1fc8b7e4ad..74f8556a44d 100644 --- a/test/integration/targets/win_command/tasks/main.yml +++ b/test/integration/targets/win_command/tasks/main.yml @@ -28,7 +28,7 @@ - cmdout is not changed - cmdout.cmd == 'bogus_command1234' - cmdout.rc == 2 - - "'Could not locate the following executable bogus_command1234' in cmdout.msg" + - "\"Could not find file 'bogus_command1234.exe'.\" in cmdout.msg" - name: execute something with error output win_command: cmd /c "echo some output & echo some error 1>&2" diff --git a/test/integration/targets/win_csharp_utils/library/ansible_become_tests.ps1 b/test/integration/targets/win_csharp_utils/library/ansible_become_tests.ps1 new file mode 100644 index 00000000000..12ec8b2683a --- /dev/null +++ b/test/integration/targets/win_csharp_utils/library/ansible_become_tests.ps1 @@ -0,0 +1,1011 @@ +#!powershell + +#AnsibleRequires -CSharpUtil Ansible.Basic +#AnsibleRequires -CSharpUtil Ansible.Become + +$module = [Ansible.Basic.AnsibleModule]::Create($args, @{}) + +Function Assert-Equals { + param( + [Parameter(Mandatory=$true, ValueFromPipeline=$true)][AllowNull()]$Actual, + [Parameter(Mandatory=$true, Position=0)][AllowNull()]$Expected + ) + + $matched = $false + if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array]) { + $Actual.Count | Assert-Equals -Expected $Expected.Count + for ($i = 0; $i -lt $Actual.Count; $i++) { + $actual_value = $Actual[$i] + $expected_value = $Expected[$i] + Assert-Equals -Actual $actual_value -Expected $expected_value + } + $matched = $true + } else { + $matched = $Actual -ceq $Expected + } + + if (-not $matched) { + if ($Actual -is [PSObject]) { + $Actual = $Actual.ToString() + } + + $call_stack = (Get-PSCallStack)[1] + $module.Result.test = $test + $module.Result.actual = $Actual + $module.Result.expected = $Expected + $module.Result.line = $call_stack.ScriptLineNumber + $module.Result.method = $call_stack.Position.Text + $module.FailJson("AssertionError: actual != expected") + } +} + +# Would be great to move win_whomai out into it's own module util and share the +# code here, for now just rely on a cut down version +$test_whoami = { + Add-Type -TypeDefinition @' +using Microsoft.Win32.SafeHandles; +using System; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Security.Principal; +using System.Text; + +namespace Ansible +{ + internal class NativeHelpers + { + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct LSA_UNICODE_STRING + { + public UInt16 Length; + public UInt16 MaximumLength; + public IntPtr Buffer; + + public override string ToString() + { + return Marshal.PtrToStringUni(Buffer, Length / sizeof(char)); + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct LUID + { + public UInt32 LowPart; + public Int32 HighPart; + + public static explicit operator UInt64(LUID l) + { + return (UInt64)((UInt64)l.HighPart << 32) | (UInt64)l.LowPart; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct SECURITY_LOGON_SESSION_DATA + { + public UInt32 Size; + public LUID LogonId; + public LSA_UNICODE_STRING UserName; + public LSA_UNICODE_STRING LogonDomain; + public LSA_UNICODE_STRING AuthenticationPackage; + public SECURITY_LOGON_TYPE LogonType; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SID_AND_ATTRIBUTES + { + public IntPtr Sid; + public int Attributes; + } + + [StructLayout(LayoutKind.Sequential)] + public struct TOKEN_MANDATORY_LABEL + { + public SID_AND_ATTRIBUTES Label; + } + + [StructLayout(LayoutKind.Sequential)] + public struct TOKEN_SOURCE + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] public char[] SourceName; + public LUID SourceIdentifier; + } + + [StructLayout(LayoutKind.Sequential)] + public struct TOKEN_STATISTICS + { + public LUID TokenId; + public LUID AuthenticationId; + } + + [StructLayout(LayoutKind.Sequential)] + public struct TOKEN_USER + { + public SID_AND_ATTRIBUTES User; + } + + public enum SECURITY_LOGON_TYPE + { + System = 0, // Used only by the Sytem account + Interactive = 2, + Network, + Batch, + Service, + Proxy, + Unlock, + NetworkCleartext, + NewCredentials, + RemoteInteractive, + CachedInteractive, + CachedRemoteInteractive, + CachedUnlock + } + + public enum TokenInformationClass + { + TokenUser = 1, + TokenSource = 7, + TokenStatistics = 10, + TokenIntegrityLevel = 25, + } + } + + internal class NativeMethods + { + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool CloseHandle( + IntPtr hObject); + + [DllImport("kernel32.dll")] + public static extern SafeNativeHandle GetCurrentProcess(); + + [DllImport("userenv.dll", SetLastError = true)] + public static extern bool GetProfileType( + out UInt32 dwFlags); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool GetTokenInformation( + SafeNativeHandle TokenHandle, + NativeHelpers.TokenInformationClass TokenInformationClass, + SafeMemoryBuffer TokenInformation, + UInt32 TokenInformationLength, + out UInt32 ReturnLength); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool LookupAccountSid( + string lpSystemName, + IntPtr Sid, + StringBuilder lpName, + ref UInt32 cchName, + StringBuilder ReferencedDomainName, + ref UInt32 cchReferencedDomainName, + out UInt32 peUse); + + [DllImport("secur32.dll", SetLastError = true)] + public static extern UInt32 LsaEnumerateLogonSessions( + out UInt32 LogonSessionCount, + out SafeLsaMemoryBuffer LogonSessionList); + + [DllImport("secur32.dll", SetLastError = true)] + public static extern UInt32 LsaFreeReturnBuffer( + IntPtr Buffer); + + [DllImport("secur32.dll", SetLastError = true)] + public static extern UInt32 LsaGetLogonSessionData( + IntPtr LogonId, + out SafeLsaMemoryBuffer ppLogonSessionData); + + [DllImport("advapi32.dll")] + public static extern UInt32 LsaNtStatusToWinError( + UInt32 Status); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool OpenProcessToken( + SafeNativeHandle ProcessHandle, + TokenAccessLevels DesiredAccess, + out SafeNativeHandle TokenHandle); + } + + internal class SafeLsaMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeLsaMemoryBuffer() : base(true) { } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + UInt32 res = NativeMethods.LsaFreeReturnBuffer(handle); + return res == 0; + } + } + + internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeMemoryBuffer() : base(true) { } + public SafeMemoryBuffer(int cb) : base(true) + { + base.SetHandle(Marshal.AllocHGlobal(cb)); + } + public SafeMemoryBuffer(IntPtr handle) : base(true) + { + base.SetHandle(handle); + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + Marshal.FreeHGlobal(handle); + return true; + } + } + + internal class SafeNativeHandle : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeNativeHandle() : base(true) { } + public SafeNativeHandle(IntPtr handle) : base(true) { this.handle = handle; } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + return NativeMethods.CloseHandle(handle); + } + } + + public class Win32Exception : System.ComponentModel.Win32Exception + { + private string _msg; + + public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { } + public Win32Exception(int errorCode, string message) : base(errorCode) + { + _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode); + } + + public override string Message { get { return _msg; } } + public static explicit operator Win32Exception(string message) { return new Win32Exception(message); } + } + + public class Logon + { + public string AuthenticationPackage { get; internal set; } + public string LogonType { get; internal set; } + public string MandatoryLabelName { get; internal set; } + public SecurityIdentifier MandatoryLabelSid { get; internal set; } + public bool ProfileLoaded { get; internal set; } + public string SourceName { get; internal set; } + public string UserName { get; internal set; } + public SecurityIdentifier UserSid { get; internal set; } + + public Logon() + { + using (SafeNativeHandle process = NativeMethods.GetCurrentProcess()) + { + TokenAccessLevels dwAccess = TokenAccessLevels.Query | TokenAccessLevels.QuerySource; + + SafeNativeHandle hToken; + NativeMethods.OpenProcessToken(process, dwAccess, out hToken); + using (hToken) + { + SetLogonSessionData(hToken); + SetTokenMandatoryLabel(hToken); + SetTokenSource(hToken); + SetTokenUser(hToken); + } + } + SetProfileLoaded(); + } + + private void SetLogonSessionData(SafeNativeHandle hToken) + { + NativeHelpers.TokenInformationClass tokenClass = NativeHelpers.TokenInformationClass.TokenStatistics; + UInt32 returnLength; + NativeMethods.GetTokenInformation(hToken, tokenClass, new SafeMemoryBuffer(IntPtr.Zero), 0, out returnLength); + + UInt64 tokenLuidId; + using (SafeMemoryBuffer infoPtr = new SafeMemoryBuffer((int)returnLength)) + { + if (!NativeMethods.GetTokenInformation(hToken, tokenClass, infoPtr, returnLength, out returnLength)) + throw new Win32Exception("GetTokenInformation(TokenStatistics) failed"); + + NativeHelpers.TOKEN_STATISTICS stats = (NativeHelpers.TOKEN_STATISTICS)Marshal.PtrToStructure( + infoPtr.DangerousGetHandle(), typeof(NativeHelpers.TOKEN_STATISTICS)); + tokenLuidId = (UInt64)stats.AuthenticationId; + } + + UInt32 sessionCount; + SafeLsaMemoryBuffer sessionPtr; + UInt32 res = NativeMethods.LsaEnumerateLogonSessions(out sessionCount, out sessionPtr); + if (res != 0) + throw new Win32Exception((int)NativeMethods.LsaNtStatusToWinError(res), "LsaEnumerateLogonSession() failed"); + using (sessionPtr) + { + IntPtr currentSession = sessionPtr.DangerousGetHandle(); + for (UInt32 i = 0; i < sessionCount; i++) + { + SafeLsaMemoryBuffer sessionDataPtr; + res = NativeMethods.LsaGetLogonSessionData(currentSession, out sessionDataPtr); + if (res != 0) + { + currentSession = IntPtr.Add(currentSession, Marshal.SizeOf(typeof(NativeHelpers.LUID))); + continue; + } + using (sessionDataPtr) + { + NativeHelpers.SECURITY_LOGON_SESSION_DATA sessionData = (NativeHelpers.SECURITY_LOGON_SESSION_DATA)Marshal.PtrToStructure( + sessionDataPtr.DangerousGetHandle(), typeof(NativeHelpers.SECURITY_LOGON_SESSION_DATA)); + UInt64 sessionId = (UInt64)sessionData.LogonId; + if (sessionId == tokenLuidId) + { + AuthenticationPackage = sessionData.AuthenticationPackage.ToString(); + LogonType = sessionData.LogonType.ToString(); + break; + } + } + + currentSession = IntPtr.Add(currentSession, Marshal.SizeOf(typeof(NativeHelpers.LUID))); + } + } + } + + private void SetTokenMandatoryLabel(SafeNativeHandle hToken) + { + NativeHelpers.TokenInformationClass tokenClass = NativeHelpers.TokenInformationClass.TokenIntegrityLevel; + UInt32 returnLength; + NativeMethods.GetTokenInformation(hToken, tokenClass, new SafeMemoryBuffer(IntPtr.Zero), 0, out returnLength); + using (SafeMemoryBuffer infoPtr = new SafeMemoryBuffer((int)returnLength)) + { + if (!NativeMethods.GetTokenInformation(hToken, tokenClass, infoPtr, returnLength, out returnLength)) + throw new Win32Exception("GetTokenInformation(TokenIntegrityLevel) failed"); + NativeHelpers.TOKEN_MANDATORY_LABEL label = (NativeHelpers.TOKEN_MANDATORY_LABEL)Marshal.PtrToStructure( + infoPtr.DangerousGetHandle(), typeof(NativeHelpers.TOKEN_MANDATORY_LABEL)); + MandatoryLabelName = LookupSidName(label.Label.Sid); + MandatoryLabelSid = new SecurityIdentifier(label.Label.Sid); + } + } + + private void SetTokenSource(SafeNativeHandle hToken) + { + NativeHelpers.TokenInformationClass tokenClass = NativeHelpers.TokenInformationClass.TokenSource; + UInt32 returnLength; + NativeMethods.GetTokenInformation(hToken, tokenClass, new SafeMemoryBuffer(IntPtr.Zero), 0, out returnLength); + using (SafeMemoryBuffer infoPtr = new SafeMemoryBuffer((int)returnLength)) + { + if (!NativeMethods.GetTokenInformation(hToken, tokenClass, infoPtr, returnLength, out returnLength)) + throw new Win32Exception("GetTokenInformation(TokenSource) failed"); + NativeHelpers.TOKEN_SOURCE source = (NativeHelpers.TOKEN_SOURCE)Marshal.PtrToStructure( + infoPtr.DangerousGetHandle(), typeof(NativeHelpers.TOKEN_SOURCE)); + SourceName = new string(source.SourceName).Replace('\0', ' ').TrimEnd(); + } + } + + private void SetTokenUser(SafeNativeHandle hToken) + { + NativeHelpers.TokenInformationClass tokenClass = NativeHelpers.TokenInformationClass.TokenUser; + UInt32 returnLength; + NativeMethods.GetTokenInformation(hToken, tokenClass, new SafeMemoryBuffer(IntPtr.Zero), 0, out returnLength); + using (SafeMemoryBuffer infoPtr = new SafeMemoryBuffer((int)returnLength)) + { + if (!NativeMethods.GetTokenInformation(hToken, tokenClass, infoPtr, returnLength, out returnLength)) + throw new Win32Exception("GetTokenInformation(TokenSource) failed"); + NativeHelpers.TOKEN_USER user = (NativeHelpers.TOKEN_USER)Marshal.PtrToStructure( + infoPtr.DangerousGetHandle(), typeof(NativeHelpers.TOKEN_USER)); + UserName = LookupSidName(user.User.Sid); + UserSid = new SecurityIdentifier(user.User.Sid); + } + } + + private void SetProfileLoaded() + { + UInt32 flags; + ProfileLoaded = NativeMethods.GetProfileType(out flags); + } + + private static string LookupSidName(IntPtr pSid) + { + StringBuilder name = new StringBuilder(0); + StringBuilder domain = new StringBuilder(0); + UInt32 nameLength = 0; + UInt32 domainLength = 0; + UInt32 peUse; + NativeMethods.LookupAccountSid(null, pSid, name, ref nameLength, domain, ref domainLength, out peUse); + name.EnsureCapacity((int)nameLength); + domain.EnsureCapacity((int)domainLength); + + if (!NativeMethods.LookupAccountSid(null, pSid, name, ref nameLength, domain, ref domainLength, out peUse)) + throw new Win32Exception("LookupAccountSid() failed"); + + return String.Format("{0}\\{1}", domain.ToString(), name.ToString()); + } + } +} +'@ + $logon = New-Object -TypeName Ansible.Logon + ConvertTo-Json -InputObject $logon +}.ToString() + +$current_user_raw = [Ansible.Process.ProcessUtil]::CreateProcess($null, "powershell.exe -NoProfile -", $null, $null, $test_whoami + "`r`n") +$current_user = ConvertFrom-Json -InputObject $current_user_raw.StandardOut + +$adsi = [ADSI]"WinNT://$env:COMPUTERNAME" + +$standard_user = "become_standard" +$admin_user = "become_admin" +$become_pass = "password123!$([System.IO.Path]::GetRandomFileName())" +$medium_integrity_sid = "S-1-16-8192" +$high_integrity_sid = "S-1-16-12288" +$system_integrity_sid = "S-1-16-16384" + +$tests = @{ + "Runas standard user" = { + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, + "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + $stdout.LogonType | Assert-Equals -Expected "Interactive" + $stdout.ProfileLoaded | Assert-Equals -Expected $true + $stdout.UserSid.Value | Assert-Equals -Expected $standard_user_sid + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $medium_integrity_sid + } + + "Runas admin user" = { + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, + "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + $stdout.LogonType | Assert-Equals -Expected "Interactive" + $stdout.ProfileLoaded | Assert-Equals -Expected $true + $stdout.UserSid.Value | Assert-Equals -Expected $admin_user_sid + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $high_integrity_sid + } + + "Runas SYSTEM" = { + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("SYSTEM", $null, + "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + $stdout.LogonType | Assert-Equals -Expected "System" + $stdout.ProfileLoaded | Assert-Equals -Expected $true + $stdout.UserSid.Value | Assert-Equals -Expected "S-1-5-18" + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $system_integrity_sid + + $with_domain = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("NT AUTHORITY\System", $null, "whoami.exe") + $with_domain.StandardOut | Assert-Equals -Expected "nt authority\system`r`n" + } + + "Runas LocalService" = { + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("LocalService", $null, + "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + $stdout.LogonType | Assert-Equals -Expected "Service" + $stdout.ProfileLoaded | Assert-Equals -Expected $true + $stdout.UserSid.Value | Assert-Equals -Expected "S-1-5-19" + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $system_integrity_sid + + $with_domain = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("NT AUTHORITY\LocalService", $null, "whoami.exe") + $with_domain.StandardOut | Assert-Equals -Expected "nt authority\local service`r`n" + } + + "Runas NetworkService" = { + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("NetworkService", $null, + "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + $stdout.LogonType | Assert-Equals -Expected "Service" + $stdout.ProfileLoaded | Assert-Equals -Expected $true + $stdout.UserSid.Value | Assert-Equals -Expected "S-1-5-20" + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $system_integrity_sid + + $with_domain = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("NT AUTHORITY\NetworkService", $null, "whoami.exe") + $with_domain.StandardOut | Assert-Equals -Expected "nt authority\network service`r`n" + } + + "Runas without working dir set" = { + $expected = "$env:SystemRoot\system32`r`n" + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, 0, "LOGON32_LOGON_INTERACTIVE", $null, + 'powershell.exe $pwd.Path', $null, $null, "") + $actual.StandardOut | Assert-Equals -Expected $expected + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "Runas with working dir set" = { + $expected = "$env:SystemRoot`r`n" + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, 0, "LOGON32_LOGON_INTERACTIVE", $null, + 'powershell.exe $pwd.Path', $env:SystemRoot, $null, "") + $actual.StandardOut | Assert-Equals -Expected $expected + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "Runas without environment set" = { + $expected = "Windows_NT`r`n" + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, 0, "LOGON32_LOGON_INTERACTIVE", $null, + 'powershell.exe $env:TEST; $env:OS', $null, $null, "") + $actual.StandardOut | Assert-Equals -Expected $expected + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "Runas with environment set" = { + $env_vars = @{ + TEST = "tesTing" + TEST2 = "Testing 2" + } + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, 0, "LOGON32_LOGON_INTERACTIVE", $null, + 'cmd.exe /c set', $null, $env_vars, "") + ("TEST=tesTing" -cin $actual.StandardOut.Split("`r`n")) | Assert-Equals -Expected $true + ("TEST2=Testing 2" -cin $actual.StandardOut.Split("`r`n")) | Assert-Equals -Expected $true + ("OS=Windows_NT" -cnotin $actual.StandardOut.Split("`r`n")) | Assert-Equals -Expected $true + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "Runas with string stdin" = { + $expected = "input value`r`n`r`n" + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, 0, "LOGON32_LOGON_INTERACTIVE", $null, + 'powershell.exe [System.Console]::In.ReadToEnd()', $null, $null, "input value") + $actual.StandardOut | Assert-Equals -Expected $expected + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "Runas with string stdin and newline" = { + $expected = "input value`r`n`r`n" + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, 0, "LOGON32_LOGON_INTERACTIVE", $null, + 'powershell.exe [System.Console]::In.ReadToEnd()', $null, $null, "input value`r`n") + $actual.StandardOut | Assert-Equals -Expected $expected + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "Runas with byte stdin" = { + $expected = "input value`r`n" + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, 0, "LOGON32_LOGON_INTERACTIVE", $null, + 'powershell.exe [System.Console]::In.ReadToEnd()', $null, $null, [System.Text.Encoding]::UTF8.GetBytes("input value")) + $actual.StandardOut | Assert-Equals -Expected $expected + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "Missing executable" = { + $failed = $false + try { + [Ansible.Become.BecomeUtil]::CreateProcessAsUser("SYSTEM", $null, "fake.exe") + } catch { + $failed = $true + $_.Exception.InnerException.GetType().FullName | Assert-Equals -Expected "Ansible.Process.Win32Exception" + $expected = 'Exception calling "CreateProcessAsUser" with "3" argument(s): "CreateProcessWithTokenW() failed ' + $expected += '(The system cannot find the file specified, Win32ErrorCode 2)"' + $_.Exception.Message | Assert-Equals -Expected $expected + } + $failed | Assert-Equals -Expected $true + } + + "CreateProcessAsUser with lpApplicationName" = { + $expected = "abc`r`n" + $full_path = "$($env:SystemRoot)\System32\WindowsPowerShell\v1.0\powershell.exe" + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("SYSTEM", $null, 0, "LOGON32_LOGON_INTERACTIVE", $full_path, + "Write-Output 'abc'", $null, $null, "") + $actual.StandardOut | Assert-Equals -Expected $expected + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("SYSTEM", $null, 0, "LOGON32_LOGON_INTERACTIVE", $full_path, + "powershell.exe Write-Output 'abc'", $null, $null, "") + $actual.StandardOut | Assert-Equals -Expected $expected + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "CreateProcessAsUser with stderr" = { + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("SYSTEM", $null, 0, "LOGON32_LOGON_INTERACTIVE", $null, + "powershell.exe [System.Console]::Error.WriteLine('hi')", $null, $null, "") + $actual.StandardOut | Assert-Equals -Expected "" + $actual.StandardError | Assert-Equals -Expected "hi`r`n" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "CreateProcessAsUser with exit code" = { + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("SYSTEM", $null, 0, "LOGON32_LOGON_INTERACTIVE", $null, + "powershell.exe exit 10", $null, $null, "") + $actual.StandardOut | Assert-Equals -Expected "" + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 10 + } + + "Local account with computer name" = { + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("$env:COMPUTERNAME\$standard_user", $become_pass, + "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + $stdout.LogonType | Assert-Equals -Expected "Interactive" + $stdout.ProfileLoaded | Assert-Equals -Expected $true + $stdout.UserSid.Value | Assert-Equals -Expected $standard_user_sid + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $medium_integrity_sid + } + + "Local account with computer as period" = { + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser(".\$standard_user", $become_pass, + "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + $stdout.LogonType | Assert-Equals -Expected "Interactive" + $stdout.ProfileLoaded | Assert-Equals -Expected $true + $stdout.UserSid.Value | Assert-Equals -Expected $standard_user_sid + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $medium_integrity_sid + } + + "Local account with invalid password" = { + $failed = $false + try { + [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, "incorrect", "powershell.exe Write-Output abc") + } catch { + $failed = $true + $_.Exception.InnerException.GetType().FullName | Assert-Equals -Expected "Ansible.Process.Win32Exception" + # Server 2008 has a slightly different error msg, just assert we get the error 1326 + ($_.Exception.Message.Contains("Win32ErrorCode 1326")) | Assert-Equals -Expected $true + } + $failed | Assert-Equals -Expected $true + } + + "Invalid account" = { + $failed = $false + try { + [Ansible.Become.BecomeUtil]::CreateProcessAsUser("incorrect", "incorrect", "powershell.exe Write-Output abc") + } catch { + $failed = $true + $_.Exception.InnerException.GetType().FullName | Assert-Equals -Expected "System.Security.Principal.IdentityNotMappedException" + $expected = 'Exception calling "CreateProcessAsUser" with "3" argument(s): "Some or all ' + $expected += 'identity references could not be translated."' + $_.Exception.Message | Assert-Equals -Expected $expected + } + $failed | Assert-Equals -Expected $true + } + + "Interactive logon with standard" = { + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, "LOGON_WITH_PROFILE", + "LOGON32_LOGON_INTERACTIVE", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + $stdout.LogonType | Assert-Equals -Expected "Interactive" + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $medium_integrity_sid + $stdout.ProfileLoaded | Assert-Equals -Expected $true + $stdout.SourceName | Assert-Equals -Expected "Advapi" + $stdout.UserSid.Value | Assert-Equals -Expected $standard_user_sid + } + + "Batch logon with standard" = { + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, "LOGON_WITH_PROFILE", + "LOGON32_LOGON_BATCH", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + $stdout.LogonType | Assert-Equals -Expected "Batch" + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $medium_integrity_sid + $stdout.ProfileLoaded | Assert-Equals -Expected $true + $stdout.SourceName | Assert-Equals -Expected "Advapi" + $stdout.UserSid.Value | Assert-Equals -Expected $standard_user_sid + } + + "Network logon with standard" = { + # Server 2008 will not work with become to Network or Network Credentials + if ([System.Environment]::OSVersion.Version -lt [Version]"6.1") { + continue + } + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, "LOGON_WITH_PROFILE", + "LOGON32_LOGON_NETWORK", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + $stdout.LogonType | Assert-Equals -Expected "Network" + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $medium_integrity_sid + $stdout.ProfileLoaded | Assert-Equals -Expected $true + $stdout.SourceName | Assert-Equals -Expected "Advapi" + $stdout.UserSid.Value | Assert-Equals -Expected $standard_user_sid + } + + "Network with cleartext logon with standard" = { + # Server 2008 will not work with become to Network or Network Cleartext + if ([System.Environment]::OSVersion.Version -lt [Version]"6.1") { + continue + } + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, "LOGON_WITH_PROFILE", + "LOGON32_LOGON_NETWORK_CLEARTEXT", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + $stdout.LogonType | Assert-Equals -Expected "NetworkCleartext" + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $medium_integrity_sid + $stdout.ProfileLoaded | Assert-Equals -Expected $true + $stdout.SourceName | Assert-Equals -Expected "Advapi" + $stdout.UserSid.Value | Assert-Equals -Expected $standard_user_sid + } + + "Logon without password with standard" = { + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, [NullString]::Value, "LOGON_WITH_PROFILE", + "LOGON32_LOGON_INTERACTIVE", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + # Too unstable, there might be another process still lingering which causes become to steal instead of using + # S4U. Just don't check the type and source to verify we can become without a password + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + # $stdout.LogonType | Assert-Equals -Expected "Batch" + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $medium_integrity_sid + $stdout.ProfileLoaded | Assert-Equals -Expected $true + # $stdout.SourceName | Assert-Equals -Expected "ansible" + $stdout.UserSid.Value | Assert-Equals -Expected $standard_user_sid + } + + "Logon without password and network type with standard" = { + # Server 2008 will not work with become to Network or Network Cleartext + if ([System.Environment]::OSVersion.Version -lt [Version]"6.1") { + continue + } + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, [NullString]::Value, "LOGON_WITH_PROFILE", + "LOGON32_LOGON_NETWORK", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + # Too unstable, there might be another process still lingering which causes become to steal instead of using + # S4U. Just don't check the type and source to verify we can become without a password + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + # $stdout.LogonType | Assert-Equals -Expected "Network" + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $medium_integrity_sid + $stdout.ProfileLoaded | Assert-Equals -Expected $true + # $stdout.SourceName | Assert-Equals -Expected "ansible" + $stdout.UserSid.Value | Assert-Equals -Expected $standard_user_sid + } + + "Interactive logon with admin" = { + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, "LOGON_WITH_PROFILE", + "LOGON32_LOGON_INTERACTIVE", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + $stdout.LogonType | Assert-Equals -Expected "Interactive" + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $high_integrity_sid + $stdout.ProfileLoaded | Assert-Equals -Expected $true + $stdout.SourceName | Assert-Equals -Expected "Advapi" + $stdout.UserSid.Value | Assert-Equals -Expected $admin_user_sid + } + + "Batch logon with admin" = { + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, "LOGON_WITH_PROFILE", + "LOGON32_LOGON_BATCH", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + $stdout.LogonType | Assert-Equals -Expected "Batch" + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $high_integrity_sid + $stdout.ProfileLoaded | Assert-Equals -Expected $true + $stdout.SourceName | Assert-Equals -Expected "Advapi" + $stdout.UserSid.Value | Assert-Equals -Expected $admin_user_sid + } + + "Network logon with admin" = { + # Server 2008 will not work with become to Network or Network Credentials + if ([System.Environment]::OSVersion.Version -lt [Version]"6.1") { + continue + } + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, "LOGON_WITH_PROFILE", + "LOGON32_LOGON_NETWORK", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + $stdout.LogonType | Assert-Equals -Expected "Network" + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $high_integrity_sid + $stdout.ProfileLoaded | Assert-Equals -Expected $true + $stdout.SourceName | Assert-Equals -Expected "Advapi" + $stdout.UserSid.Value | Assert-Equals -Expected $admin_user_sid + } + + "Network with cleartext logon with admin" = { + # Server 2008 will not work with become to Network or Network Credentials + if ([System.Environment]::OSVersion.Version -lt [Version]"6.1") { + continue + } + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, "LOGON_WITH_PROFILE", + "LOGON32_LOGON_NETWORK_CLEARTEXT", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + $stdout.LogonType | Assert-Equals -Expected "NetworkCleartext" + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $high_integrity_sid + $stdout.ProfileLoaded | Assert-Equals -Expected $true + $stdout.SourceName | Assert-Equals -Expected "Advapi" + $stdout.UserSid.Value | Assert-Equals -Expected $admin_user_sid + } + + "Fail to logon with null or empty password" = { + $failed = $false + try { + # Having $null or an empty string means we are trying to become a user with a blank password and not + # become without setting the password. This is confusing as $null gets converted to "" and we need to + # use [NullString]::Value instead if we want that behaviour. This just tests to see that an empty + # string won't go the S4U route. + [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $null, "LOGON_WITH_PROFILE", + "LOGON32_LOGON_INTERACTIVE", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n") + } catch { + $failed = $true + $_.Exception.InnerException.GetType().FullName | Assert-Equals -Expected "Ansible.Process.Win32Exception" + # Server 2008 has a slightly different error msg, just assert we get the error 1326 + ($_.Exception.Message.Contains("Win32ErrorCode 1326")) | Assert-Equals -Expected $true + } + $failed | Assert-Equals -Expected $true + } + + "Logon without password with admin" = { + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, [NullString]::Value, "LOGON_WITH_PROFILE", + "LOGON32_LOGON_INTERACTIVE", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + # Too unstable, there might be another process still lingering which causes become to steal instead of using + # S4U. Just don't check the type and source to verify we can become without a password + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + # $stdout.LogonType | Assert-Equals -Expected "Batch" + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $high_integrity_sid + $stdout.ProfileLoaded | Assert-Equals -Expected $true + # $stdout.SourceName | Assert-Equals -Expected "ansible" + $stdout.UserSid.Value | Assert-Equals -Expected $admin_user_sid + } + + "Logon without password and network type with admin" = { + # become network doesn't work on Server 2008 + if ([System.Environment]::OSVersion.Version -lt [Version]"6.1") { + continue + } + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, [NullString]::Value, "LOGON_WITH_PROFILE", + "LOGON32_LOGON_NETWORK", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + # Too unstable, there might be another process still lingering which causes become to steal instead of using + # S4U. Just don't check the type and source to verify we can become without a password + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + # $stdout.LogonType | Assert-Equals -Expected "Network" + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $high_integrity_sid + $stdout.ProfileLoaded | Assert-Equals -Expected $true + # $stdout.SourceName | Assert-Equals -Expected "ansible" + $stdout.UserSid.Value | Assert-Equals -Expected $admin_user_sid + } + + "Logon without profile with admin" = { + # Server 2008 and 2008 R2 does not support running without the profile being set + if ([System.Environment]::OSVersion.Version -lt [Version]"6.2") { + continue + } + + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, 0, + "LOGON32_LOGON_INTERACTIVE", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + $stdout.LogonType | Assert-Equals -Expected "Interactive" + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $high_integrity_sid + $stdout.ProfileLoaded | Assert-Equals -Expected $false + $stdout.SourceName | Assert-Equals -Expected "Advapi" + $stdout.UserSid.Value | Assert-Equals -Expected $admin_user_sid + } + + "Logon with network credentials and no profile" = { + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("fakeuser", "fakepassword", "LOGON_NETCREDENTIALS_ONLY", + "LOGON32_LOGON_NEW_CREDENTIALS", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + $stdout.LogonType | Assert-Equals -Expected "NewCredentials" + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $current_user.MandatoryLabelSid.Value + + # while we didn't set LOGON_WITH_PROFILE, the new process is based on the current process + $stdout.ProfileLoaded | Assert-Equals -Expected $current_user.ProfileLoaded + $stdout.SourceName | Assert-Equals -Expected "Advapi" + $stdout.UserSid.Value | Assert-Equals -Expected $current_user.UserSid.Value + } + + "Logon with network credentials and with profile" = { + $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("fakeuser", "fakepassword", "LOGON_NETCREDENTIALS_ONLY, LOGON_WITH_PROFILE", + "LOGON32_LOGON_NEW_CREDENTIALS", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n") + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $stdout = ConvertFrom-Json -InputObject $actual.StandardOut + $stdout.LogonType | Assert-Equals -Expected "NewCredentials" + $stdout.MandatoryLabelSid.Value | Assert-Equals -Expected $current_user.MandatoryLabelSid.Value + $stdout.ProfileLoaded | Assert-Equals -Expected $current_user.ProfileLoaded + $stdout.SourceName | Assert-Equals -Expected "Advapi" + $stdout.UserSid.Value | Assert-Equals -Expected $current_user.UserSid.Value + } +} + +try { + $tmp_dir = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ([System.IO.Path]::GetRandomFileName()) + New-Item -Path $tmp_dir -ItemType Directory > $null + $acl = Get-Acl -Path $tmp_dir + $ace = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList @( + New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList ([System.Security.Principal.WellKnownSidType]::WorldSid, $null) + [System.Security.AccessControl.FileSystemRights]::FullControl, + [System.Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit", + [System.Security.AccessControl.PropagationFlags]::None, + [System.Security.AccessControl.AccessControlType]::Allow + ) + $acl.AddAccessRule($ace) + Set-Acl -Path $tmp_dir -AclObject $acl + + $tmp_script = Join-Path -Path $tmp_dir -ChildPath "whoami.ps1" + Set-Content -Path $tmp_script -Value $test_whoami + + foreach ($user in $standard_user, $admin_user) { + $user_obj = $adsi.Children | Where-Object { $_.SchemaClassName -eq "User" -and $_.Name -eq $user } + if ($null -eq $user_obj) { + $user_obj = $adsi.Create("User", $user) + $user_obj.SetPassword($become_pass) + $user_obj.SetInfo() + } else { + $user_obj.SetPassword($become_pass) + } + $user_obj.RefreshCache() + + if ($user -eq $standard_user) { + $standard_user_sid = (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @($user_obj.ObjectSid.Value, 0)).Value + $group = [System.Security.Principal.WellKnownSidType]::BuiltinUsersSid + } else { + $admin_user_sid = (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @($user_obj.ObjectSid.Value, 0)).Value + $group = [System.Security.Principal.WellKnownSidType]::BuiltinAdministratorsSid + } + $group = (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $group, $null).Value + [string[]]$current_groups = $user_obj.Groups() | ForEach-Object { + New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @($_.GetType().InvokeMember("objectSID", "GetProperty", $null, $_, $null), 0) + } + if ($current_groups -notcontains $group) { + $group_obj = $adsi.Children | Where-Object { + if ($_.SchemaClassName -eq "Group") { + $group_sid = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @($_.objectSID.Value, 0) + $group_sid -eq $group + } + } + $group_obj.Add($user_obj.Path) + } + + + } + foreach ($test_impl in $tests.GetEnumerator()) { + $test = $test_impl.Key + &$test_impl.Value + } +} finally { + Remove-Item -Path $tmp_dir -Force -Recurse + foreach ($user in $standard_user, $admin_user) { + $user_obj = $adsi.Children | Where-Object { $_.SchemaClassName -eq "User" -and $_.Name -eq $user } + $adsi.Delete("User", $user_obj.Name.Value) + } +} + + +$module.Result.data = "success" +$module.ExitJson() + diff --git a/test/integration/targets/win_csharp_utils/library/ansible_process_tests.ps1 b/test/integration/targets/win_csharp_utils/library/ansible_process_tests.ps1 new file mode 100644 index 00000000000..f01c672e096 --- /dev/null +++ b/test/integration/targets/win_csharp_utils/library/ansible_process_tests.ps1 @@ -0,0 +1,229 @@ +#!powershell + +#AnsibleRequires -CSharpUtil Ansible.Basic +#AnsibleRequires -CSharpUtil Ansible.Process + +$module = [Ansible.Basic.AnsibleModule]::Create($args, @{}) + +Function Assert-Equals { + param( + [Parameter(Mandatory=$true, ValueFromPipeline=$true)][AllowNull()]$Actual, + [Parameter(Mandatory=$true, Position=0)][AllowNull()]$Expected + ) + + $matched = $false + if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array]) { + $Actual.Count | Assert-Equals -Expected $Expected.Count + for ($i = 0; $i -lt $Actual.Count; $i++) { + $actual_value = $Actual[$i] + $expected_value = $Expected[$i] + Assert-Equals -Actual $actual_value -Expected $expected_value + } + $matched = $true + } else { + $matched = $Actual -ceq $Expected + } + + if (-not $matched) { + if ($Actual -is [PSObject]) { + $Actual = $Actual.ToString() + } + + $call_stack = (Get-PSCallStack)[1] + $module.Result.test = $test + $module.Result.actual = $Actual + $module.Result.expected = $Expected + $module.Result.line = $call_stack.ScriptLineNumber + $module.Result.method = $call_stack.Position.Text + $module.FailJson("AssertionError: actual != expected") + } +} + +$tests = @{ + "ParseCommandLine empty string" = { + $expected = @((Get-Process -Id $pid).Path) + $actual = [Ansible.Process.ProcessUtil]::ParseCommandLine("") + Assert-Equals -Actual $actual -Expected $expected + } + + "ParseCommandLine single argument" = { + $expected = @("powershell.exe") + $actual = [Ansible.Process.ProcessUtil]::ParseCommandLine("powershell.exe") + Assert-Equals -Actual $actual -Expected $expected + } + + "ParseCommandLine multiple arguments" = { + $expected = @("powershell.exe", "-File", "C:\temp\script.ps1") + $actual = [Ansible.Process.ProcessUtil]::ParseCommandLine("powershell.exe -File C:\temp\script.ps1") + Assert-Equals -Actual $actual -Expected $expected + } + + "ParseCommandLine comples arguments" = { + $expected = @('abc', 'd', 'ef gh', 'i\j', 'k"l', 'm\n op', 'ADDLOCAL=qr, s', 'tuv\', 'w''x', 'yz') + $actual = [Ansible.Process.ProcessUtil]::ParseCommandLine('abc d "ef gh" i\j k\"l m\\"n op" ADDLOCAL="qr, s" tuv\ w''x yz') + Assert-Equals -Actual $actual -Expected $expected + } + + "SearchPath normal" = { + $expected = "$($env:SystemRoot)\System32\WindowsPowerShell\v1.0\powershell.exe" + $actual = [Ansible.Process.ProcessUtil]::SearchPath("powershell.exe") + $actual | Assert-Equals -Expected $expected + } + + "SearchPath missing" = { + $failed = $false + try { + [Ansible.Process.ProcessUtil]::SearchPath("fake.exe") + } catch { + $failed = $true + $_.Exception.InnerException.GetType().FullName | Assert-Equals -Expected "System.IO.FileNotFoundException" + $expected = 'Exception calling "SearchPath" with "1" argument(s): "Could not find file ''fake.exe''."' + $_.Exception.Message | Assert-Equals -Expected $expected + } + $failed | Assert-Equals -Expected $true + } + + "CreateProcess basic" = { + $actual = [Ansible.Process.ProcessUtil]::CreateProcess("whoami.exe") + $actual.GetType().FullName | Assert-Equals -Expected "Ansible.Process.Result" + $actual.StandardOut | Assert-Equals -Expected "$(&whoami.exe)`r`n" + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "CreateProcess stderr" = { + $actual = [Ansible.Process.ProcessUtil]::CreateProcess("powershell.exe [System.Console]::Error.WriteLine('hi')") + $actual.StandardOut | Assert-Equals -Expected "" + $actual.StandardError | Assert-Equals -Expected "hi`r`n" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "CreateProcess exit code" = { + $actual = [Ansible.Process.ProcessUtil]::CreateProcess("powershell.exe exit 10") + $actual.StandardOut | Assert-Equals -Expected "" + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 10 + } + + "CreateProcess bad executable" = { + $failed = $false + try { + [Ansible.Process.ProcessUtil]::CreateProcess("fake.exe") + } catch { + $failed = $true + $_.Exception.InnerException.GetType().FullName | Assert-Equals -Expected "Ansible.Process.Win32Exception" + $expected = 'Exception calling "CreateProcess" with "1" argument(s): "CreateProcessW() failed ' + $expected += '(The system cannot find the file specified, Win32ErrorCode 2)"' + $_.Exception.Message | Assert-Equals -Expected $expected + } + $failed | Assert-Equals -Expected $true + } + + "CreateProcess with unicode" = { + $actual = [Ansible.Process.ProcessUtil]::CreateProcess("cmd.exe /c echo 💩 café") + $actual.StandardOut | Assert-Equals -Expected "💩 café`r`n" + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, "cmd.exe /c echo 💩 café", $null, $null) + $actual.StandardOut | Assert-Equals -Expected "💩 café`r`n" + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "CreateProcess without working dir" = { + $expected = $pwd.Path + "`r`n" + $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe $pwd.Path', $null, $null) + $actual.StandardOut | Assert-Equals -Expected $expected + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "CreateProcess with working dir" = { + $expected = "C:\Windows`r`n" + $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe $pwd.Path', "C:\Windows", $null) + $actual.StandardOut | Assert-Equals -Expected $expected + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "CreateProcess without environment" = { + $expected = "$($env:USERNAME)`r`n" + $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe $env:TEST; $env:USERNAME', $null, $null) + $actual.StandardOut | Assert-Equals -Expected $expected + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "CreateProcess with environment" = { + $env_vars = @{ + TEST = "tesTing" + TEST2 = "Testing 2" + } + $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'cmd.exe /c set', $null, $env_vars) + ("TEST=tesTing" -cin $actual.StandardOut.Split("`r`n")) | Assert-Equals -Expected $true + ("TEST2=Testing 2" -cin $actual.StandardOut.Split("`r`n")) | Assert-Equals -Expected $true + ("USERNAME=$($env:USERNAME)" -cnotin $actual.StandardOut.Split("`r`n")) | Assert-Equals -Expected $true + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "CreateProcess with string stdin" = { + $expected = "input value`r`n`r`n" + $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()', + $null, $null, "input value") + $actual.StandardOut | Assert-Equals -Expected $expected + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "CreateProcess with string stdin and newline" = { + $expected = "input value`r`n`r`n" + $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()', + $null, $null, "input value`r`n") + $actual.StandardOut | Assert-Equals -Expected $expected + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "CreateProcess with byte stdin" = { + $expected = "input value`r`n" + $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()', + $null, $null, [System.Text.Encoding]::UTF8.GetBytes("input value")) + $actual.StandardOut | Assert-Equals -Expected $expected + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "CreateProcess with byte stdin and newline" = { + $expected = "input value`r`n`r`n" + $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()', + $null, $null, [System.Text.Encoding]::UTF8.GetBytes("input value`r`n")) + $actual.StandardOut | Assert-Equals -Expected $expected + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + } + + "CreateProcess with lpApplicationName" = { + $expected = "abc`r`n" + $full_path = "$($env:SystemRoot)\System32\WindowsPowerShell\v1.0\powershell.exe" + $actual = [Ansible.Process.ProcessUtil]::CreateProcess($full_path, "Write-Output 'abc'", $null, $null) + $actual.StandardOut | Assert-Equals -Expected $expected + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + + $actual = [Ansible.Process.ProcessUtil]::CreateProcess($full_path, "powershell.exe Write-Output 'abc'", $null, $null) + $actual.StandardOut | Assert-Equals -Expected $expected + $actual.StandardError | Assert-Equals -Expected "" + $actual.ExitCode | Assert-Equals -Expected 0 + } +} + +foreach ($test_impl in $tests.GetEnumerator()) { + $test = $test_impl.Key + &$test_impl.Value +} + +$module.Result.data = "success" +$module.ExitJson() + diff --git a/test/integration/targets/win_csharp_utils/tasks/main.yml b/test/integration/targets/win_csharp_utils/tasks/main.yml index 010c2d5076c..2e5506dfc07 100644 --- a/test/integration/targets/win_csharp_utils/tasks/main.yml +++ b/test/integration/targets/win_csharp_utils/tasks/main.yml @@ -7,3 +7,40 @@ assert: that: - ansible_basic_test.data == "success" + +# Users by default don't have this right, temporarily enable it +- name: ensure the Users group have the SeBatchLogonRight + win_user_right: + name: SeBatchLogonRight + users: + - Users + action: add + register: batch_user_add + +- block: + - name: test Ansible.Become.cs + ansible_become_tests: + register: ansible_become_tests + + always: + - name: remove SeBatchLogonRight from users if added in test + win_user_right: + name: SeBatchLogonRight + users: + - Users + action: remove + when: batch_user_add is changed + +- name: assert test Ansible.Become.cs + assert: + that: + - ansible_become_tests.data == "success" + +- name: test Ansible.Process.cs + ansible_process_tests: + register: ansible_process_tests + +- name: assert test Ansible.Process.cs + assert: + that: + - ansible_process_tests.data == "success" diff --git a/test/integration/targets/win_module_utils/library/command_util_test.ps1 b/test/integration/targets/win_module_utils/library/command_util_test.ps1 index 3515112e38e..3d7405ddaef 100644 --- a/test/integration/targets/win_module_utils/library/command_util_test.ps1 +++ b/test/integration/targets/win_module_utils/library/command_util_test.ps1 @@ -34,7 +34,7 @@ try { $actual = Run-Command -command "C:\fakepath\$exe_filename arg1" Fail-Json -obj $result -message "Test $test_name failed`nCommand should have thrown an exception" } catch { - Assert-Equals -actual $_.Exception.Message -expected "Exception calling `"SearchPath`" with `"1`" argument(s): `"Could not locate the following executable C:\fakepath\$exe_filename`"" + Assert-Equals -actual $_.Exception.Message -expected "Exception calling `"SearchPath`" with `"1`" argument(s): `"Could not find file 'C:\fakepath\$exe_filename'.`"" } $test_name = "exe in current folder" diff --git a/test/sanity/pslint/ignore.txt b/test/sanity/pslint/ignore.txt index 580ee91f269..f64bf8f4475 100644 --- a/test/sanity/pslint/ignore.txt +++ b/test/sanity/pslint/ignore.txt @@ -2,6 +2,7 @@ examples/scripts/ConfigureRemotingForAnsible.ps1 PSAvoidUsingCmdletAliases examples/scripts/upgrade_to_ps3.ps1 PSAvoidUsingWriteHost examples/scripts/upgrade_to_ps3.ps1 PSUseApprovedVerbs lib/ansible/module_utils/powershell/Ansible.ModuleUtils.ArgvParser.psm1 PSUseApprovedVerbs +lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 PSProvideCommentHelp # need to agree on best format for comment location lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 PSUseApprovedVerbs lib/ansible/module_utils/powershell/Ansible.ModuleUtils.FileUtil.psm1 PSProvideCommentHelp lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Legacy.psm1 PSAvoidUsingWMICmdlet