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