From 4d3ebd65db512b445e1de4a62dfa9fb1fa3f6156 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Thu, 5 Dec 2019 11:24:30 +1000 Subject: [PATCH] win_auto_logon - check, diff and store pass in LSA (#65528) * win_auto_logon - check, diff and store pass in LSA * Ensure baseline keys are set for test * Skip remove item prop on check mode due to win bug * Start at a cleared baseline to ensure old LSA secrets are cleared --- .../modules/windows/win_auto_logon.ps1 | 390 ++++++++++++++++-- lib/ansible/modules/windows/win_auto_logon.py | 16 + .../targets/win_auto_logon/defaults/main.yml | 3 + .../library/test_autologon_info.ps1 | 214 ++++++++++ .../targets/win_auto_logon/tasks/main.yml | 66 +-- .../targets/win_auto_logon/tasks/tests.yml | 178 ++++++++ 6 files changed, 802 insertions(+), 65 deletions(-) create mode 100644 test/integration/targets/win_auto_logon/defaults/main.yml create mode 100644 test/integration/targets/win_auto_logon/library/test_autologon_info.ps1 create mode 100644 test/integration/targets/win_auto_logon/tasks/tests.yml diff --git a/lib/ansible/modules/windows/win_auto_logon.ps1 b/lib/ansible/modules/windows/win_auto_logon.ps1 index 25d3bdfbfbe..61b0d67f5a7 100644 --- a/lib/ansible/modules/windows/win_auto_logon.ps1 +++ b/lib/ansible/modules/windows/win_auto_logon.ps1 @@ -3,27 +3,31 @@ # Copyright: (c) 2019, Prasoon Karunan V (@prasoonkarunan) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - # All helper methods are written in a binary module and has to be loaded for consuming them. #AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module Ansible.ModuleUtils.AddType Set-StrictMode -Version 2.0 $spec = @{ options = @{ + logon_count = @{type = "int"} password = @{type = "str"; no_log = $true} - state = @{type = "str"; choices = "absent","present"; default = "present"} + state = @{type = "str"; choices = "absent", "present"; default = "present"} username = @{type = "str"} } required_if = @( - , @("state", "present", @("username", "password")) + ,@("state", "present", @("username", "password")) ) + supports_check_mode = $true } $module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) -$password = $module.params.password -$state = $module.params.state -$username = $module.params.username + +$logonCount = $module.Params.logon_count +$password = $module.Params.password +$state = $module.Params.state +$username = $module.Params.username $domain = $null if ($username) { @@ -40,44 +44,360 @@ if ($username) { $domain, $username = $ntAccount.Value -split '\\' } -#Build ParamHash - -$autoAdminLogon = 1 -if($state -eq 'absent'){ - $autoadminlogon = 0 +# Make sure $null regardless of any input value if state: absent +if ($state -eq 'absent') { + $password = $null } -$autoLogonKeyList = @{ - DefaultPassword = $password - DefaultUserName = $username - DefaultDomain = $domain - AutoAdminLogon = $autoAdminLogon -} -$actionTaken = $null -$autoLogonRegPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\' -$autoLogonKeyRegList = Get-ItemProperty -LiteralPath $autoLogonRegPath -Name $autoLogonKeyList.GetEnumerator().Name -ErrorAction SilentlyContinue -Foreach($key in $autoLogonKeyList.GetEnumerator().Name){ - $currentKeyValue = $autoLogonKeyRegList | Select-Object -ExpandProperty $key -ErrorAction SilentlyContinue - if (-not [String]::IsNullOrEmpty($currentKeyValue)) { - $expectedValue = $autoLogonKeyList[$key] - if(($state -eq 'present') -and ($currentKeyValue -ne $expectedValue)) { - Set-ItemProperty -LiteralPath $autoLogonRegPath -Name $key -Value $autoLogonKeyList[$key] -Force - $actionTaken = $true +Add-CSharpType -AnsibleModule $module -References @' +using Microsoft.Win32.SafeHandles; +using System; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ansible.WinAutoLogon +{ + internal class NativeHelpers + { + [StructLayout(LayoutKind.Sequential)] + public class LSA_OBJECT_ATTRIBUTES + { + public UInt32 Length = 0; + public IntPtr RootDirectory = IntPtr.Zero; + public IntPtr ObjectName = IntPtr.Zero; + public UInt32 Attributes = 0; + public IntPtr SecurityDescriptor = IntPtr.Zero; + public IntPtr SecurityQualityOfService = IntPtr.Zero; } - elseif($state -eq 'absent') { - $actionTaken = $true - Remove-ItemProperty -LiteralPath $autoLogonRegPath -Name $key -Force + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct LSA_UNICODE_STRING + { + public UInt16 Length; + public UInt16 MaximumLength; + public IntPtr Buffer; + + public static explicit operator string(LSA_UNICODE_STRING s) + { + byte[] strBytes = new byte[s.Length]; + Marshal.Copy(s.Buffer, strBytes, 0, s.Length); + return Encoding.Unicode.GetString(strBytes); + } + + public static SafeMemoryBuffer CreateSafeBuffer(string s) + { + if (s == null) + return new SafeMemoryBuffer(IntPtr.Zero); + + byte[] stringBytes = Encoding.Unicode.GetBytes(s); + int structSize = Marshal.SizeOf(typeof(LSA_UNICODE_STRING)); + IntPtr buffer = Marshal.AllocHGlobal(structSize + stringBytes.Length); + try + { + LSA_UNICODE_STRING lsaString = new LSA_UNICODE_STRING() + { + Length = (UInt16)(stringBytes.Length), + MaximumLength = (UInt16)(stringBytes.Length), + Buffer = IntPtr.Add(buffer, structSize), + }; + Marshal.StructureToPtr(lsaString, buffer, false); + Marshal.Copy(stringBytes, 0, lsaString.Buffer, stringBytes.Length); + return new SafeMemoryBuffer(buffer); + } + catch + { + // Make sure we free the pointer before raising the exception. + Marshal.FreeHGlobal(buffer); + throw; + } + } } } - else { - if ($state -eq 'present') { - $actionTaken = $true - New-ItemProperty -LiteralPath $autoLogonRegPath -Name $key -Value $autoLogonKeyList[$key] -Force | Out-Null + + internal class NativeMethods + { + [DllImport("Advapi32.dll")] + public static extern UInt32 LsaClose( + IntPtr ObjectHandle); + + [DllImport("Advapi32.dll")] + public static extern UInt32 LsaFreeMemory( + IntPtr Buffer); + + [DllImport("Advapi32.dll")] + internal static extern Int32 LsaNtStatusToWinError( + UInt32 Status); + + [DllImport("Advapi32.dll")] + public static extern UInt32 LsaOpenPolicy( + IntPtr SystemName, + NativeHelpers.LSA_OBJECT_ATTRIBUTES ObjectAttributes, + LsaPolicyAccessMask AccessMask, + out SafeLsaHandle PolicyHandle); + + [DllImport("Advapi32.dll")] + public static extern UInt32 LsaRetrievePrivateData( + SafeLsaHandle PolicyHandle, + SafeMemoryBuffer KeyName, + out SafeLsaMemory PrivateData); + + [DllImport("Advapi32.dll")] + public static extern UInt32 LsaStorePrivateData( + SafeLsaHandle PolicyHandle, + SafeMemoryBuffer KeyName, + SafeMemoryBuffer PrivateData); + } + + internal class SafeLsaMemory : SafeBuffer + { + internal SafeLsaMemory() : base(true) { } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + + protected override bool ReleaseHandle() + { + return NativeMethods.LsaFreeMemory(handle) == 0; + } + } + + internal class SafeMemoryBuffer : SafeBuffer + { + internal SafeMemoryBuffer() : base(true) { } + + internal SafeMemoryBuffer(IntPtr ptr) : base(true) + { + base.SetHandle(ptr); + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + + protected override bool ReleaseHandle() + { + if (handle != IntPtr.Zero) + Marshal.FreeHGlobal(handle); + return true; + } + } + + public class SafeLsaHandle : SafeHandleZeroOrMinusOneIsInvalid + { + internal SafeLsaHandle() : base(true) { } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + + protected override bool ReleaseHandle() + { + return NativeMethods.LsaClose(handle) == 0; + } + } + + public class Win32Exception : System.ComponentModel.Win32Exception + { + private string _exception_msg; + public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { } + public Win32Exception(int errorCode, string message) : base(errorCode) + { + _exception_msg = String.Format("{0} - {1} (Win32 Error Code {2}: 0x{3})", message, base.Message, errorCode, errorCode.ToString("X8")); + } + public override string Message { get { return _exception_msg; } } + public static explicit operator Win32Exception(string message) { return new Win32Exception(message); } + } + + [Flags] + public enum LsaPolicyAccessMask : uint + { + ViewLocalInformation = 0x00000001, + ViewAuditInformation = 0x00000002, + GetPrivateInformation = 0x00000004, + TrustAdmin = 0x00000008, + CreateAccount = 0x00000010, + CreateSecret = 0x00000020, + CreatePrivilege = 0x00000040, + SetDefaultQuotaLimits = 0x00000080, + SetAuditRequirements = 0x00000100, + AuditLogAdmin = 0x00000200, + ServerAdmin = 0x00000400, + LookupNames = 0x00000800, + Read = 0x00020006, + Write = 0x000207F8, + Execute = 0x00020801, + AllAccess = 0x000F0FFF, + } + + public class LsaUtil + { + public static SafeLsaHandle OpenPolicy(LsaPolicyAccessMask access) + { + NativeHelpers.LSA_OBJECT_ATTRIBUTES oa = new NativeHelpers.LSA_OBJECT_ATTRIBUTES(); + SafeLsaHandle lsaHandle; + UInt32 res = NativeMethods.LsaOpenPolicy(IntPtr.Zero, oa, access, out lsaHandle); + if (res != 0) + throw new Win32Exception(NativeMethods.LsaNtStatusToWinError(res), + String.Format("LsaOpenPolicy({0}) failed", access.ToString())); + return lsaHandle; + } + + public static string RetrievePrivateData(SafeLsaHandle handle, string key) + { + using (SafeMemoryBuffer keyBuffer = NativeHelpers.LSA_UNICODE_STRING.CreateSafeBuffer(key)) + { + SafeLsaMemory buffer; + UInt32 res = NativeMethods.LsaRetrievePrivateData(handle, keyBuffer, out buffer); + using (buffer) + { + if (res != 0) + { + // If the data object was not found we return null to indicate it isn't set. + if (res == 0xC0000034) // STATUS_OBJECT_NAME_NOT_FOUND + return null; + + throw new Win32Exception(NativeMethods.LsaNtStatusToWinError(res), + String.Format("LsaRetrievePrivateData({0}) failed", key)); + } + + NativeHelpers.LSA_UNICODE_STRING lsaString = (NativeHelpers.LSA_UNICODE_STRING) + Marshal.PtrToStructure(buffer.DangerousGetHandle(), + typeof(NativeHelpers.LSA_UNICODE_STRING)); + return (string)lsaString; + } + } + } + + public static void StorePrivateData(SafeLsaHandle handle, string key, string data) + { + using (SafeMemoryBuffer keyBuffer = NativeHelpers.LSA_UNICODE_STRING.CreateSafeBuffer(key)) + using (SafeMemoryBuffer dataBuffer = NativeHelpers.LSA_UNICODE_STRING.CreateSafeBuffer(data)) + { + UInt32 res = NativeMethods.LsaStorePrivateData(handle, keyBuffer, dataBuffer); + if (res != 0) + { + // When clearing the private data with null it may return this error which we can ignore. + if (data == null && res == 0xC0000034) // STATUS_OBJECT_NAME_NOT_FOUND + return; + + throw new Win32Exception(NativeMethods.LsaNtStatusToWinError(res), + String.Format("LsaStorePrivateData({0}) failed", key)); + } + } } } } -if($actionTaken){ +'@ + +$autoLogonRegPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' +$logonDetails = Get-ItemProperty -LiteralPath $autoLogonRegPath + +$before = @{ + state = 'absent' +} +if ('AutoAdminLogon' -in $logonDetails.PSObject.Properties.Name -and $logonDetails.AutoAdminLogon -eq 1) { + $before.state = 'present' +} + +$mapping = @{ + DefaultUserName = 'username' + DefaultDomainName = 'domain' + AutoLogonCount = 'logon_count' +} +foreach ($map_detail in $mapping.GetEnumerator()) { + if ($map_detail.Key -in $logonDetails.PSObject.Properties.Name) { + $before."$($map_detail.Value)" = $logonDetails."$($map_detail.Key)" + } +} + +$module.Diff.before = $before + +$propParams = @{ + LiteralPath = $autoLogonRegPath + WhatIf = $module.CheckMode + Force = $true +} + +# First set the registry information +# The DefaultPassword reg key should never be set, we use LSA to store the password in a more secure way. +if ('DefaultPassword' -in (Get-Item -LiteralPath $autoLogonRegPath).Property) { + # Bug on older Windows hosts where -WhatIf causes it fail to find the property + if (-not $module.CheckMode) { + Remove-ItemProperty -Name 'DefaultPassword' @propParams + } $module.Result.changed = $true } +$autoLogonKeyList = @{ + DefaultUserName = @{ + before = if ($before.ContainsKey('username')) { $before.username } else { $null } + after = $username + } + DefaultDomainName = @{ + before = if ($before.ContainsKey('domain')) { $before.domain } else { $null } + after = $domain + } + AutoLogonCount = @{ + before = if ($before.ContainsKey('logon_count')) { $before.logon_count } else { $null } + after = $logonCount + } +} + +# Check AutoAdminLogon separately as it has different logic (key must exist) +if ($state -ne $before.state) { + $newValue = if ($state -eq 'present') { 1 } else { 0 } + $null = New-ItemProperty -Name 'AutoAdminLogon' -Value $newValue -PropertyType DWord @propParams + $module.Result.changed = $true +} + +foreach ($key in $autoLogonKeyList.GetEnumerator()) { + $beforeVal = $key.Value.before + $after = $key.Value.after + + if ($state -eq 'present' -and $beforeVal -cne $after) { + if ($null -ne $after) { + $null = New-ItemProperty -Name $key.Key -Value $after @propParams + } + elseif (-not $module.CheckMode) { + Remove-ItemProperty -Name $key.Key @propParams + } + $module.Result.changed = $true + } + elseif ($state -eq 'absent' -and $null -ne $beforeVal) { + if (-not $module.CheckMode) { + Remove-ItemProperty -Name $key.Key @propParams + } + $module.Result.changed = $true + } +} + +# Finally update the password in the LSA private store. +$lsaHandle = [Ansible.WinAutoLogon.LsaUtil]::OpenPolicy('CreateSecret, GetPrivateInformation') +try { + $beforePass = [Ansible.WinAutoLogon.LsaUtil]::RetrievePrivateData($lsaHandle, 'DefaultPassword') + + if ($beforePass -cne $password) { + # Due to .NET marshaling we need to pass in $null as NullString.Value so it's truly a null value. + if ($null -eq $password) { + $password = [NullString]::Value + } + if (-not $module.CheckMode) { + [Ansible.WinAutoLogon.LsaUtil]::StorePrivateData($lsaHandle, 'DefaultPassword', $password) + } + $module.Result.changed = $true + } +} +finally { + $lsaHandle.Dispose() +} + +# Need to manually craft the after diff in case we are running in check mode +$module.Diff.after = @{ + state = $state +} +if ($state -eq 'present') { + $module.Diff.after.username = $username + $module.Diff.after.domain = $domain + if ($null -ne $logonCount) { + $module.Diff.after.logon_count = $logonCount + } +} + $module.ExitJson() + diff --git a/lib/ansible/modules/windows/win_auto_logon.py b/lib/ansible/modules/windows/win_auto_logon.py index fe31b2961f0..c869361df92 100644 --- a/lib/ansible/modules/windows/win_auto_logon.py +++ b/lib/ansible/modules/windows/win_auto_logon.py @@ -16,6 +16,14 @@ description: - Used to apply auto logon registry setting. version_added: "2.10" options: + logon_count: + description: + - The number of times to do an automatic logon. + - This count is deremented by Windows everytime an automatic logon is + performed. + - Once the count reaches C(0) then the automatic logon process is + disabled. + type: int username: description: - Username to login automatically. @@ -29,6 +37,8 @@ options: - Password to be used for automatic login. - Must be set when C(state=present). - Value of this input will be used as password for I(username). + - While this value is encrypted by LSA it is decryptable to any user who + is an Administrator on the remote host. type: str state: description: @@ -54,6 +64,12 @@ EXAMPLES = r''' - name: Remove autologon for user1 win_auto_logon: state: absent + +- name: Set autologon for user1 with a limited logon count + win_auto_logon: + username: User1 + password: str0ngp@ssword + logon_count: 5 ''' RETURN = r''' diff --git a/test/integration/targets/win_auto_logon/defaults/main.yml b/test/integration/targets/win_auto_logon/defaults/main.yml new file mode 100644 index 00000000000..d5462bb6a35 --- /dev/null +++ b/test/integration/targets/win_auto_logon/defaults/main.yml @@ -0,0 +1,3 @@ +# This doesn't have to be valid, just testing weird chars in the pass +test_logon_password: 'café - 💩' +test_logon_password2: '.ÅÑŚÌβŁÈ [$!@^&test(;)]' diff --git a/test/integration/targets/win_auto_logon/library/test_autologon_info.ps1 b/test/integration/targets/win_auto_logon/library/test_autologon_info.ps1 new file mode 100644 index 00000000000..ef4cf2d0f3e --- /dev/null +++ b/test/integration/targets/win_auto_logon/library/test_autologon_info.ps1 @@ -0,0 +1,214 @@ +#!powershell + +#AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module Ansible.ModuleUtils.AddType + +$module = [Ansible.Basic.AnsibleModule]::Create($args, @{}) + +Add-CSharpType -AnsibleModule $module -References @' +using Microsoft.Win32.SafeHandles; +using System; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ansible.TestAutoLogonInfo +{ + internal class NativeHelpers + { + [StructLayout(LayoutKind.Sequential)] + public class LSA_OBJECT_ATTRIBUTES + { + public UInt32 Length = 0; + public IntPtr RootDirectory = IntPtr.Zero; + public IntPtr ObjectName = IntPtr.Zero; + public UInt32 Attributes = 0; + public IntPtr SecurityDescriptor = IntPtr.Zero; + public IntPtr SecurityQualityOfService = IntPtr.Zero; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct LSA_UNICODE_STRING + { + public UInt16 Length; + public UInt16 MaximumLength; + public IntPtr Buffer; + + public static explicit operator string(LSA_UNICODE_STRING s) + { + byte[] strBytes = new byte[s.Length]; + Marshal.Copy(s.Buffer, strBytes, 0, s.Length); + return Encoding.Unicode.GetString(strBytes); + } + + public static SafeMemoryBuffer CreateSafeBuffer(string s) + { + if (s == null) + return new SafeMemoryBuffer(IntPtr.Zero); + + byte[] stringBytes = Encoding.Unicode.GetBytes(s); + int structSize = Marshal.SizeOf(typeof(LSA_UNICODE_STRING)); + IntPtr buffer = Marshal.AllocHGlobal(structSize + stringBytes.Length); + try + { + LSA_UNICODE_STRING lsaString = new LSA_UNICODE_STRING() + { + Length = (UInt16)(stringBytes.Length), + MaximumLength = (UInt16)(stringBytes.Length), + Buffer = IntPtr.Add(buffer, structSize), + }; + Marshal.StructureToPtr(lsaString, buffer, false); + Marshal.Copy(stringBytes, 0, lsaString.Buffer, stringBytes.Length); + return new SafeMemoryBuffer(buffer); + } + catch + { + // Make sure we free the pointer before raising the exception. + Marshal.FreeHGlobal(buffer); + throw; + } + } + } + } + + internal class NativeMethods + { + [DllImport("Advapi32.dll")] + public static extern UInt32 LsaClose( + IntPtr ObjectHandle); + + [DllImport("Advapi32.dll")] + public static extern UInt32 LsaFreeMemory( + IntPtr Buffer); + + [DllImport("Advapi32.dll")] + internal static extern Int32 LsaNtStatusToWinError( + UInt32 Status); + + [DllImport("Advapi32.dll")] + public static extern UInt32 LsaOpenPolicy( + IntPtr SystemName, + NativeHelpers.LSA_OBJECT_ATTRIBUTES ObjectAttributes, + UInt32 AccessMask, + out SafeLsaHandle PolicyHandle); + + [DllImport("Advapi32.dll")] + public static extern UInt32 LsaRetrievePrivateData( + SafeLsaHandle PolicyHandle, + SafeMemoryBuffer KeyName, + out SafeLsaMemory PrivateData); + } + + internal class SafeLsaMemory : SafeBuffer + { + internal SafeLsaMemory() : base(true) { } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + + protected override bool ReleaseHandle() + { + return NativeMethods.LsaFreeMemory(handle) == 0; + } + } + + internal class SafeMemoryBuffer : SafeBuffer + { + internal SafeMemoryBuffer() : base(true) { } + + internal SafeMemoryBuffer(IntPtr ptr) : base(true) + { + base.SetHandle(ptr); + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + + protected override bool ReleaseHandle() + { + if (handle != IntPtr.Zero) + Marshal.FreeHGlobal(handle); + return true; + } + } + + public class SafeLsaHandle : SafeHandleZeroOrMinusOneIsInvalid + { + internal SafeLsaHandle() : base(true) { } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + + protected override bool ReleaseHandle() + { + return NativeMethods.LsaClose(handle) == 0; + } + } + + public class Win32Exception : System.ComponentModel.Win32Exception + { + private string _exception_msg; + public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { } + public Win32Exception(int errorCode, string message) : base(errorCode) + { + _exception_msg = String.Format("{0} - {1} (Win32 Error Code {2}: 0x{3})", message, base.Message, errorCode, errorCode.ToString("X8")); + } + public override string Message { get { return _exception_msg; } } + public static explicit operator Win32Exception(string message) { return new Win32Exception(message); } + } + + public class LsaUtil + { + public static SafeLsaHandle OpenPolicy(UInt32 access) + { + NativeHelpers.LSA_OBJECT_ATTRIBUTES oa = new NativeHelpers.LSA_OBJECT_ATTRIBUTES(); + SafeLsaHandle lsaHandle; + UInt32 res = NativeMethods.LsaOpenPolicy(IntPtr.Zero, oa, access, out lsaHandle); + if (res != 0) + throw new Win32Exception(NativeMethods.LsaNtStatusToWinError(res), + String.Format("LsaOpenPolicy({0}) failed", access.ToString())); + return lsaHandle; + } + + public static string RetrievePrivateData(SafeLsaHandle handle, string key) + { + using (SafeMemoryBuffer keyBuffer = NativeHelpers.LSA_UNICODE_STRING.CreateSafeBuffer(key)) + { + SafeLsaMemory buffer; + UInt32 res = NativeMethods.LsaRetrievePrivateData(handle, keyBuffer, out buffer); + using (buffer) + { + if (res != 0) + { + // If the data object was not found we return null to indicate it isn't set. + if (res == 0xC0000034) // STATUS_OBJECT_NAME_NOT_FOUND + return null; + + throw new Win32Exception(NativeMethods.LsaNtStatusToWinError(res), + String.Format("LsaRetrievePrivateData({0}) failed", key)); + } + + NativeHelpers.LSA_UNICODE_STRING lsaString = (NativeHelpers.LSA_UNICODE_STRING) + Marshal.PtrToStructure(buffer.DangerousGetHandle(), + typeof(NativeHelpers.LSA_UNICODE_STRING)); + return (string)lsaString; + } + } + } + } +} +'@ + +$details = Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' +$module.Result.AutoAdminLogon = $details.AutoAdminLogon +$module.Result.DefaultUserName = $details.DefaultUserName +$module.Result.DefaultDomainName = $details.DefaultDomainName +$module.Result.DefaultPassword = $details.DefaultPassword +$module.Result.AutoLogonCount = $details.AutoLogonCount + +$handle = [Ansible.TestAutoLogonInfo.LsaUtil]::OpenPolicy(0x00000004) +try { + $password = [Ansible.TestAutoLogonInfo.LsaUtil]::RetrievePrivateData($handle, 'DefaultPassword') + $module.Result.LsaPassword = $password +} finally { + $handle.Dispose() +} + +$module.ExitJson() diff --git a/test/integration/targets/win_auto_logon/tasks/main.yml b/test/integration/targets/win_auto_logon/tasks/main.yml index 0636d7128f9..f8a8d01624f 100644 --- a/test/integration/targets/win_auto_logon/tasks/main.yml +++ b/test/integration/targets/win_auto_logon/tasks/main.yml @@ -1,36 +1,42 @@ -# Copyright: (c) 2019, Prasoon Karunan V (@prasoonkarunan) -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) --- -- name: Set autologon registry keys - win_auto_logon: - username: "{{ ansible_user }}" - password: "{{ ansible_password }}" - state: present - register: win_auto_logon_create_registry_key_set +- name: get user domain split for ansible_user + win_shell: | + $account = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList '{{ ansible_user }}' + $sid = $account.Translate([System.Security.Principal.SecurityIdentifier]) + $sid.Translate([System.Security.Principal.NTAccount]).Value -split '{{ "\\" }}' + changed_when: False + register: test_user_split -- name: check win_auto_logon_create_registry_key_set is changed - assert: - that: - - win_auto_logon_create_registry_key_set is changed +- set_fact: + test_domain: '{{ test_user_split.stdout_lines[0] }}' + test_user: '{{ test_user_split.stdout_lines[1] }}' -- name: Set autologon registry keys with missing input - win_auto_logon: - username: "{{ ansible_user }}" - state: present - register: win_auto_logon_create_registry_key_missing_input - ignore_errors: true - -- name: check win_auto_logon_create_registry_key_missing_input is failed - assert: - that: - - win_auto_logon_create_registry_key_missing_input is failed - -- name: Remove autologon registry keys +- name: ensure auto logon is cleared before test win_auto_logon: state: absent - register: win_auto_logon_create_registry_key_remove -- name: check win_auto_logon_create_registry_key_remove is changed - assert: - that: - - win_auto_logon_create_registry_key_remove is changed +- name: ensure defaults are set + win_regedit: + path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon + name: '{{ item.name }}' + data: '{{ item.value }}' + type: '{{ item.type }}' + state: present + loop: + # We set the DefaultPassword to ensure win_auto_logon clears this out + - name: DefaultPassword + value: abc + type: string + # Ensures the host we test on has a baseline key to check against + - name: AutoAdminLogon + value: 0 + type: dword + +- block: + - name: run tests + include_tasks: tests.yml + + always: + - name: make sure the auto logon is cleared + win_auto_logon: + state: absent diff --git a/test/integration/targets/win_auto_logon/tasks/tests.yml b/test/integration/targets/win_auto_logon/tasks/tests.yml new file mode 100644 index 00000000000..c25e07709ba --- /dev/null +++ b/test/integration/targets/win_auto_logon/tasks/tests.yml @@ -0,0 +1,178 @@ +# Copyright: (c) 2019, Prasoon Karunan V (@prasoonkarunan) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: set autologon registry keys (check mode) + win_auto_logon: + username: '{{ ansible_user }}' + password: '{{ test_logon_password }}' + state: present + register: set_check + check_mode: yes + +- name: get acutal of set autologon registry keys (check mode) + test_autologon_info: + register: set_actual_check + +- name: assert set autologon registry keys (check mode) + assert: + that: + - set_check is changed + - set_actual_check.AutoAdminLogon == 0 + - set_actual_check.AutoLogonCount == None + - set_actual_check.DefaultDomainName == None + - set_actual_check.DefaultPassword == 'abc' + - set_actual_check.DefaultUserName == None + - set_actual_check.LsaPassword == None + +- name: set autologon registry keys + win_auto_logon: + username: '{{ ansible_user }}' + password: '{{ test_logon_password }}' + state: present + register: set + +- name: get acutal of set autologon registry keys + test_autologon_info: + register: set_actual + +- name: assert set autologon registry keys + assert: + that: + - set is changed + - set_actual.AutoAdminLogon == 1 + - set_actual.AutoLogonCount == None + - set_actual.DefaultDomainName == test_domain + - set_actual.DefaultPassword == None + - set_actual.DefaultUserName == test_user + - set_actual.LsaPassword == test_logon_password + +- name: set autologon registry keys (idempotent) + win_auto_logon: + username: '{{ ansible_user }}' + password: '{{ test_logon_password }}' + state: present + register: set_again + +- name: assert set autologon registry keys (idempotent) + assert: + that: + - not set_again is changed + +- name: add logon count (check mode) + win_auto_logon: + username: '{{ ansible_user }}' + password: '{{ test_logon_password }}' + logon_count: 2 + state: present + register: logon_count_check + check_mode: yes + +- name: get result of add logon count (check mode) + test_autologon_info: + register: logon_count_actual_check + +- name: assert add logon count (check mode) + assert: + that: + - logon_count_check is changed + - logon_count_actual_check.AutoLogonCount == None + +- name: add logon count + win_auto_logon: + username: '{{ ansible_user }}' + password: '{{ test_logon_password }}' + logon_count: 2 + state: present + register: logon_count + +- name: get result of add logon count + test_autologon_info: + register: logon_count_actual + +- name: assert add logon count + assert: + that: + - logon_count is changed + - logon_count_actual.AutoLogonCount == 2 + +- name: change auto logon (check mode) + win_auto_logon: + username: '{{ ansible_user }}' + password: '{{ test_logon_password2 }}' + state: present + register: change_check + check_mode: yes + +- name: get reuslt of change auto logon (check mode) + test_autologon_info: + register: change_actual_check + +- name: assert change auto logon (check mode) + assert: + that: + - change_check is changed + - change_actual_check == logon_count_actual + +- name: change auto logon + win_auto_logon: + username: '{{ ansible_user }}' + password: '{{ test_logon_password2 }}' + state: present + register: change + +- name: get reuslt of change auto logon + test_autologon_info: + register: change_actual + +- name: assert change auto logon + assert: + that: + - change is changed + - change_actual.AutoLogonCount == None + - change_actual.LsaPassword == test_logon_password2 + +- name: remove autologon registry keys (check mode) + win_auto_logon: + state: absent + register: remove_check + check_mode: yes + +- name: get result of remove autologon registry keys (check mode) + test_autologon_info: + register: remove_actual_check + +- name: assert remove autologon registry keys (check mode) + assert: + that: + - remove_check is changed + - remove_actual_check == change_actual + +- name: remove autologon registry keys + win_auto_logon: + state: absent + register: remove + +- name: get result of remove autologon registry keys + test_autologon_info: + register: remove_actual + +- name: assert remove autologon registry keys + assert: + that: + - remove is changed + - remove_actual.AutoAdminLogon == 0 + - remove_actual.AutoLogonCount == None + - remove_actual.DefaultDomainName == None + - remove_actual.DefaultPassword == None + - remove_actual.DefaultUserName == None + - remove_actual.LsaPassword == None + +- name: remove autologon registry keys (idempotent) + win_auto_logon: + state: absent + register: remove_again + +- name: assert remove autologon registry keys (idempotent) + assert: + that: + - not remove_again is changed