1 #!powershell
2 
3 # Copyright: (c) 2019, 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.Basic
7 
8 $spec = @{
9     options = @{
10         name = @{ type = "str" }
11         remove_multiple = @{ type = "bool"; default = $false }
12         state = @{ type = "str"; default = "present"; choices = @("absent", "present") }
13         username = @{ type = "sid"; }
14     }
15     required_if = @(
16         @("state", "present", @("username")),
17         @("state", "absent", @("name", "username"), $true)
18     )
19     supports_check_mode = $true
20 }
21 
22 $module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
23 $module.Result.path = $null
24 
25 $name = $module.Params.name
26 $remove_multiple = $module.Params.remove_multiple
27 $state = $module.Params.state
28 $username = $module.Params.username
29 
30 Add-CSharpType -AnsibleModule $module -References @'
31 using System;
32 using System.Runtime.InteropServices;
33 using System.Text;
34 
35 namespace Ansible.WinUserProfile
36 {
37     public class NativeMethods
38     {
39         [DllImport("Userenv.dll", CharSet = CharSet.Unicode)]
40         public static extern int CreateProfile(
41             [MarshalAs(UnmanagedType.LPWStr)] string pszUserSid,
42             [MarshalAs(UnmanagedType.LPWStr)] string pszUserName,
43             [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszProfilePath,
44             UInt32 cchProfilePath);
45 
46         [DllImport("Userenv.dll", SetLastError = true, CharSet = CharSet.Unicode)]
47         public static extern bool DeleteProfileW(
48             [MarshalAs(UnmanagedType.LPWStr)] string lpSidString,
49             IntPtr lpProfile,
50             IntPtr lpComputerName);
51 
52         [DllImport("Userenv.dll", SetLastError = true, CharSet = CharSet.Unicode)]
53         public static extern bool GetProfilesDirectoryW(
54             [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpProfileDir,
55             ref UInt32 lpcchSize);
56     }
57 }
58 '@
59 
Get-LastWin32ExceptionMessage()60 Function Get-LastWin32ExceptionMessage {
61     param([int]$ErrorCode)
62     $exp = New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList $ErrorCode
63     $exp_msg = "{0} (Win32 ErrorCode {1} - 0x{1:X8})" -f $exp.Message, $ErrorCode
64     return $exp_msg
65 }
66 
Get-ExpectedProfilePath()67 Function Get-ExpectedProfilePath {
68     param([String]$BaseName)
69 
70     # Environment.GetFolderPath does not have an enumeration to get the base profile dir, use PInvoke instead
71     # and combine with the base name to return back to the user - best efforts
72     $profile_path_length = 0
73     [Ansible.WinUserProfile.NativeMethods]::GetProfilesDirectoryW($null,
74         [ref]$profile_path_length) > $null
75 
76     $raw_profile_path = New-Object -TypeName System.Text.StringBuilder -ArgumentList $profile_path_length
77     $res = [Ansible.WinUserProfile.NativeMethods]::GetProfilesDirectoryW($raw_profile_path,
78         [ref]$profile_path_length)
79 
80     if ($res -eq $false) {
81         $msg = Get-LastWin32ExceptionMessage -Error ([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())
82         $module.FailJson("Failed to determine profile path with the base name '$BaseName': $msg")
83     }
84     $profile_path = Join-Path -Path $raw_profile_path.ToString() -ChildPath $BaseName
85 
86     return $profile_path
87 }
88 
89 $profiles = Get-ChildItem -LiteralPath "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList"
90 
91 if ($state -eq "absent") {
92     if ($null -ne $username) {
93         $user_profiles = $profiles | Where-Object { $_.PSChildName -eq $username.Value }
94     } else {
95         # If the username was not provided, or we are removing a profile for a deleted user, we need to try and find
96         # the correct SID to delete. We just verify that the path matches based on the name passed in
97         $expected_profile_path = Get-ExpectedProfilePath -BaseName $name
98 
99         $user_profiles = $profiles | Where-Object {
100             $profile_path = (Get-ItemProperty -LiteralPath $_.PSPath -Name ProfileImagePath).ProfileImagePath
101             $profile_path -eq $expected_profile_path
102         }
103 
104         if ($user_profiles.Length -gt 1 -and -not $remove_multiple) {
105             $module.FailJson("Found multiple profiles matching the path '$expected_profile_path', set 'remove_multiple=True' to remove all the profiles for this match")
106         }
107     }
108 
109     foreach ($user_profile in $user_profiles) {
110         $profile_path = (Get-ItemProperty -LiteralPath $user_profile.PSPath -Name ProfileImagePath).ProfileImagePath
111         if (-not $module.CheckMode) {
112             $res = [Ansible.WinUserProfile.NativeMethods]::DeleteProfileW($user_profile.PSChildName, [IntPtr]::Zero,
113                 [IntPtr]::Zero)
114             if ($res -eq $false) {
115                 $msg = Get-LastWin32ExceptionMessage -Error ([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())
116                 $module.FailJson("Failed to delete the profile for $($user_profile.PSChildName): $msg")
117             }
118         }
119 
120         # While we may have multiple profiles when the name option was used, it will always be the same path due to
121         # how we match name to a profile so setting it mutliple time sis fine
122         $module.Result.path = $profile_path
123         $module.Result.changed = $true
124     }
125 } elseif ($state -eq "present") {
126     # Now we know the SID, see if the profile already exists
127     $user_profile = $profiles | Where-Object { $_.PSChildName -eq $username.Value }
128     if ($null -eq $user_profile) {
129         # In case a SID was set as the username we still need to make sure the SID is mapped to a valid local account
130         try {
131             $account_name = $username.Translate([System.Security.Principal.NTAccount])
132         } catch [System.Security.Principal.IdentityNotMappedException] {
133             $module.FailJson("Fail to map the account '$($username.Value)' to a valid user")
134         }
135 
136         # If the basename was not provided, determine it from the actual username
137         if ($null -eq $name) {
138             $name = $account_name.Value.Split('\', 2)[-1]
139         }
140 
141         if ($module.CheckMode) {
142             $profile_path = Get-ExpectedProfilePath -BaseName $name
143         } else {
144             $raw_profile_path = New-Object -TypeName System.Text.StringBuilder -ArgumentList 260
145             $res = [Ansible.WinUserProfile.NativeMethods]::CreateProfile($username.Value, $name, $raw_profile_path,
146                 $raw_profile_path.Capacity)
147 
148             if ($res -ne 0) {
149                 $exp = [System.Runtime.InteropServices.Marshal]::GetExceptionForHR($res)
150                 $module.FailJson("Failed to create profile for user '$username': $($exp.Message)")
151             }
152             $profile_path = $raw_profile_path.ToString()
153         }
154 
155         $module.Result.changed = $true
156         $module.Result.path = $profile_path
157     } else {
158         $module.Result.path = (Get-ItemProperty -LiteralPath $user_profile.PSPath -Name ProfileImagePath).ProfileImagePath
159     }
160 }
161 
162 $module.ExitJson()
163 
164