1"""
2Run processes as a different user in Windows
3"""
4
5# Import Python Libraries
6import ctypes
7import logging
8import os
9import time
10
11from salt.exceptions import CommandExecutionError
12
13try:
14    import psutil
15
16    HAS_PSUTIL = True
17except ImportError:
18    HAS_PSUTIL = False
19
20try:
21    import win32api
22    import win32con
23    import win32process
24    import win32security
25    import win32pipe
26    import win32event
27    import win32profile
28    import msvcrt
29    import salt.platform.win
30    import pywintypes
31
32    HAS_WIN32 = True
33except ImportError:
34    HAS_WIN32 = False
35
36
37log = logging.getLogger(__name__)
38
39
40# Although utils are often directly imported, it is also possible to use the
41# loader.
42def __virtual__():
43    """
44    Only load if Win32 Libraries are installed
45    """
46    if not HAS_WIN32 or not HAS_PSUTIL:
47        return False, "This utility requires pywin32 and psutil"
48
49    return "win_runas"
50
51
52def split_username(username):
53    # TODO: Is there a windows api for this?
54    domain = "."
55    if "@" in username:
56        username, domain = username.split("@")
57    if "\\" in username:
58        domain, username = username.split("\\")
59    return username, domain
60
61
62def create_env(user_token, inherit, timeout=1):
63    """
64    CreateEnvironmentBlock might fail when we close a login session and then
65    try to re-open one very quickly. Run the method multiple times to work
66    around the async nature of logoffs.
67    """
68    start = time.time()
69    env = None
70    exc = None
71    while True:
72        try:
73            env = win32profile.CreateEnvironmentBlock(user_token, False)
74        except pywintypes.error as exc:
75            pass
76        else:
77            break
78        if time.time() - start > timeout:
79            break
80    if env is not None:
81        return env
82    raise exc
83
84
85def runas(cmdLine, username, password=None, cwd=None):
86    """
87    Run a command as another user. If the process is running as an admin or
88    system account this method does not require a password. Other non
89    privileged accounts need to provide a password for the user to runas.
90    Commands are run in with the highest level privileges possible for the
91    account provided.
92    """
93    # Validate the domain and sid exist for the username
94    username, domain = split_username(username)
95    try:
96        _, domain, _ = win32security.LookupAccountName(domain, username)
97    except pywintypes.error as exc:
98        message = win32api.FormatMessage(exc.winerror).rstrip("\n")
99        raise CommandExecutionError(message)
100
101    # Elevate the token from the current process
102    access = win32security.TOKEN_QUERY | win32security.TOKEN_ADJUST_PRIVILEGES
103    th = win32security.OpenProcessToken(win32api.GetCurrentProcess(), access)
104    salt.platform.win.elevate_token(th)
105
106    # Try to impersonate the SYSTEM user. This process needs to be running as a
107    # user who as been granted the SeImpersonatePrivilege, Administrator
108    # accounts have this permission by default.
109    try:
110        impersonation_token = salt.platform.win.impersonate_sid(
111            salt.platform.win.SYSTEM_SID,
112            session_id=0,
113            privs=["SeTcbPrivilege"],
114        )
115    except OSError:
116        log.debug("Unable to impersonate SYSTEM user")
117        impersonation_token = None
118        win32api.CloseHandle(th)
119
120    # Impersonation of the SYSTEM user failed. Fallback to an un-privileged
121    # runas.
122    if not impersonation_token:
123        log.debug("No impersonation token, using unprivileged runas")
124        return runas_unpriv(cmdLine, username, password, cwd)
125
126    if domain == "NT AUTHORITY":
127        # Logon as a system level account, SYSTEM, LOCAL SERVICE, or NETWORK
128        # SERVICE.
129        user_token = win32security.LogonUser(
130            username,
131            domain,
132            "",
133            win32con.LOGON32_LOGON_SERVICE,
134            win32con.LOGON32_PROVIDER_DEFAULT,
135        )
136    elif password:
137        # Login with a password.
138        user_token = win32security.LogonUser(
139            username,
140            domain,
141            password,
142            win32con.LOGON32_LOGON_INTERACTIVE,
143            win32con.LOGON32_PROVIDER_DEFAULT,
144        )
145    else:
146        # Login without a password. This always returns an elevated token.
147        user_token = salt.platform.win.logon_msv1_s4u(username).Token
148
149    # Get a linked user token to elevate if needed
150    elevation_type = win32security.GetTokenInformation(
151        user_token, win32security.TokenElevationType
152    )
153    if elevation_type > 1:
154        user_token = win32security.GetTokenInformation(
155            user_token, win32security.TokenLinkedToken
156        )
157
158    # Elevate the user token
159    salt.platform.win.elevate_token(user_token)
160
161    # Make sure the user's token has access to a windows station and desktop
162    salt.platform.win.grant_winsta_and_desktop(user_token)
163
164    # Create pipes for standard in, out and error streams
165    security_attributes = win32security.SECURITY_ATTRIBUTES()
166    security_attributes.bInheritHandle = 1
167
168    stdin_read, stdin_write = win32pipe.CreatePipe(security_attributes, 0)
169    stdin_read = salt.platform.win.make_inheritable(stdin_read)
170
171    stdout_read, stdout_write = win32pipe.CreatePipe(security_attributes, 0)
172    stdout_write = salt.platform.win.make_inheritable(stdout_write)
173
174    stderr_read, stderr_write = win32pipe.CreatePipe(security_attributes, 0)
175    stderr_write = salt.platform.win.make_inheritable(stderr_write)
176
177    # Run the process without showing a window.
178    creationflags = (
179        win32process.CREATE_NO_WINDOW
180        | win32process.CREATE_NEW_CONSOLE
181        | win32process.CREATE_SUSPENDED
182    )
183
184    startup_info = salt.platform.win.STARTUPINFO(
185        dwFlags=win32con.STARTF_USESTDHANDLES,
186        hStdInput=stdin_read.handle,
187        hStdOutput=stdout_write.handle,
188        hStdError=stderr_write.handle,
189    )
190
191    # Create the environment for the user
192    env = create_env(user_token, False)
193
194    hProcess = None
195    try:
196        # Start the process in a suspended state.
197        process_info = salt.platform.win.CreateProcessWithTokenW(
198            int(user_token),
199            logonflags=1,
200            applicationname=None,
201            commandline=cmdLine,
202            currentdirectory=cwd,
203            creationflags=creationflags,
204            startupinfo=startup_info,
205            environment=env,
206        )
207
208        hProcess = process_info.hProcess
209        hThread = process_info.hThread
210        dwProcessId = process_info.dwProcessId
211        dwThreadId = process_info.dwThreadId
212
213        # We don't use these so let's close the handle
214        salt.platform.win.kernel32.CloseHandle(stdin_write.handle)
215        salt.platform.win.kernel32.CloseHandle(stdout_write.handle)
216        salt.platform.win.kernel32.CloseHandle(stderr_write.handle)
217
218        ret = {"pid": dwProcessId}
219        # Resume the process
220        psutil.Process(dwProcessId).resume()
221
222        # Wait for the process to exit and get its return code.
223        if (
224            win32event.WaitForSingleObject(hProcess, win32event.INFINITE)
225            == win32con.WAIT_OBJECT_0
226        ):
227            exitcode = win32process.GetExitCodeProcess(hProcess)
228            ret["retcode"] = exitcode
229
230        # Read standard out
231        fd_out = msvcrt.open_osfhandle(stdout_read.handle, os.O_RDONLY | os.O_TEXT)
232        with os.fdopen(fd_out, "r") as f_out:
233            stdout = f_out.read()
234            ret["stdout"] = stdout
235
236        # Read standard error
237        fd_err = msvcrt.open_osfhandle(stderr_read.handle, os.O_RDONLY | os.O_TEXT)
238        with os.fdopen(fd_err, "r") as f_err:
239            stderr = f_err.read()
240            ret["stderr"] = stderr
241    finally:
242        if hProcess is not None:
243            salt.platform.win.kernel32.CloseHandle(hProcess)
244        win32api.CloseHandle(th)
245        win32api.CloseHandle(user_token)
246        if impersonation_token:
247            win32security.RevertToSelf()
248        win32api.CloseHandle(impersonation_token)
249
250    return ret
251
252
253def runas_unpriv(cmd, username, password, cwd=None):
254    """
255    Runas that works for non-privileged users
256    """
257    # Validate the domain and sid exist for the username
258    username, domain = split_username(username)
259    try:
260        _, domain, _ = win32security.LookupAccountName(domain, username)
261    except pywintypes.error as exc:
262        message = win32api.FormatMessage(exc.winerror).rstrip("\n")
263        raise CommandExecutionError(message)
264
265    # Create a pipe to set as stdout in the child. The write handle needs to be
266    # inheritable.
267    c2pread, c2pwrite = salt.platform.win.CreatePipe(
268        inherit_read=False,
269        inherit_write=True,
270    )
271    errread, errwrite = salt.platform.win.CreatePipe(
272        inherit_read=False,
273        inherit_write=True,
274    )
275
276    # Create inheritable copy of the stdin
277    stdin = salt.platform.win.kernel32.GetStdHandle(
278        salt.platform.win.STD_INPUT_HANDLE,
279    )
280    dupin = salt.platform.win.DuplicateHandle(srchandle=stdin, inherit=True)
281
282    # Get startup info structure
283    startup_info = salt.platform.win.STARTUPINFO(
284        dwFlags=win32con.STARTF_USESTDHANDLES,
285        hStdInput=dupin,
286        hStdOutput=c2pwrite,
287        hStdError=errwrite,
288    )
289
290    try:
291        # Run command and return process info structure
292        process_info = salt.platform.win.CreateProcessWithLogonW(
293            username=username,
294            domain=domain,
295            password=password,
296            logonflags=salt.platform.win.LOGON_WITH_PROFILE,
297            commandline=cmd,
298            startupinfo=startup_info,
299            currentdirectory=cwd,
300        )
301        salt.platform.win.kernel32.CloseHandle(process_info.hThread)
302    finally:
303        salt.platform.win.kernel32.CloseHandle(dupin)
304        salt.platform.win.kernel32.CloseHandle(c2pwrite)
305        salt.platform.win.kernel32.CloseHandle(errwrite)
306
307    # Initialize ret and set first element
308    ret = {"pid": process_info.dwProcessId}
309
310    # Get Standard Out
311    fd_out = msvcrt.open_osfhandle(c2pread, os.O_RDONLY | os.O_TEXT)
312    with os.fdopen(fd_out, "r") as f_out:
313        ret["stdout"] = f_out.read()
314
315    # Get Standard Error
316    fd_err = msvcrt.open_osfhandle(errread, os.O_RDONLY | os.O_TEXT)
317    with os.fdopen(fd_err, "r") as f_err:
318        ret["stderr"] = f_err.read()
319
320    # Get Return Code
321    if (
322        salt.platform.win.kernel32.WaitForSingleObject(
323            process_info.hProcess, win32event.INFINITE
324        )
325        == win32con.WAIT_OBJECT_0
326    ):
327        exitcode = salt.platform.win.wintypes.DWORD()
328        salt.platform.win.kernel32.GetExitCodeProcess(
329            process_info.hProcess, ctypes.byref(exitcode)
330        )
331        ret["retcode"] = exitcode.value
332
333    # Close handle to process
334    salt.platform.win.kernel32.CloseHandle(process_info.hProcess)
335
336    return ret
337