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
This commit is contained in:
parent
cff80f1319
commit
4d3ebd65db
6 changed files with 802 additions and 65 deletions
|
@ -3,27 +3,31 @@
|
||||||
# Copyright: (c) 2019, Prasoon Karunan V (@prasoonkarunan) <kvprasoon@Live.in>
|
# Copyright: (c) 2019, Prasoon Karunan V (@prasoonkarunan) <kvprasoon@Live.in>
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# 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.
|
# All helper methods are written in a binary module and has to be loaded for consuming them.
|
||||||
#AnsibleRequires -CSharpUtil Ansible.Basic
|
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||||
|
#Requires -Module Ansible.ModuleUtils.AddType
|
||||||
|
|
||||||
Set-StrictMode -Version 2.0
|
Set-StrictMode -Version 2.0
|
||||||
|
|
||||||
$spec = @{
|
$spec = @{
|
||||||
options = @{
|
options = @{
|
||||||
|
logon_count = @{type = "int"}
|
||||||
password = @{type = "str"; no_log = $true}
|
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"}
|
username = @{type = "str"}
|
||||||
}
|
}
|
||||||
required_if = @(
|
required_if = @(
|
||||||
, @("state", "present", @("username", "password"))
|
,@("state", "present", @("username", "password"))
|
||||||
)
|
)
|
||||||
|
supports_check_mode = $true
|
||||||
}
|
}
|
||||||
|
|
||||||
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||||
$password = $module.params.password
|
|
||||||
$state = $module.params.state
|
$logonCount = $module.Params.logon_count
|
||||||
$username = $module.params.username
|
$password = $module.Params.password
|
||||||
|
$state = $module.Params.state
|
||||||
|
$username = $module.Params.username
|
||||||
$domain = $null
|
$domain = $null
|
||||||
|
|
||||||
if ($username) {
|
if ($username) {
|
||||||
|
@ -40,44 +44,360 @@ if ($username) {
|
||||||
$domain, $username = $ntAccount.Value -split '\\'
|
$domain, $username = $ntAccount.Value -split '\\'
|
||||||
}
|
}
|
||||||
|
|
||||||
#Build ParamHash
|
# Make sure $null regardless of any input value if state: absent
|
||||||
|
if ($state -eq 'absent') {
|
||||||
$autoAdminLogon = 1
|
$password = $null
|
||||||
if($state -eq 'absent'){
|
|
||||||
$autoadminlogon = 0
|
|
||||||
}
|
}
|
||||||
$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){
|
Add-CSharpType -AnsibleModule $module -References @'
|
||||||
$currentKeyValue = $autoLogonKeyRegList | Select-Object -ExpandProperty $key -ErrorAction SilentlyContinue
|
using Microsoft.Win32.SafeHandles;
|
||||||
if (-not [String]::IsNullOrEmpty($currentKeyValue)) {
|
using System;
|
||||||
$expectedValue = $autoLogonKeyList[$key]
|
using System.Runtime.ConstrainedExecution;
|
||||||
if(($state -eq 'present') -and ($currentKeyValue -ne $expectedValue)) {
|
using System.Runtime.InteropServices;
|
||||||
Set-ItemProperty -LiteralPath $autoLogonRegPath -Name $key -Value $autoLogonKeyList[$key] -Force
|
using System.Text;
|
||||||
$actionTaken = $true
|
|
||||||
|
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
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||||
Remove-ItemProperty -LiteralPath $autoLogonRegPath -Name $key -Force
|
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') {
|
internal class NativeMethods
|
||||||
$actionTaken = $true
|
{
|
||||||
New-ItemProperty -LiteralPath $autoLogonRegPath -Name $key -Value $autoLogonKeyList[$key] -Force | Out-Null
|
[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
|
$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()
|
$module.ExitJson()
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,14 @@ description:
|
||||||
- Used to apply auto logon registry setting.
|
- Used to apply auto logon registry setting.
|
||||||
version_added: "2.10"
|
version_added: "2.10"
|
||||||
options:
|
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:
|
username:
|
||||||
description:
|
description:
|
||||||
- Username to login automatically.
|
- Username to login automatically.
|
||||||
|
@ -29,6 +37,8 @@ options:
|
||||||
- Password to be used for automatic login.
|
- Password to be used for automatic login.
|
||||||
- Must be set when C(state=present).
|
- Must be set when C(state=present).
|
||||||
- Value of this input will be used as password for I(username).
|
- 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
|
type: str
|
||||||
state:
|
state:
|
||||||
description:
|
description:
|
||||||
|
@ -54,6 +64,12 @@ EXAMPLES = r'''
|
||||||
- name: Remove autologon for user1
|
- name: Remove autologon for user1
|
||||||
win_auto_logon:
|
win_auto_logon:
|
||||||
state: absent
|
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'''
|
RETURN = r'''
|
||||||
|
|
|
@ -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(;)]'
|
|
@ -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()
|
|
@ -1,36 +1,42 @@
|
||||||
# Copyright: (c) 2019, Prasoon Karunan V (@prasoonkarunan) <kvprasoon@Live.in>
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
---
|
---
|
||||||
- name: Set autologon registry keys
|
- name: get user domain split for ansible_user
|
||||||
win_auto_logon:
|
win_shell: |
|
||||||
username: "{{ ansible_user }}"
|
$account = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList '{{ ansible_user }}'
|
||||||
password: "{{ ansible_password }}"
|
$sid = $account.Translate([System.Security.Principal.SecurityIdentifier])
|
||||||
state: present
|
$sid.Translate([System.Security.Principal.NTAccount]).Value -split '{{ "\\" }}'
|
||||||
register: win_auto_logon_create_registry_key_set
|
changed_when: False
|
||||||
|
register: test_user_split
|
||||||
|
|
||||||
- name: check win_auto_logon_create_registry_key_set is changed
|
- set_fact:
|
||||||
assert:
|
test_domain: '{{ test_user_split.stdout_lines[0] }}'
|
||||||
that:
|
test_user: '{{ test_user_split.stdout_lines[1] }}'
|
||||||
- win_auto_logon_create_registry_key_set is changed
|
|
||||||
|
|
||||||
- name: Set autologon registry keys with missing input
|
- name: ensure auto logon is cleared before test
|
||||||
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
|
|
||||||
win_auto_logon:
|
win_auto_logon:
|
||||||
state: absent
|
state: absent
|
||||||
register: win_auto_logon_create_registry_key_remove
|
|
||||||
|
|
||||||
- name: check win_auto_logon_create_registry_key_remove is changed
|
- name: ensure defaults are set
|
||||||
assert:
|
win_regedit:
|
||||||
that:
|
path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
|
||||||
- win_auto_logon_create_registry_key_remove is changed
|
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
|
||||||
|
|
178
test/integration/targets/win_auto_logon/tasks/tests.yml
Normal file
178
test/integration/targets/win_auto_logon/tasks/tests.yml
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
# Copyright: (c) 2019, Prasoon Karunan V (@prasoonkarunan) <kvprasoon@Live.in>
|
||||||
|
# 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
|
Loading…
Reference in a new issue