1
0
Fork 0
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:
Jordan Borean 2018-11-22 08:12:41 +10:00 committed by GitHub
parent 8e92cca139
commit a568bbed3c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 760 additions and 109 deletions

View 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

View file

@ -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()

View file

@ -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'''

View file

@ -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

View file

@ -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