1 #!powershell
2 
3 # Copyright: (c) 2017, Ansible Project
4 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5 
6 #AnsibleRequires -CSharpUtil Ansible.AccessToken
7 #AnsibleRequires -CSharpUtil Ansible.Basic
8 #Requires -Module Ansible.ModuleUtils.AddType
9 
10 $spec = @{
11     options = @{
12         letter = @{ type = "str"; required = $true }
13         path = @{ type = "path"; }
14         state = @{ type = "str"; default = "present"; choices = @("absent", "present") }
15         username = @{ type = "str" }
16         password = @{ type = "str"; no_log = $true }
17     }
18     required_if = @(
19         ,@("state", "present", @("path"))
20     )
21     supports_check_mode = $true
22 }
23 
24 $module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
25 
26 $letter = $module.Params.letter
27 $path = $module.Params.path
28 $state = $module.Params.state
29 $username = $module.Params.username
30 $password = $module.Params.password
31 
32 if ($letter -notmatch "^[a-zA-z]{1}$") {
33     $module.FailJson("letter must be a single letter from A-Z, was: $letter")
34 }
35 $letter_root = "$($letter):"
36 
37 $module.Diff.before = ""
38 $module.Diff.after = ""
39 
40 Add-CSharpType -AnsibleModule $module -References @'
41 using Microsoft.Win32.SafeHandles;
42 using System;
43 using System.Collections.Generic;
44 using System.Runtime.ConstrainedExecution;
45 using System.Runtime.InteropServices;
46 
47 namespace Ansible.MappedDrive
48 {
49     internal class NativeHelpers
50     {
51         public enum ResourceScope : uint
52         {
53             Connected = 0x00000001,
54             GlobalNet = 0x00000002,
55             Remembered = 0x00000003,
56             Recent = 0x00000004,
57             Context = 0x00000005,
58         }
59 
60         [Flags]
61         public enum ResourceType : uint
62         {
63             Any = 0x0000000,
64             Disk = 0x00000001,
65             Print = 0x00000002,
66             Reserved = 0x00000008,
67             Unknown = 0xFFFFFFFF,
68         }
69 
70         public enum CloseFlags : uint
71         {
72             None = 0x00000000,
73             UpdateProfile = 0x00000001,
74         }
75 
76         [Flags]
77         public enum AddFlags : uint
78         {
79             UpdateProfile = 0x00000001,
80             UpdateRecent = 0x00000002,
81             Temporary = 0x00000004,
82             Interactive = 0x00000008,
83             Prompt = 0x00000010,
84             Redirect = 0x00000080,
85             CurrentMedia = 0x00000200,
86             CommandLine = 0x00000800,
87             CmdSaveCred = 0x00001000,
88             CredReset = 0x00002000,
89         }
90 
91         [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
92         public struct NETRESOURCEW
93         {
94             public ResourceScope dwScope;
95             public ResourceType dwType;
96             public UInt32 dwDisplayType;
97             public UInt32 dwUsage;
98             [MarshalAs(UnmanagedType.LPWStr)] public string lpLocalName;
99             [MarshalAs(UnmanagedType.LPWStr)] public string lpRemoteName;
100             [MarshalAs(UnmanagedType.LPWStr)] public string lpComment;
101             [MarshalAs(UnmanagedType.LPWStr)] public string lpProvider;
102         }
103     }
104 
105     internal class NativeMethods
106     {
107         [DllImport("kernel32.dll", SetLastError = true)]
108         public static extern bool CloseHandle(
109             IntPtr hObject);
110 
111         [DllImport("advapi32.dll", SetLastError = true)]
112         public static extern bool ImpersonateLoggedOnUser(
113             IntPtr hToken);
114 
115         [DllImport("advapi32.dll", SetLastError = true)]
116         public static extern bool RevertToSelf();
117 
118         [DllImport("Mpr.dll", CharSet = CharSet.Unicode)]
119         public static extern UInt32 WNetAddConnection2W(
120             NativeHelpers.NETRESOURCEW lpNetResource,
121             [MarshalAs(UnmanagedType.LPWStr)] string lpPassword,
122             [MarshalAs(UnmanagedType.LPWStr)] string lpUserName,
123             NativeHelpers.AddFlags dwFlags);
124 
125         [DllImport("Mpr.dll", CharSet = CharSet.Unicode)]
126         public static extern UInt32 WNetCancelConnection2W(
127             [MarshalAs(UnmanagedType.LPWStr)] string lpName,
128             NativeHelpers.CloseFlags dwFlags,
129             bool fForce);
130 
131         [DllImport("Mpr.dll")]
132         public static extern UInt32 WNetCloseEnum(
133             IntPtr hEnum);
134 
135         [DllImport("Mpr.dll", CharSet = CharSet.Unicode)]
136         public static extern UInt32 WNetEnumResourceW(
137             IntPtr hEnum,
138             ref Int32 lpcCount,
139             SafeMemoryBuffer lpBuffer,
140             ref UInt32 lpBufferSize);
141 
142         [DllImport("Mpr.dll", CharSet = CharSet.Unicode)]
143         public static extern UInt32 WNetOpenEnumW(
144             NativeHelpers.ResourceScope dwScope,
145             NativeHelpers.ResourceType dwType,
146             UInt32 dwUsage,
147             IntPtr lpNetResource,
148             out IntPtr lphEnum);
149     }
150 
151     internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
152     {
153         public SafeMemoryBuffer() : base(true) { }
154         public SafeMemoryBuffer(int cb) : base(true)
155         {
156             base.SetHandle(Marshal.AllocHGlobal(cb));
157         }
158         public SafeMemoryBuffer(IntPtr handle) : base(true)
159         {
160             base.SetHandle(handle);
161         }
162 
163         [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
164         protected override bool ReleaseHandle()
165         {
166             Marshal.FreeHGlobal(handle);
167             return true;
168         }
169     }
170 
171     internal class Impersonation : IDisposable
172     {
173         private IntPtr hToken = IntPtr.Zero;
174 
175         public Impersonation(IntPtr token)
176         {
177             hToken = token;
178             if (token != IntPtr.Zero)
179                 if (!NativeMethods.ImpersonateLoggedOnUser(hToken))
180                     throw new Win32Exception("Failed to impersonate token with ImpersonateLoggedOnUser()");
181         }
182 
183         public void Dispose()
184         {
185             if (hToken != null)
186                 NativeMethods.RevertToSelf();
187             GC.SuppressFinalize(this);
188         }
189         ~Impersonation() { Dispose(); }
190     }
191 
192     public class DriveInfo
193     {
194         public string Drive;
195         public string Path;
196     }
197 
198     public class Win32Exception : System.ComponentModel.Win32Exception
199     {
200         private string _msg;
201         public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
202         public Win32Exception(int errorCode, string message) : base(errorCode)
203         {
204             _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
205         }
206         public override string Message { get { return _msg; } }
207         public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
208     }
209 
210     public class Utils
211     {
212         private const UInt32 ERROR_SUCCESS = 0x00000000;
213         private const UInt32 ERROR_NO_MORE_ITEMS = 0x0000103;
214 
215         public static void AddMappedDrive(string drive, string path, IntPtr iToken, string username = null, string password = null)
216         {
217             NativeHelpers.NETRESOURCEW resource = new NativeHelpers.NETRESOURCEW
218             {
219                 dwType = NativeHelpers.ResourceType.Disk,
220                 lpLocalName = drive,
221                 lpRemoteName = path,
222             };
223             NativeHelpers.AddFlags dwFlags = NativeHelpers.AddFlags.UpdateProfile;
224             // While WNetAddConnection2W supports user/pass, this is only used for the first connection and the
225             // password is not remembered. We will delete the username mapping afterwards as it interferes with
226             // the implicit credential cache used in Windows
227             using (Impersonation imp = new Impersonation(iToken))
228             {
229                 UInt32 res = NativeMethods.WNetAddConnection2W(resource, password, username, dwFlags);
230                 if (res != ERROR_SUCCESS)
231                     throw new Win32Exception((int)res, String.Format("Failed to map {0} to '{1}' with WNetAddConnection2W()", drive, path));
232             }
233         }
234 
235         public static List<DriveInfo> GetMappedDrives(IntPtr iToken)
236         {
237             using (Impersonation imp = new Impersonation(iToken))
238             {
239                 IntPtr enumPtr = IntPtr.Zero;
240                 UInt32 res = NativeMethods.WNetOpenEnumW(NativeHelpers.ResourceScope.Remembered, NativeHelpers.ResourceType.Disk,
241                     0, IntPtr.Zero, out enumPtr);
242                 if (res != ERROR_SUCCESS)
243                     throw new Win32Exception((int)res, "WNetOpenEnumW()");
244 
245                 List<DriveInfo> resources = new List<DriveInfo>();
246                 try
247                 {
248                     // MS recommend a buffer size of 16 KiB
249                     UInt32 bufferSize = 16384;
250                     int lpcCount = -1;
251 
252                     // keep iterating the enum until ERROR_NO_MORE_ITEMS is returned
253                     do
254                     {
255                         using (SafeMemoryBuffer buffer = new SafeMemoryBuffer((int)bufferSize))
256                         {
257                             res = NativeMethods.WNetEnumResourceW(enumPtr, ref lpcCount, buffer, ref bufferSize);
258                             if (res == ERROR_NO_MORE_ITEMS)
259                                 continue;
260                             else if (res != ERROR_SUCCESS)
261                                 throw new Win32Exception((int)res, "WNetEnumResourceW()");
262                             lpcCount = lpcCount < 0 ? 0 : lpcCount;
263 
264                             NativeHelpers.NETRESOURCEW[] rawResources = new NativeHelpers.NETRESOURCEW[lpcCount];
265                             PtrToStructureArray(rawResources, buffer.DangerousGetHandle());
266                             foreach (NativeHelpers.NETRESOURCEW resource in rawResources)
267                             {
268                                 DriveInfo currentDrive = new DriveInfo
269                                 {
270                                     Drive = resource.lpLocalName,
271                                     Path = resource.lpRemoteName,
272                                 };
273                                 resources.Add(currentDrive);
274                             }
275                         }
276                     }
277                     while (res != ERROR_NO_MORE_ITEMS);
278                 }
279                 finally
280                 {
281                     NativeMethods.WNetCloseEnum(enumPtr);
282                 }
283 
284                 return resources;
285             }
286         }
287 
288         public static void RemoveMappedDrive(string drive, IntPtr iToken)
289         {
290             using (Impersonation imp = new Impersonation(iToken))
291             {
292                 UInt32 res = NativeMethods.WNetCancelConnection2W(drive, NativeHelpers.CloseFlags.UpdateProfile, true);
293                 if (res != ERROR_SUCCESS)
294                     throw new Win32Exception((int)res, String.Format("Failed to remove mapped drive {0} with WNetCancelConnection2W()", drive));
295             }
296         }
297 
298         private static void PtrToStructureArray<T>(T[] array, IntPtr ptr)
299         {
300             IntPtr ptrOffset = ptr;
301             for (int i = 0; i < array.Length; i++, ptrOffset = IntPtr.Add(ptrOffset, Marshal.SizeOf(typeof(T))))
302                 array[i] = (T)Marshal.PtrToStructure(ptrOffset, typeof(T));
303         }
304     }
305 }
306 '@
307 
Get-LimitedToken()308 Function Get-LimitedToken {
309     $h_process = [Ansible.AccessToken.TokenUtil]::OpenProcess()
310     $h_token = [Ansible.AccessToken.TokenUtil]::OpenProcessToken($h_process, "Duplicate, Query")
311 
312     try {
313         # If we don't have a Full token, we don't need to get the limited one to set a mapped drive
314         $tet = [Ansible.AccessToken.TokenUtil]::GetTokenElevationType($h_token)
315         if ($tet -ne [Ansible.AccessToken.TokenElevationType]::Full) {
316             return
317         }
318 
319         foreach ($system_token in [Ansible.AccessToken.TokenUtil]::EnumerateUserTokens("S-1-5-18", "Duplicate")) {
320             # To get the TokenLinkedToken we need the SeTcbPrivilege, not all SYSTEM tokens have this assigned so
321             # we need to check before impersonating that token
322             $token_privileges = [Ansible.AccessToken.TokenUtil]::GetTokenPrivileges($system_token)
323             if ($null -eq ($token_privileges | Where-Object { $_.Name -eq "SeTcbPrivilege" })) {
324                 continue
325             }
326 
327             [Ansible.AccessToken.TokenUtil]::ImpersonateToken($system_token)
328             try {
329                 return [Ansible.AccessToken.TokenUtil]::GetTokenLinkedToken($h_token)
330             } finally {
331                 [Ansible.AccessToken.TokenUtil]::RevertToSelf()
332             }
333         }
334     } finally {
335         $h_token.Dispose()
336     }
337 }
338 
339 <#
340 When we run with become and UAC is enabled, the become process will most likely be the Admin/Full token. This is
341 an issue with the WNetConnection APIs as the Full token is unable to add/enumerate/remove connections due to
342 Windows storing the connection details on each token session ID. Unless EnabledLinkedConnections (reg key) is
343 set to 1, the Full token is unable to manage connections in a persisted way whereas the Limited token is. This
344 is similar to running 'net use' normally and an admin process is unable to see those and vice versa.
345 
346 To overcome this problem, we attempt to get a handle on the Limited token for the current logon and impersonate
347 that before making any WNetConnection calls. If the token is not split, or we are already running on the Limited
348 token then no impersonatoin is used/required. This allows the module to run with become (required to access the
349 credential store) but still be able to manage the mapped connections.
350 
351 These are the following scenarios we have to handle;
352 
353     1. Run without become
354         A network logon is usually not split so GetLimitedToken() will return $null and no impersonation is needed
355     2. Run with become on admin user with admin priv
356         We will have a Full token, GetLimitedToken() will return the limited token and impersonation is used
357     3. Run with become on admin user without admin priv
358         We are already running with a Limited token, GetLimitedToken() return $nul and no impersonation is needed
359     4. Run with become on standard user
360         There's no split token, GetLimitedToken() will return $null and no impersonation is needed
361 #>
362 $impersonation_token = Get-LimitedToken
363 
364 try {
365     $i_token_ptr = [System.IntPtr]::Zero
366     if ($null -ne $impersonation_token) {
367         $i_token_ptr = $impersonation_token.DangerousGetHandle()
368     }
369 
370     $existing_targets = [Ansible.MappedDrive.Utils]::GetMappedDrives($i_token_ptr)
371     $existing_target = $existing_targets | Where-Object { $_.Drive -eq $letter_root }
372 
373     if ($existing_target) {
374         $module.Diff.before = @{
375             letter = $letter
376             path = $existing_target.Path
377         }
378     }
379 
380     if ($state -eq "absent") {
381         if ($null -ne $existing_target) {
382             if ($null -ne $path -and $existing_target.Path -ne $path) {
383                 $module.FailJson("did not delete mapped drive $letter, the target path is pointing to a different location at $( $existing_target.Path )")
384             }
385             if (-not $module.CheckMode) {
386                 [Ansible.MappedDrive.Utils]::RemoveMappedDrive($letter_root, $i_token_ptr)
387             }
388 
389             $module.Result.changed = $true
390         }
391     } else {
392         $physical_drives = Get-PSDrive -PSProvider "FileSystem"
393         if ($letter -in $physical_drives.Name) {
394             $module.FailJson("failed to create mapped drive $letter, this letter is in use and is pointing to a non UNC path")
395         }
396 
397         # PowerShell converts a $null value to "" when crossing the .NET marshaler, we need to convert the input
398         # to a missing value so it uses the defaults. We also need to Invoke it with MethodInfo.Invoke so the defaults
399         # are still used
400         $input_username = $username
401         if ($null -eq $username) {
402             $input_username = [Type]::Missing
403         }
404         $input_password = $password
405         if ($null -eq $password) {
406             $input_password = [Type]::Missing
407         }
408         $add_method = [Ansible.MappedDrive.Utils].GetMethod("AddMappedDrive")
409 
410         if ($null -ne $existing_target) {
411             if ($existing_target.Path -ne $path) {
412                 if (-not $module.CheckMode) {
413                     [Ansible.MappedDrive.Utils]::RemoveMappedDrive($letter_root, $i_token_ptr)
414                     $add_method.Invoke($null, [Object[]]@($letter_root, $path, $i_token_ptr, $input_username, $input_password))
415                 }
416                 $module.Result.changed = $true
417             }
418         } else  {
419             if (-not $module.CheckMode)  {
420                 $add_method.Invoke($null, [Object[]]@($letter_root, $path, $i_token_ptr, $input_username, $input_password))
421             }
422 
423             $module.Result.changed = $true
424         }
425 
426         # If username was set and we made a change, remove the UserName value so Windows will continue to use the cred
427         # cache. If we don't do this then the drive will fail to map in the future as WNetAddConnection does not cache
428         # the password and relies on the credential store.
429         if ($null -ne $username -and $module.Result.changed -and -not $module.CheckMode) {
430             Set-ItemProperty -LiteralPath HKCU:\Network\$letter -Name UserName -Value "" -WhatIf:$module.CheckMode
431         }
432 
433         $module.Diff.after = @{
434             letter = $letter
435             path = $path
436         }
437     }
438 } finally {
439     if ($null -ne $impersonation_token) {
440         $impersonation_token.Dispose()
441     }
442 }
443 
444 $module.ExitJson()
445