mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
win_mapped_drive - refactor module and docs (#48642)
* win_mapped_drive - refactor module and docs * Updated code to work with become and split tokens * use win_credential_manager instead of cmdkey * updated credential manager module name * harden the system token impersonation process
This commit is contained in:
parent
8e92cca139
commit
a568bbed3c
5 changed files with 760 additions and 109 deletions
2
changelogs/fragments/win_mapped_drive-fixes.yaml
Normal file
2
changelogs/fragments/win_mapped_drive-fixes.yaml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
bugfixes:
|
||||||
|
- win_mapped_drive - Updated win_mapped_drive to use the proper Win32 APIs and updated documentation for proper usage
|
|
@ -3,117 +3,617 @@
|
||||||
# Copyright: (c) 2017, Ansible Project
|
# Copyright: (c) 2017, Ansible Project
|
||||||
# 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)
|
||||||
|
|
||||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||||
|
#Requires -Module Ansible.ModuleUtils.AddType
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$spec = @{
|
||||||
|
options = @{
|
||||||
$params = Parse-Args $args -supports_check_mode $true
|
letter = @{ type = "str"; required = $true }
|
||||||
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
|
path = @{ type = "path"; }
|
||||||
$diff_mode = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false
|
state = @{ type = "str"; default = "present"; choices = @("absent", "present") }
|
||||||
|
username = @{ type = "str" }
|
||||||
$letter = Get-AnsibleParam -obj $params -name "letter" -type "str" -failifempty $true
|
password = @{ type = "str"; no_log = $true }
|
||||||
$path = Get-AnsibleParam -obj $params -name "path" -type "path"
|
}
|
||||||
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent","present"
|
required_if = @(
|
||||||
$username = Get-AnsibleParam -obj $params -name "username" -type "str"
|
,@("state", "present", @("path"))
|
||||||
$password = Get-AnsibleParam -obj $params -name "password" -type "str"
|
)
|
||||||
|
supports_check_mode = $true
|
||||||
$result = @{
|
|
||||||
changed = $false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($diff_mode) {
|
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||||
$result.diff = @{}
|
|
||||||
}
|
$letter = $module.Params.letter
|
||||||
|
$path = $module.Params.path
|
||||||
|
$state = $module.Params.state
|
||||||
|
$username = $module.Params.username
|
||||||
|
$password = $module.Params.password
|
||||||
|
|
||||||
if ($letter -notmatch "^[a-zA-z]{1}$") {
|
if ($letter -notmatch "^[a-zA-z]{1}$") {
|
||||||
Fail-Json $result "letter must be a single letter from A-Z, was: $letter"
|
$module.FailJson("letter must be a single letter from A-Z, was: $letter")
|
||||||
|
}
|
||||||
|
$letter_root = "$($letter):"
|
||||||
|
|
||||||
|
$module.Diff.before = ""
|
||||||
|
$module.Diff.after = ""
|
||||||
|
|
||||||
|
Add-CSharpType -AnsibleModule $module -References @'
|
||||||
|
using Microsoft.Win32.SafeHandles;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.ConstrainedExecution;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Security.Principal;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Ansible.MappedDrive
|
||||||
|
{
|
||||||
|
internal class NativeHelpers
|
||||||
|
{
|
||||||
|
public enum ResourceScope : uint
|
||||||
|
{
|
||||||
|
Connected = 0x00000001,
|
||||||
|
GlobalNet = 0x00000002,
|
||||||
|
Remembered = 0x00000003,
|
||||||
|
Recent = 0x00000004,
|
||||||
|
Context = 0x00000005,
|
||||||
}
|
}
|
||||||
|
|
||||||
Function Get-MappedDriveTarget($letter) {
|
[Flags]
|
||||||
# Get-PSDrive and Get-CimInstance doesn't work through WinRM
|
public enum ResourceType : uint
|
||||||
$target = $null
|
{
|
||||||
if (Test-Path -Path HKCU:\Network\$letter) {
|
Any = 0x0000000,
|
||||||
$target = (Get-ItemProperty -Path HKCU:\Network\$letter -Name RemotePath).RemotePath
|
Disk = 0x00000001,
|
||||||
|
Print = 0x00000002,
|
||||||
|
Reserved = 0x00000008,
|
||||||
|
Unknown = 0xFFFFFFFF,
|
||||||
}
|
}
|
||||||
|
|
||||||
return $target
|
public enum CloseFlags : uint
|
||||||
|
{
|
||||||
|
None = 0x00000000,
|
||||||
|
UpdateProfile = 0x00000001,
|
||||||
}
|
}
|
||||||
|
|
||||||
Function Remove-MappedDrive($letter) {
|
[Flags]
|
||||||
# Remove-PSDrive doesn't work through WinRM as it cannot view the mapped drives for the user
|
public enum AddFlags : uint
|
||||||
if (-not $check_mode) {
|
{
|
||||||
|
UpdateProfile = 0x00000001,
|
||||||
|
UpdateRecent = 0x00000002,
|
||||||
|
Temporary = 0x00000004,
|
||||||
|
Interactive = 0x00000008,
|
||||||
|
Prompt = 0x00000010,
|
||||||
|
Redirect = 0x00000080,
|
||||||
|
CurrentMedia = 0x00000200,
|
||||||
|
CommandLine = 0x00000800,
|
||||||
|
CmdSaveCred = 0x00001000,
|
||||||
|
CredReset = 0x00002000,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum TokenElevationType
|
||||||
|
{
|
||||||
|
TokenElevationTypeDefault = 1,
|
||||||
|
TokenElevationTypeFull,
|
||||||
|
TokenElevationTypeLimited
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum TokenInformationClass
|
||||||
|
{
|
||||||
|
TokenUser = 1,
|
||||||
|
TokenPrivileges = 3,
|
||||||
|
TokenElevationType = 18,
|
||||||
|
TokenLinkedToken = 19,
|
||||||
|
}
|
||||||
|
|
||||||
|
[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, CharSet = CharSet.Unicode)]
|
||||||
|
public struct NETRESOURCEW
|
||||||
|
{
|
||||||
|
public ResourceScope dwScope;
|
||||||
|
public ResourceType dwType;
|
||||||
|
public UInt32 dwDisplayType;
|
||||||
|
public UInt32 dwUsage;
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] public string lpLocalName;
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] public string lpRemoteName;
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] public string lpComment;
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] public string lpProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct SID_AND_ATTRIBUTES
|
||||||
|
{
|
||||||
|
public IntPtr Sid;
|
||||||
|
public UInt32 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_USER
|
||||||
|
{
|
||||||
|
public SID_AND_ATTRIBUTES User;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class NativeMethods
|
||||||
|
{
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern bool CloseHandle(
|
||||||
|
IntPtr hObject);
|
||||||
|
|
||||||
|
[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("kernel32.dll")]
|
||||||
|
public static extern SafeNativeHandle GetCurrentProcess();
|
||||||
|
|
||||||
|
[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("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern SafeNativeHandle OpenProcess(
|
||||||
|
UInt32 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();
|
||||||
|
|
||||||
|
[DllImport("Mpr.dll", CharSet = CharSet.Unicode)]
|
||||||
|
public static extern UInt32 WNetAddConnection2W(
|
||||||
|
NativeHelpers.NETRESOURCEW lpNetResource,
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] string lpPassword,
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] string lpUserName,
|
||||||
|
NativeHelpers.AddFlags dwFlags);
|
||||||
|
|
||||||
|
[DllImport("Mpr.dll", CharSet = CharSet.Unicode)]
|
||||||
|
public static extern UInt32 WNetCancelConnection2W(
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] string lpName,
|
||||||
|
NativeHelpers.CloseFlags dwFlags,
|
||||||
|
bool fForce);
|
||||||
|
|
||||||
|
[DllImport("Mpr.dll")]
|
||||||
|
public static extern UInt32 WNetCloseEnum(
|
||||||
|
IntPtr hEnum);
|
||||||
|
|
||||||
|
[DllImport("Mpr.dll", CharSet = CharSet.Unicode)]
|
||||||
|
public static extern UInt32 WNetEnumResourceW(
|
||||||
|
IntPtr hEnum,
|
||||||
|
ref Int32 lpcCount,
|
||||||
|
SafeMemoryBuffer lpBuffer,
|
||||||
|
ref UInt32 lpBufferSize);
|
||||||
|
|
||||||
|
[DllImport("Mpr.dll", CharSet = CharSet.Unicode)]
|
||||||
|
public static extern UInt32 WNetOpenEnumW(
|
||||||
|
NativeHelpers.ResourceScope dwScope,
|
||||||
|
NativeHelpers.ResourceType dwType,
|
||||||
|
UInt32 dwUsage,
|
||||||
|
IntPtr lpNetResource,
|
||||||
|
out IntPtr lphEnum);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Impersonation : IDisposable
|
||||||
|
{
|
||||||
|
private SafeNativeHandle hToken = null;
|
||||||
|
|
||||||
|
public Impersonation(SafeNativeHandle token)
|
||||||
|
{
|
||||||
|
hToken = token;
|
||||||
|
if (token != null)
|
||||||
|
if (!NativeMethods.ImpersonateLoggedOnUser(hToken))
|
||||||
|
throw new Win32Exception("Failed to impersonate token with ImpersonateLoggedOnUser()");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (hToken != null)
|
||||||
|
NativeMethods.RevertToSelf();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
~Impersonation() { Dispose(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DriveInfo
|
||||||
|
{
|
||||||
|
public string Drive;
|
||||||
|
public string Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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 Utils
|
||||||
|
{
|
||||||
|
private const TokenAccessLevels IMPERSONATE_ACCESS = TokenAccessLevels.Query | TokenAccessLevels.Duplicate;
|
||||||
|
private const UInt32 ERROR_SUCCESS = 0x00000000;
|
||||||
|
private const UInt32 ERROR_NO_MORE_ITEMS = 0x0000103;
|
||||||
|
|
||||||
|
public static void AddMappedDrive(string drive, string path, SafeNativeHandle iToken, string username = null, string password = null)
|
||||||
|
{
|
||||||
|
NativeHelpers.NETRESOURCEW resource = new NativeHelpers.NETRESOURCEW
|
||||||
|
{
|
||||||
|
dwType = NativeHelpers.ResourceType.Disk,
|
||||||
|
lpLocalName = drive,
|
||||||
|
lpRemoteName = path,
|
||||||
|
};
|
||||||
|
NativeHelpers.AddFlags dwFlags = NativeHelpers.AddFlags.UpdateProfile;
|
||||||
|
// While WNetAddConnection2W supports user/pass, this is only used for the first connection and the
|
||||||
|
// password is not remembered. We will delete the username mapping afterwards as it interferes with
|
||||||
|
// the implicit credential cache used in Windows
|
||||||
|
using (Impersonation imp = new Impersonation(iToken))
|
||||||
|
{
|
||||||
|
UInt32 res = NativeMethods.WNetAddConnection2W(resource, password, username, dwFlags);
|
||||||
|
if (res != ERROR_SUCCESS)
|
||||||
|
throw new Win32Exception((int)res, String.Format("Failed to map {0} to '{1}' with WNetAddConnection2W()", drive, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<DriveInfo> GetMappedDrives(SafeNativeHandle iToken)
|
||||||
|
{
|
||||||
|
using (Impersonation imp = new Impersonation(iToken))
|
||||||
|
{
|
||||||
|
IntPtr enumPtr = IntPtr.Zero;
|
||||||
|
UInt32 res = NativeMethods.WNetOpenEnumW(NativeHelpers.ResourceScope.Remembered, NativeHelpers.ResourceType.Disk,
|
||||||
|
0, IntPtr.Zero, out enumPtr);
|
||||||
|
if (res != ERROR_SUCCESS)
|
||||||
|
throw new Win32Exception((int)res, "WNetOpenEnumW()");
|
||||||
|
|
||||||
|
List<DriveInfo> resources = new List<DriveInfo>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// MS recommend a buffer size of 16 KiB
|
||||||
|
UInt32 bufferSize = 16384;
|
||||||
|
int lpcCount = -1;
|
||||||
|
|
||||||
|
// keep iterating the enum until ERROR_NO_MORE_ITEMS is returned
|
||||||
|
do
|
||||||
|
{
|
||||||
|
using (SafeMemoryBuffer buffer = new SafeMemoryBuffer((int)bufferSize))
|
||||||
|
{
|
||||||
|
res = NativeMethods.WNetEnumResourceW(enumPtr, ref lpcCount, buffer, ref bufferSize);
|
||||||
|
if (res == ERROR_NO_MORE_ITEMS)
|
||||||
|
continue;
|
||||||
|
else if (res != ERROR_SUCCESS)
|
||||||
|
throw new Win32Exception((int)res, "WNetEnumResourceW()");
|
||||||
|
lpcCount = lpcCount < 0 ? 0 : lpcCount;
|
||||||
|
|
||||||
|
NativeHelpers.NETRESOURCEW[] rawResources = new NativeHelpers.NETRESOURCEW[lpcCount];
|
||||||
|
PtrToStructureArray(rawResources, buffer.DangerousGetHandle());
|
||||||
|
foreach (NativeHelpers.NETRESOURCEW resource in rawResources)
|
||||||
|
{
|
||||||
|
DriveInfo currentDrive = new DriveInfo
|
||||||
|
{
|
||||||
|
Drive = resource.lpLocalName,
|
||||||
|
Path = resource.lpRemoteName,
|
||||||
|
};
|
||||||
|
resources.Add(currentDrive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (res != ERROR_NO_MORE_ITEMS);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
NativeMethods.WNetCloseEnum(enumPtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RemoveMappedDrive(string drive, SafeNativeHandle iToken)
|
||||||
|
{
|
||||||
|
using (Impersonation imp = new Impersonation(iToken))
|
||||||
|
{
|
||||||
|
UInt32 res = NativeMethods.WNetCancelConnection2W(drive, NativeHelpers.CloseFlags.UpdateProfile, true);
|
||||||
|
if (res != ERROR_SUCCESS)
|
||||||
|
throw new Win32Exception((int)res, String.Format("Failed to remove mapped drive {0} with WNetCancelConnection2W()", drive));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SafeNativeHandle GetLimitedToken()
|
||||||
|
{
|
||||||
|
SafeNativeHandle hToken = null;
|
||||||
|
if (!NativeMethods.OpenProcessToken(NativeMethods.GetCurrentProcess(), IMPERSONATE_ACCESS, out hToken))
|
||||||
|
throw new Win32Exception("Failed to open current process token with OpenProcessToken()");
|
||||||
|
|
||||||
|
using (hToken)
|
||||||
|
{
|
||||||
|
// Check the elevation type of the current token, only need to impersonate if it's a Full token
|
||||||
|
using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, NativeHelpers.TokenInformationClass.TokenElevationType))
|
||||||
|
{
|
||||||
|
NativeHelpers.TokenElevationType tet = (NativeHelpers.TokenElevationType)Marshal.ReadInt32(tokenInfo.DangerousGetHandle());
|
||||||
|
|
||||||
|
// If we don't have a Full token, we don't need to get the limited one to set a mapped drive
|
||||||
|
if (tet != NativeHelpers.TokenElevationType.TokenElevationTypeFull)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have a full token, need to get the TokenLinkedToken, this requires the SeTcbPrivilege privilege
|
||||||
|
// and we can get that from impersonating a SYSTEM account token. Without this privilege we only get
|
||||||
|
// an SecurityIdentification token which won't work for what we need
|
||||||
|
using (SafeNativeHandle systemToken = GetSystemToken())
|
||||||
|
using (Impersonation systemImpersonation = new Impersonation(systemToken))
|
||||||
|
using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, NativeHelpers.TokenInformationClass.TokenLinkedToken))
|
||||||
|
return new SafeNativeHandle(Marshal.ReadIntPtr(tokenInfo.DangerousGetHandle()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SafeNativeHandle GetSystemToken()
|
||||||
|
{
|
||||||
|
foreach (System.Diagnostics.Process process in System.Diagnostics.Process.GetProcesses())
|
||||||
|
{
|
||||||
|
using (process)
|
||||||
|
{
|
||||||
|
// 0x00000400 == PROCESS_QUERY_INFORMATION
|
||||||
|
using (SafeNativeHandle hProcess = NativeMethods.OpenProcess(0x00000400, false, (UInt32)process.Id))
|
||||||
|
{
|
||||||
|
if (hProcess.IsInvalid)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
SafeNativeHandle hToken;
|
||||||
|
NativeMethods.OpenProcessToken(hProcess, IMPERSONATE_ACCESS, out hToken);
|
||||||
|
if (hToken.IsInvalid)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if ("S-1-5-18" == GetTokenUserSID(hToken))
|
||||||
|
{
|
||||||
|
// To get the TokenLinkedToken we need the SeTcbPrivilege, not all SYSTEM tokens have this
|
||||||
|
// assigned so we check before trying again
|
||||||
|
List<string> actualPrivileges = GetTokenPrivileges(hToken);
|
||||||
|
if (actualPrivileges.Contains("SeTcbPrivilege"))
|
||||||
|
return hToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
hToken.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException("Failed to get a copy of the SYSTEM token required to de-elevate the current user's token");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> 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 string GetTokenUserSID(SafeNativeHandle hToken)
|
||||||
|
{
|
||||||
|
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).Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>(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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'@
|
||||||
|
|
||||||
|
<#
|
||||||
|
When we run with become and UAC is enabled, the become process will most likely be the Admin/Full token. This is
|
||||||
|
an issue with the WNetConnection APIs as the Full token is unable to add/enumerate/remove connections due to
|
||||||
|
Windows storing the connection details on each token session ID. Unless EnabledLinkedConnections (reg key) is
|
||||||
|
set to 1, the Full token is unable to manage connections in a persisted way whereas the Limited token is. This
|
||||||
|
is similar to running 'net use' normally and an admin process is unable to see those and vice versa.
|
||||||
|
|
||||||
|
To overcome this problem, we attempt to get a handle on the Limited token for the current logon and impersonate
|
||||||
|
that before making any WNetConnection calls. If the token is not split, or we are already running on the Limited
|
||||||
|
token then no impersonatoin is used/required. This allows the module to run with become (required to access the
|
||||||
|
credential store) but still be able to manage the mapped connections.
|
||||||
|
|
||||||
|
These are the following scenarios we have to handle;
|
||||||
|
|
||||||
|
1. Run without become
|
||||||
|
A network logon is usually not split so GetLimitedToken() will return $null and no impersonation is needed
|
||||||
|
2. Run with become on admin user with admin priv
|
||||||
|
We will have a Full token, GetLimitedToken() will return the limited token and impersonation is used
|
||||||
|
3. Run with become on admin user without admin priv
|
||||||
|
We are already running with a Limited token, GetLimitedToken() return $nul and no impersonation is needed
|
||||||
|
4. Run with become on standard user
|
||||||
|
There's no split token, GetLimitedToken() will return $null and no impersonation is needed
|
||||||
|
#>
|
||||||
|
$impersonation_token = [Ansible.MappedDrive.Utils]::GetLimitedToken()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
&cmd.exe /c net use "$($letter):" /delete
|
$existing_targets = [Ansible.MappedDrive.Utils]::GetMappedDrives($impersonation_token)
|
||||||
} catch {
|
$existing_target = $existing_targets | Where-Object { $_.Drive -eq $letter_root }
|
||||||
Fail-Json $result "failed to removed mapped drive $($letter): $($_.Exception.Message)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$existing_target = Get-MappedDriveTarget -letter $letter
|
if ($existing_target) {
|
||||||
|
$module.Diff.before = @{
|
||||||
|
letter = $letter
|
||||||
|
path = $existing_target.Path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($state -eq "absent") {
|
if ($state -eq "absent") {
|
||||||
if ($existing_target -ne $null) {
|
if ($existing_target -ne $null) {
|
||||||
if ($path -ne $null) {
|
if ($null -ne $path -and $existing_target.Path -ne $path) {
|
||||||
if ($existing_target -eq $path) {
|
$module.FailJson("did not delete mapped drive $letter, the target path is pointing to a different location at $( $existing_target.Path )")
|
||||||
Remove-MappedDrive -letter $letter
|
|
||||||
} else {
|
|
||||||
Fail-Json $result "did not delete mapped drive $letter, the target path is pointing to a different location at $existing_target"
|
|
||||||
}
|
}
|
||||||
} else {
|
if (-not $module.CheckMode) {
|
||||||
Remove-MappedDrive -letter $letter
|
[Ansible.MappedDrive.Utils]::RemoveMappedDrive($letter_root, $impersonation_token)
|
||||||
}
|
}
|
||||||
|
|
||||||
$result.changed = $true
|
$module.Result.changed = $true
|
||||||
if ($diff_mode) {
|
|
||||||
$result.diff.prepared = "-$($letter): $existing_target"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ($path -eq $null) {
|
|
||||||
Fail-Json $result "path must be set when creating a mapped drive"
|
|
||||||
}
|
|
||||||
|
|
||||||
$extra_args = @{}
|
|
||||||
if ($username -ne $null) {
|
|
||||||
$sec_password = ConvertTo-SecureString -String $password -AsPlainText -Force
|
|
||||||
$credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $username, $sec_password
|
|
||||||
$extra_args.Credential = $credential
|
|
||||||
}
|
|
||||||
|
|
||||||
$physical_drives = Get-PSDrive -PSProvider "FileSystem"
|
$physical_drives = Get-PSDrive -PSProvider "FileSystem"
|
||||||
if ($letter -in $physical_drives.Name) {
|
if ($letter -in $physical_drives.Name) {
|
||||||
Fail-Json $result "failed to create mapped drive $letter, this letter is in use and is pointing to a non UNC path"
|
$module.FailJson("failed to create mapped drive $letter, this letter is in use and is pointing to a non UNC path")
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($existing_target -ne $null) {
|
# PowerShell converts a $null value to "" when crossing the .NET marshaler, we need to convert the input
|
||||||
if ($existing_target -ne $path -or ($username -ne $null)) {
|
# to a missing value so it uses the defaults. We also need to Invoke it with MethodInfo.Invoke so the defaults
|
||||||
# the source path doesn't match or we are putting in a credential
|
# are still used
|
||||||
Remove-MappedDrive -letter $letter
|
$input_username = $username
|
||||||
$result.changed = $true
|
if ($null -eq $username) {
|
||||||
|
$input_username = [Type]::Missing
|
||||||
try {
|
|
||||||
New-PSDrive -Name $letter -PSProvider "FileSystem" -root $path -Persist -WhatIf:$check_mode @extra_args | Out-Null
|
|
||||||
} catch {
|
|
||||||
Fail-Json $result "failed to create mapped drive $letter pointed to $($path): $($_.Exception.Message)"
|
|
||||||
}
|
}
|
||||||
|
$input_password = $password
|
||||||
if ($diff_mode) {
|
if ($null -eq $password) {
|
||||||
$result.diff.prepared = "-$($letter): $existing_target`n+$($letter): $path"
|
$input_password = [Type]::Missing
|
||||||
}
|
}
|
||||||
|
$add_method = [Ansible.MappedDrive.Utils].GetMethod("AddMappedDrive")
|
||||||
|
|
||||||
|
if ($null -ne $existing_target) {
|
||||||
|
if ($existing_target.Path -ne $path) {
|
||||||
|
if (-not $module.CheckMode) {
|
||||||
|
[Ansible.MappedDrive.Utils]::RemoveMappedDrive($letter_root, $impersonation_token)
|
||||||
|
$add_method.Invoke($null, [Object[]]@($letter_root, $path, $impersonation_token, $input_username, $input_password))
|
||||||
|
}
|
||||||
|
$module.Result.changed = $true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
if (-not $module.CheckMode) {
|
||||||
New-PSDrive -Name $letter -PSProvider "FileSystem" -Root $path -Persist -WhatIf:$check_mode @extra_args | Out-Null
|
$add_method.Invoke($null, [Object[]]@($letter_root, $path, $impersonation_token, $input_username, $input_password))
|
||||||
} catch {
|
|
||||||
Fail-Json $result "failed to create mapped drive $letter pointed to $($path): $($_.Exception.Message)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$result.changed = $true
|
$module.Result.changed = $true
|
||||||
if ($diff_mode) {
|
|
||||||
$result.diff.prepared = "+$($letter): $path"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# If username was set and we made a change, remove the UserName value so Windows will continue to use the cred
|
||||||
|
# cache. If we don't do this then the drive will fail to map in the future as WNetAddConnection does not cache
|
||||||
|
# the password and relies on the credential store.
|
||||||
|
if ($null -ne $username -and $module.Result.changed -and -not $module.CheckMode) {
|
||||||
|
Set-ItemProperty -Path HKCU:\Network\$letter -Name UserName -Value "" -WhatIf:$module.CheckMode
|
||||||
|
}
|
||||||
|
|
||||||
|
$module.Diff.after = @{
|
||||||
|
letter = $letter
|
||||||
|
path = $path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if ($null -ne $impersonation_token) {
|
||||||
|
$impersonation_token.Dispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Exit-Json $result
|
$module.ExitJson()
|
||||||
|
|
|
@ -20,12 +20,20 @@ short_description: Map network drives for users
|
||||||
description:
|
description:
|
||||||
- Allows you to modify mapped network drives for individual users.
|
- Allows you to modify mapped network drives for individual users.
|
||||||
notes:
|
notes:
|
||||||
- This can only map a network drive for the current executing user and does not
|
|
||||||
allow you to set a default drive for all users of a system. Use other
|
|
||||||
Microsoft tools like GPOs to achieve this goal.
|
|
||||||
- You cannot use this module to access a mapped drive in another Ansible task,
|
- You cannot use this module to access a mapped drive in another Ansible task,
|
||||||
drives mapped with this module are only accessible when logging in
|
drives mapped with this module are only accessible when logging in
|
||||||
interactively with the user through the console or RDP.
|
interactively with the user through the console or RDP.
|
||||||
|
- It is recommend to run this module with become or CredSSP when the remote
|
||||||
|
path requires authentication.
|
||||||
|
- When using become or CredSSP, the task will have access to any local
|
||||||
|
credentials stored in the user's vault.
|
||||||
|
- If become or CredSSP is not available, the I(username) and I(password)
|
||||||
|
options can be used for the initial authentication but these are not
|
||||||
|
persisted.
|
||||||
|
- To create a system wide mount, use become with the U(SYSTEM) account.
|
||||||
|
- A system wide mount will always show as disconnected in Windows Explorer and
|
||||||
|
still relies on the user's credentials or credential vault for further
|
||||||
|
authentication if needed.
|
||||||
options:
|
options:
|
||||||
letter:
|
letter:
|
||||||
description:
|
description:
|
||||||
|
@ -34,15 +42,18 @@ options:
|
||||||
required: yes
|
required: yes
|
||||||
password:
|
password:
|
||||||
description:
|
description:
|
||||||
- The password for C(username).
|
- The password for C(username) that is used when testing the initial
|
||||||
|
connection.
|
||||||
|
- This is never saved with a mapped drive, use the M(win_credential) module
|
||||||
|
to persist a username and password for a host.
|
||||||
path:
|
path:
|
||||||
description:
|
description:
|
||||||
- The UNC path to map the drive to.
|
- The UNC path to map the drive to.
|
||||||
- This is required if C(state=present).
|
- This is required if C(state=present).
|
||||||
- If C(state=absent) and path is not set, the module will delete the mapped
|
- If C(state=absent) and I(path) is not set, the module will delete the
|
||||||
drive regardless of the target.
|
mapped drive regardless of the target.
|
||||||
- If C(state=absent) and the path is set, the module will throw an error if
|
- If C(state=absent) and the I(path) is set, the module will throw an error
|
||||||
path does not match the target of the mapped drive.
|
if path does not match the target of the mapped drive.
|
||||||
type: path
|
type: path
|
||||||
state:
|
state:
|
||||||
description:
|
description:
|
||||||
|
@ -52,9 +63,13 @@ options:
|
||||||
default: present
|
default: present
|
||||||
username:
|
username:
|
||||||
description:
|
description:
|
||||||
- Credentials to map the drive with.
|
- The username that is used when testing the initial connection.
|
||||||
- The username MUST include the domain or servername like SERVER\user, see
|
- This is never saved with a mapped drive, the the M(win_credential) module
|
||||||
the example for more information.
|
to persist a username and password for a host.
|
||||||
|
- This is required if the mapped drive requires authentication with
|
||||||
|
custom credentials and become, or CredSSP cannot be used.
|
||||||
|
- If become or CredSSP is used, any credentials saved with
|
||||||
|
M(win_credential) will automatically be used instead.
|
||||||
author:
|
author:
|
||||||
- Jordan Borean (@jborean93)
|
- Jordan Borean (@jborean93)
|
||||||
'''
|
'''
|
||||||
|
@ -76,19 +91,44 @@ EXAMPLES = r'''
|
||||||
path: \\domain\appdata\accounting
|
path: \\domain\appdata\accounting
|
||||||
state: absent
|
state: absent
|
||||||
|
|
||||||
- name: Create mapped drive with local credentials
|
- name: Create mapped drive with credentials and save the username and password
|
||||||
win_mapped_drive:
|
block:
|
||||||
letter: M
|
- name: Save the network credentials required for the mapped drive
|
||||||
path: \\SERVER\c$
|
win_credential:
|
||||||
username: SERVER\Administrator
|
name: server
|
||||||
password: Password
|
type: domain_password
|
||||||
|
username: username@DOMAIN
|
||||||
|
secret: Password01
|
||||||
|
state: present
|
||||||
|
|
||||||
- name: Create mapped drive with domain credentials
|
- name: Create a mapped drive that requires authentication
|
||||||
win_mapped_drive:
|
win_mapped_drive:
|
||||||
letter: M
|
letter: M
|
||||||
path: \\domain\appdata\it
|
path: \\SERVER\C$
|
||||||
username: DOMAIN\IT
|
state: present
|
||||||
password: Password
|
vars:
|
||||||
|
# become is required to save and retrieve the credentials in the tasks
|
||||||
|
ansible_become: yes
|
||||||
|
ansible_become_method: runas
|
||||||
|
ansible_become_user: '{{ ansible_user }}'
|
||||||
|
ansible_become_pass: '{{ ansible_password }}'
|
||||||
|
|
||||||
|
- name: Create mapped drive with credentials that do not persist on the next logon
|
||||||
|
win_mapped_drive:
|
||||||
|
letter: M
|
||||||
|
path: \\SERVER\C$
|
||||||
|
state: present
|
||||||
|
username: '{{ ansible_user }}'
|
||||||
|
password: '{{ ansible_password }}'
|
||||||
|
|
||||||
|
- name: Create a system wide mapped drive
|
||||||
|
win_mapped_drive:
|
||||||
|
letter: S
|
||||||
|
path: \\SERVER\C$
|
||||||
|
state: present
|
||||||
|
become: yes
|
||||||
|
become_method: runas
|
||||||
|
become_user: SYSTEM
|
||||||
'''
|
'''
|
||||||
|
|
||||||
RETURN = r'''
|
RETURN = r'''
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
# test setup
|
# test setup
|
||||||
- name: gather facts required by the tests
|
- name: gather facts required by the tests
|
||||||
setup:
|
setup:
|
||||||
|
gather_subset: platform
|
||||||
|
|
||||||
- name: ensure mapped drive is deleted before test
|
- name: ensure mapped drive is deleted before test
|
||||||
win_mapped_drive:
|
win_mapped_drive:
|
||||||
|
@ -31,12 +32,36 @@
|
||||||
- { name: '{{test_win_mapped_drive_path}}', path: '{{test_win_mapped_drive_local_path}}' }
|
- { name: '{{test_win_mapped_drive_path}}', path: '{{test_win_mapped_drive_local_path}}' }
|
||||||
- { name: '{{test_win_mapped_drive_path2}}', path: '{{test_win_mapped_drive_local_path2}}' }
|
- { name: '{{test_win_mapped_drive_path2}}', path: '{{test_win_mapped_drive_local_path2}}' }
|
||||||
|
|
||||||
|
# This ensures we test out the split token/become behaviour
|
||||||
|
- name: ensure builtin Administrator has a split token
|
||||||
|
win_regedit:
|
||||||
|
path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System
|
||||||
|
name: FilterAdministratorToken
|
||||||
|
data: 1
|
||||||
|
type: dword
|
||||||
|
register: admin_uac
|
||||||
|
|
||||||
|
- name: reboot to apply Admin approval mode setting
|
||||||
|
win_reboot:
|
||||||
|
when: admin_uac is changed
|
||||||
|
|
||||||
- block:
|
- block:
|
||||||
# tests
|
# tests
|
||||||
- include_tasks: tests.yml
|
- include_tasks: tests.yml
|
||||||
|
|
||||||
# test cleanup
|
# test cleanup
|
||||||
always:
|
always:
|
||||||
|
- name: remove stored credential
|
||||||
|
win_credential:
|
||||||
|
name: '{{ ansible_hostname }}'
|
||||||
|
type: domain_password
|
||||||
|
state: absent
|
||||||
|
vars:
|
||||||
|
ansible_become: yes
|
||||||
|
ansible_become_method: runas
|
||||||
|
ansible_become_user: '{{ ansible_user }}'
|
||||||
|
ansible_become_pass: '{{ ansible_password }}'
|
||||||
|
|
||||||
- name: ensure mapped drive is deleted at the end of the test
|
- name: ensure mapped drive is deleted at the end of the test
|
||||||
win_mapped_drive:
|
win_mapped_drive:
|
||||||
letter: '{{test_win_mapped_drive_letter}}'
|
letter: '{{test_win_mapped_drive_letter}}'
|
||||||
|
@ -60,3 +85,15 @@
|
||||||
win_user:
|
win_user:
|
||||||
name: '{{test_win_mapped_drive_temp_user}}'
|
name: '{{test_win_mapped_drive_temp_user}}'
|
||||||
state: absent
|
state: absent
|
||||||
|
|
||||||
|
- name: disable Admin approval mode if changed in test
|
||||||
|
win_regedit:
|
||||||
|
path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System
|
||||||
|
name: FilterAdministratorToken
|
||||||
|
data: 0
|
||||||
|
type: dword
|
||||||
|
when: admin_uac is changed
|
||||||
|
|
||||||
|
- name: reboot to apply Admin approval mode setting
|
||||||
|
win_reboot:
|
||||||
|
when: admin_uac is changed
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
- name: fail with invalid path
|
- name: fail with invalid path
|
||||||
win_mapped_drive:
|
win_mapped_drive:
|
||||||
letter: invalid
|
letter: invalid
|
||||||
|
state: absent
|
||||||
register: fail_invalid_letter
|
register: fail_invalid_letter
|
||||||
failed_when: "fail_invalid_letter.msg != 'letter must be a single letter from A-Z, was: invalid'"
|
failed_when: "fail_invalid_letter.msg != 'letter must be a single letter from A-Z, was: invalid'"
|
||||||
|
|
||||||
|
@ -10,7 +11,7 @@
|
||||||
letter: '{{test_win_mapped_drive_letter}}'
|
letter: '{{test_win_mapped_drive_letter}}'
|
||||||
state: present
|
state: present
|
||||||
register: fail_path_missing
|
register: fail_path_missing
|
||||||
failed_when: fail_path_missing.msg != 'path must be set when creating a mapped drive'
|
failed_when: "fail_path_missing.msg != 'state is present but all of the following are missing: path'"
|
||||||
|
|
||||||
- name: fail when specifying letter with existing physical path
|
- name: fail when specifying letter with existing physical path
|
||||||
win_mapped_drive:
|
win_mapped_drive:
|
||||||
|
@ -210,7 +211,7 @@
|
||||||
that:
|
that:
|
||||||
- map_with_credentials is changed
|
- map_with_credentials is changed
|
||||||
- map_with_credentials_actual.rc == 0
|
- map_with_credentials_actual.rc == 0
|
||||||
- map_with_credential_actual_username.value == '{{ansible_hostname}}\\{{test_win_mapped_drive_temp_user}}'
|
- map_with_credential_actual_username.value == '' # we explicitly remove the username part in the module
|
||||||
|
|
||||||
- name: map drive with current credentials again
|
- name: map drive with current credentials again
|
||||||
win_mapped_drive:
|
win_mapped_drive:
|
||||||
|
@ -224,7 +225,7 @@
|
||||||
- name: assert map drive with current credentials again
|
- name: assert map drive with current credentials again
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
- map_with_credentials_again is changed # we expect a change as it will just delete and recreate if credentials are passed
|
- not map_with_credentials_again is changed
|
||||||
|
|
||||||
- name: delete mapped drive without path check
|
- name: delete mapped drive without path check
|
||||||
win_mapped_drive:
|
win_mapped_drive:
|
||||||
|
@ -270,3 +271,74 @@
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
- delete_without_path_again is not changed
|
- delete_without_path_again is not changed
|
||||||
|
|
||||||
|
- name: store credential for test network account
|
||||||
|
win_credential:
|
||||||
|
name: '{{ ansible_hostname }}'
|
||||||
|
type: domain_password
|
||||||
|
username: '{{ test_win_mapped_drive_temp_user }}'
|
||||||
|
secret: '{{ test_win_mapped_drive_temp_password }}'
|
||||||
|
state: present
|
||||||
|
vars: &become_vars
|
||||||
|
ansible_become: yes
|
||||||
|
ansible_become_method: runas
|
||||||
|
ansible_become_user: '{{ ansible_user }}'
|
||||||
|
ansible_become_pass: '{{ ansible_password }}'
|
||||||
|
|
||||||
|
- name: map drive with stored cred (check mode)
|
||||||
|
win_mapped_drive:
|
||||||
|
letter: '{{test_win_mapped_drive_letter}}'
|
||||||
|
path: \\{{ansible_hostname}}\{{test_win_mapped_drive_path}}
|
||||||
|
state: present
|
||||||
|
check_mode: yes
|
||||||
|
vars: *become_vars
|
||||||
|
register: map_with_stored_cred_check
|
||||||
|
|
||||||
|
- name: get actual of map drive with stored cred (check mode)
|
||||||
|
win_command: 'net use {{test_win_mapped_drive_letter}}:'
|
||||||
|
register: map_with_stored_cred_actual_check
|
||||||
|
failed_when: False
|
||||||
|
|
||||||
|
- name: assert map drive with stored cred (check mode)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- map_with_stored_cred_check is changed
|
||||||
|
- map_with_stored_cred_actual_check.rc == 2
|
||||||
|
|
||||||
|
- name: map drive with stored cred
|
||||||
|
win_mapped_drive:
|
||||||
|
letter: '{{test_win_mapped_drive_letter}}'
|
||||||
|
path: \\{{ansible_hostname}}\{{test_win_mapped_drive_path}}
|
||||||
|
state: present
|
||||||
|
vars: *become_vars
|
||||||
|
register: map_with_stored_cred
|
||||||
|
|
||||||
|
- name: get actual of map drive with stored cred
|
||||||
|
win_command: 'net use {{test_win_mapped_drive_letter}}:'
|
||||||
|
register: map_with_stored_cred_actual
|
||||||
|
|
||||||
|
- name: get username of mapped network drive with stored cred
|
||||||
|
win_reg_stat:
|
||||||
|
path: HKCU:\Network\{{test_win_mapped_drive_letter}}
|
||||||
|
name: UserName
|
||||||
|
register: map_with_stored_cred_actual_username
|
||||||
|
|
||||||
|
- name: assert map drive with stored cred
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- map_with_stored_cred is changed
|
||||||
|
- map_with_stored_cred_actual.rc == 0
|
||||||
|
- map_with_stored_cred_actual_username.value == ''
|
||||||
|
|
||||||
|
- name: map drive with stored cred again
|
||||||
|
win_mapped_drive:
|
||||||
|
letter: '{{test_win_mapped_drive_letter}}'
|
||||||
|
path: \\{{ansible_hostname}}\{{test_win_mapped_drive_path}}
|
||||||
|
state: present
|
||||||
|
vars: *become_vars
|
||||||
|
register: map_with_stored_cred_again
|
||||||
|
|
||||||
|
- name: assert map drive with stored cred again
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- not map_with_stored_cred_again is changed
|
||||||
|
|
Loading…
Reference in a new issue