Merge pull request #47621 from dwoz/win_runas

Runas any user even when shell is limited like winrm
This commit is contained in:
Nicole Thomas 2018-08-05 14:29:50 -04:00 committed by GitHub
commit a9daa92d73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 2020 additions and 371 deletions

View File

@ -895,3 +895,15 @@ particular Salt job. The JID will be included using the default format,
.. code-block:: yaml
log_fmt_jid: "[JID: %(jid)s]"
Security
========
Windows runas changes
---------------------
A password is no longer required with ``runas`` under normal circumstances.
The password option is only needed if the minion process is run under a
restricted (non-administrator) account. In the aforementioned case, a password
is only required when using the ``runas`` argument to run command as a
different user.

View File

@ -397,10 +397,6 @@ def _run(cmd,
log.info(log_callback(msg))
if runas and salt.utils.platform.is_windows():
if not password:
msg = 'password is a required argument for runas on Windows'
raise CommandExecutionError(msg)
if not HAS_WIN_RUNAS:
msg = 'missing salt/utils/win_runas.py'
raise CommandExecutionError(msg)
@ -966,15 +962,14 @@ def run(cmd,
cases where sensitive information must be read from standard input.
:param str runas: Specify an alternate user to run the command. The default
behavior is to run as the user under which Salt is running. If running
on a Windows minion you must also use the ``password`` argument, and
the target user account must be in the Administrators group.
behavior is to run as the user under which Salt is running.
:param str group: Group to run command as. Not currently supported
on Windows.
on Windows.
:param str password: Windows only. Required when specifying ``runas``. This
parameter will be ignored on non-Windows platforms.
:param str password: Windows only. Only required when the minion proccess
is running under a non-privileged account. This parameter will be
ignored on non-Windows platforms.
.. versionadded:: 2016.3.0

View File

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

1196
salt/platform/win.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,34 +1,35 @@
# -*- coding: utf-8 -*-
'''
Run processes as a different user in Windows
Based on a solution from http://stackoverflow.com/questions/29566330
'''
from __future__ import absolute_import, print_function, unicode_literals
from __future__ import absolute_import, unicode_literals
# Import Python Libraries
import ctypes
import os
import logging
# Import Third Party Libs
try:
import win32con
import psutil
HAS_PSUTIL = True
except ImportError:
HAS_PSUTIL = False
try:
import win32api
import win32con
import win32process
import win32security
import win32pipe
import win32event
import win32profile
import msvcrt
import ctypes
import winerror
import salt.utils.win_functions
from ctypes import wintypes
import salt.platform.win
HAS_WIN32 = True
except ImportError:
HAS_WIN32 = False
# Set up logging
log = logging.getLogger(__name__)
@ -38,405 +39,231 @@ def __virtual__():
'''
Only load if Win32 Libraries are installed
'''
if not HAS_WIN32:
return False, 'This utility requires pywin32'
if not HAS_WIN32 or not HAS_PSUTIL:
return False, 'This utility requires pywin32 and psutil'
return 'win_runas'
if HAS_WIN32:
# ctypes definitions
kernel32 = ctypes.WinDLL(str('kernel32')) # future lint: disable=blacklisted-function
advapi32 = ctypes.WinDLL(str('advapi32')) # future lint: disable=blacklisted-function
INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value
INVALID_DWORD_VALUE = wintypes.DWORD(-1).value # ~WinAPI
INFINITE = INVALID_DWORD_VALUE
LOGON_WITH_PROFILE = 0x00000001
STD_INPUT_HANDLE = wintypes.DWORD(-10).value
STD_OUTPUT_HANDLE = wintypes.DWORD(-11).value
STD_ERROR_HANDLE = wintypes.DWORD(-12).value
class SECURITY_ATTRIBUTES(ctypes.Structure):
_fields_ = (('nLength', wintypes.DWORD),
('lpSecurityDescriptor', wintypes.LPVOID),
('bInheritHandle', wintypes.BOOL))
def __init__(self, **kwds):
self.nLength = ctypes.sizeof(self)
super(SECURITY_ATTRIBUTES, self).__init__(**kwds)
LPSECURITY_ATTRIBUTES = ctypes.POINTER(SECURITY_ATTRIBUTES)
LPBYTE = ctypes.POINTER(wintypes.BYTE)
LPHANDLE = PHANDLE = ctypes.POINTER(ctypes.c_void_p)
LPDWORD = ctypes.POINTER(ctypes.c_ulong)
class STARTUPINFO(ctypes.Structure):
"""https://msdn.microsoft.com/en-us/library/ms686331"""
_fields_ = (('cb', wintypes.DWORD),
('lpReserved', wintypes.LPWSTR),
('lpDesktop', wintypes.LPWSTR),
('lpTitle', wintypes.LPWSTR),
('dwX', wintypes.DWORD),
('dwY', wintypes.DWORD),
('dwXSize', wintypes.DWORD),
('dwYSize', wintypes.DWORD),
('dwXCountChars', wintypes.DWORD),
('dwYCountChars', wintypes.DWORD),
('dwFillAttribute', wintypes.DWORD),
('dwFlags', wintypes.DWORD),
('wShowWindow', wintypes.WORD),
('cbReserved2', wintypes.WORD),
('lpReserved2', LPBYTE),
('hStdInput', wintypes.HANDLE),
('hStdOutput', wintypes.HANDLE),
('hStdError', wintypes.HANDLE))
def __init__(self, **kwds):
self.cb = ctypes.sizeof(self)
super(STARTUPINFO, self).__init__(**kwds)
if HAS_WIN32:
LPSTARTUPINFO = ctypes.POINTER(STARTUPINFO)
class PROC_THREAD_ATTRIBUTE_LIST(ctypes.Structure):
pass
PPROC_THREAD_ATTRIBUTE_LIST = ctypes.POINTER(PROC_THREAD_ATTRIBUTE_LIST)
class STARTUPINFOEX(STARTUPINFO):
_fields_ = (('lpAttributeList', PPROC_THREAD_ATTRIBUTE_LIST),)
LPSTARTUPINFOEX = ctypes.POINTER(STARTUPINFOEX)
class PROCESS_INFORMATION(ctypes.Structure):
"""https://msdn.microsoft.com/en-us/library/ms684873"""
_fields_ = (('hProcess', wintypes.HANDLE),
('hThread', wintypes.HANDLE),
('dwProcessId', wintypes.DWORD),
('dwThreadId', wintypes.DWORD))
LPPROCESS_INFORMATION = ctypes.POINTER(PROCESS_INFORMATION)
class HANDLE_IHV(wintypes.HANDLE):
pass
def errcheck_ihv(result, func, args):
if result.value == INVALID_HANDLE_VALUE:
raise ctypes.WinError()
return result.value
class DWORD_IDV(wintypes.DWORD):
pass
def errcheck_idv(result, func, args):
if result.value == INVALID_DWORD_VALUE:
raise ctypes.WinError()
return result.value
def errcheck_bool(result, func, args):
if not result:
raise ctypes.WinError()
return args
def _win(func, restype, *argtypes):
func.restype = restype
func.argtypes = argtypes
if issubclass(restype, HANDLE_IHV):
func.errcheck = errcheck_ihv
elif issubclass(restype, DWORD_IDV):
func.errcheck = errcheck_idv
else:
func.errcheck = errcheck_bool
# https://msdn.microsoft.com/en-us/library/ms687032
_win(kernel32.WaitForSingleObject, DWORD_IDV,
wintypes.HANDLE, # _In_ hHandle
wintypes.DWORD) # _In_ dwMilliseconds
# https://msdn.microsoft.com/en-us/library/ms683231
_win(kernel32.GetStdHandle, HANDLE_IHV,
wintypes.DWORD) # _In_ nStdHandle
# https://msdn.microsoft.com/en-us/library/ms724211
_win(kernel32.CloseHandle, wintypes.BOOL,
wintypes.HANDLE) # _In_ hObject
# https://msdn.microsoft.com/en-us/library/ms724935
_win(kernel32.SetHandleInformation, wintypes.BOOL,
wintypes.HANDLE, # _In_ hObject
wintypes.DWORD, # _In_ dwMask
wintypes.DWORD) # _In_ dwFlags
# https://msdn.microsoft.com/en-us/library/ms724251
_win(kernel32.DuplicateHandle, wintypes.BOOL,
wintypes.HANDLE, # _In_ hSourceProcessHandle,
wintypes.HANDLE, # _In_ hSourceHandle,
wintypes.HANDLE, # _In_ hTargetProcessHandle,
LPHANDLE, # _Out_ lpTargetHandle,
wintypes.DWORD, # _In_ dwDesiredAccess,
wintypes.BOOL, # _In_ bInheritHandle,
wintypes.DWORD) # _In_ dwOptions
# https://msdn.microsoft.com/en-us/library/ms683179
_win(kernel32.GetCurrentProcess, wintypes.HANDLE)
# https://msdn.microsoft.com/en-us/library/ms683189
_win(kernel32.GetExitCodeProcess, wintypes.BOOL,
wintypes.HANDLE, # _In_ hProcess,
LPDWORD) # _Out_ lpExitCode
# https://msdn.microsoft.com/en-us/library/aa365152
_win(kernel32.CreatePipe, wintypes.BOOL,
PHANDLE, # _Out_ hReadPipe,
PHANDLE, # _Out_ hWritePipe,
LPSECURITY_ATTRIBUTES, # _In_opt_ lpPipeAttributes,
wintypes.DWORD) # _In_ nSize
# https://msdn.microsoft.com/en-us/library/ms682431
_win(advapi32.CreateProcessWithLogonW, wintypes.BOOL,
wintypes.LPCWSTR, # _In_ lpUsername
wintypes.LPCWSTR, # _In_opt_ lpDomain
wintypes.LPCWSTR, # _In_ lpPassword
wintypes.DWORD, # _In_ dwLogonFlags
wintypes.LPCWSTR, # _In_opt_ lpApplicationName
wintypes.LPWSTR, # _Inout_opt_ lpCommandLine
wintypes.DWORD, # _In_ dwCreationFlags
wintypes.LPCWSTR, # _In_opt_ lpEnvironment
wintypes.LPCWSTR, # _In_opt_ lpCurrentDirectory
LPSTARTUPINFO, # _In_ lpStartupInfo
LPPROCESS_INFORMATION) # _Out_ lpProcessInformation
# High-level wrappers
def DuplicateHandle(hsrc=kernel32.GetCurrentProcess(),
srchandle=kernel32.GetCurrentProcess(),
htgt=kernel32.GetCurrentProcess(),
access=0, inherit=False,
options=win32con.DUPLICATE_SAME_ACCESS):
tgthandle = wintypes.HANDLE()
kernel32.DuplicateHandle(hsrc, srchandle,
htgt, ctypes.byref(tgthandle),
access, inherit, options)
return tgthandle.value
def CreatePipe(inherit_read=False, inherit_write=False):
read, write = wintypes.HANDLE(), wintypes.HANDLE()
kernel32.CreatePipe(ctypes.byref(read), ctypes.byref(write), None, 0)
if inherit_read:
kernel32.SetHandleInformation(read, win32con.HANDLE_FLAG_INHERIT,
win32con.HANDLE_FLAG_INHERIT)
if inherit_write:
kernel32.SetHandleInformation(write, win32con.HANDLE_FLAG_INHERIT,
win32con.HANDLE_FLAG_INHERIT)
return read.value, write.value
def CreateProcessWithLogonW(username=None,
domain=None,
password=None,
logonflags=0,
applicationname=None,
commandline=None,
creationflags=0,
environment=None,
currentdirectory=None,
startupinfo=None):
creationflags |= win32con.CREATE_UNICODE_ENVIRONMENT
if commandline is not None:
commandline = ctypes.create_unicode_buffer(commandline)
if startupinfo is None:
startupinfo = STARTUPINFO()
process_info = PROCESS_INFORMATION()
advapi32.CreateProcessWithLogonW(username,
domain,
password,
logonflags,
applicationname,
commandline,
creationflags,
environment,
currentdirectory,
ctypes.byref(startupinfo),
ctypes.byref(process_info))
return process_info
def make_inheritable(token):
return win32api.DuplicateHandle(win32api.GetCurrentProcess(),
token,
win32api.GetCurrentProcess(),
0,
1,
win32con.DUPLICATE_SAME_ACCESS)
def runas_system(cmd, username, password):
# This only works as system, when salt is running as a service for example
# Check for a domain
def split_username(username):
# TODO: Is there a windows api for this?
domain = '.'
if '@' in username:
username, domain = username.split('@')
if '\\' in username:
domain, username = username.split('\\')
return username, domain
# Load User and Get Token
token = win32security.LogonUser(username,
domain,
password,
win32con.LOGON32_LOGON_INTERACTIVE,
win32con.LOGON32_PROVIDER_DEFAULT)
# Load the User Profile
handle_reg = win32profile.LoadUserProfile(token, {'UserName': username})
def runas(cmdLine, username, password=None, cwd=None):
'''
Run a command as another user. It the proccess is running as an admin or
system account this method does not require a password. Other non
priviledged accounts need to provide a password for the user to runas.
Commands are run in with the highest level priviledges possible for the
account provided.
'''
# Elevate the token from the current process
access = (
win32security.TOKEN_QUERY |
win32security.TOKEN_ADJUST_PRIVILEGES
)
th = win32security.OpenProcessToken(win32api.GetCurrentProcess(), access)
salt.platform.win.elevate_token(th)
# Try to impersonate the SYSTEM user. This process needs to be runnung as a
# user who as been granted the SeImpersonatePrivilege, Administrator
# accounts have this permission by default.
try:
# Get Unrestricted Token (UAC) if this is an Admin Account
elevated_token = win32security.GetTokenInformation(
token, win32security.TokenLinkedToken)
impersonation_token = salt.platform.win.impersonate_sid(
salt.platform.win.SYSTEM_SID,
session_id=0,
privs=['SeTcbPrivilege'],
)
except WindowsError: # pylint: disable=undefined-variable
log.debug("Unable to impersonate SYSTEM user")
impersonation_token = None
# Get list of privileges this token contains
privileges = win32security.GetTokenInformation(
elevated_token, win32security.TokenPrivileges)
# Impersonation of the SYSTEM user failed. Fallback to an un-priviledged
# runas.
if not impersonation_token:
log.debug("No impersonation token, using unprivileged runas")
return runas_unpriv(cmdLine, username, password, cwd)
# Create a set of all privileges to be enabled
enable_privs = set()
for luid, flags in privileges:
enable_privs.add((luid, win32con.SE_PRIVILEGE_ENABLED))
username, domain = split_username(username)
# Validate the domain and sid exist for the username
sid, domain, sidType = win32security.LookupAccountName(domain, username)
if domain == 'NT AUTHORITY':
# Logon as a system level account, SYSTEM, LOCAL SERVICE, or NETWORK
# SERVICE.
logonType = win32con.LOGON32_LOGON_SERVICE
user_token = win32security.LogonUser(
username,
domain,
'',
win32con.LOGON32_LOGON_SERVICE,
win32con.LOGON32_PROVIDER_DEFAULT,
)
elif password:
# Login with a password.
user_token = win32security.LogonUser(
username,
domain,
password,
win32con.LOGON32_LOGON_INTERACTIVE,
win32con.LOGON32_PROVIDER_DEFAULT,
)
else:
# Login without a password. This always returns an elevated token.
user_token = salt.platform.win.logon_msv1_s4u(username).Token
# Enable the privileges
win32security.AdjustTokenPrivileges(elevated_token, 0, enable_privs)
# Get a linked user token to elevate if needed
elevation_type = win32security.GetTokenInformation(
user_token, win32security.TokenElevationType
)
if elevation_type > 1:
user_token = win32security.GetTokenInformation(
user_token,
win32security.TokenLinkedToken
)
except win32security.error as exc:
# User doesn't have admin, use existing token
if exc[0] == winerror.ERROR_NO_SUCH_LOGON_SESSION \
or exc[0] == winerror.ERROR_PRIVILEGE_NOT_HELD:
elevated_token = token
else:
raise
# Elevate the user token
salt.platform.win.elevate_token(user_token)
# Get Security Attributes
# Make sure the user's profile is loaded.
handle_reg = win32profile.LoadUserProfile(user_token, {'UserName': username})
# Make sure the user's token has access to a windows station and desktop
salt.platform.win.grant_winsta_and_desktop(user_token)
# Create pipes for standard in, out and error streams
security_attributes = win32security.SECURITY_ATTRIBUTES()
security_attributes.bInheritHandle = 1
# Create a pipe to set as stdout in the child. The write handle needs to be
# inheritable.
stdin_read, stdin_write = win32pipe.CreatePipe(security_attributes, 0)
stdin_read = make_inheritable(stdin_read)
stdin_read = salt.platform.win.make_inheritable(stdin_read)
stdout_read, stdout_write = win32pipe.CreatePipe(security_attributes, 0)
stdout_write = make_inheritable(stdout_write)
stdout_write = salt.platform.win.make_inheritable(stdout_write)
stderr_read, stderr_write = win32pipe.CreatePipe(security_attributes, 0)
stderr_write = make_inheritable(stderr_write)
stderr_write = salt.platform.win.make_inheritable(stderr_write)
# Get startup info structure
startup_info = win32process.STARTUPINFO()
startup_info.dwFlags = win32con.STARTF_USESTDHANDLES
startup_info.hStdInput = stdin_read
startup_info.hStdOutput = stdout_write
startup_info.hStdError = stderr_write
# Run the process without showing a window.
creationflags = (
win32process.CREATE_NO_WINDOW |
win32process.CREATE_NEW_CONSOLE |
win32process.CREATE_SUSPENDED
)
# Get User Environment
user_environment = win32profile.CreateEnvironmentBlock(token, False)
startup_info = salt.platform.win.STARTUPINFO(
dwFlags=win32con.STARTF_USESTDHANDLES,
hStdInput=stdin_read.handle,
hStdOutput=stdout_write.handle,
hStdError=stderr_write.handle,
)
# Build command
cmd = 'cmd /c {0}'.format(cmd)
# Create the environment for the user
env = win32profile.CreateEnvironmentBlock(user_token, False)
# Run command and return process info structure
procArgs = (None,
cmd,
security_attributes,
security_attributes,
1,
0,
user_environment,
None,
startup_info)
# Start the process in a suspended state.
process_info = salt.platform.win.CreateProcessWithTokenW(
int(user_token),
logonflags=1,
applicationname=None,
commandline=cmdLine,
currentdirectory=cwd,
creationflags=creationflags,
startupinfo=startup_info,
environment=env,
)
hProcess, hThread, PId, TId = \
win32process.CreateProcessAsUser(elevated_token, *procArgs)
hProcess = process_info.hProcess
hThread = process_info.hThread
dwProcessId = process_info.dwProcessId
dwThreadId = process_info.dwThreadId
if stdin_read is not None:
stdin_read.Close()
if stdout_write is not None:
stdout_write.Close()
if stderr_write is not None:
stderr_write.Close()
hThread.Close()
salt.platform.win.kernel32.CloseHandle(stdin_write.handle)
salt.platform.win.kernel32.CloseHandle(stdout_write.handle)
salt.platform.win.kernel32.CloseHandle(stderr_write.handle)
# Initialize ret and set first element
ret = {'pid': PId}
ret = {'pid': dwProcessId}
# Resume the process
psutil.Process(dwProcessId).resume()
# Get Standard Out
fd_out = msvcrt.open_osfhandle(stdout_read, os.O_RDONLY | os.O_TEXT)
with os.fdopen(fd_out, 'r') as f_out:
ret['stdout'] = f_out.read()
# Get Standard Error
fd_err = msvcrt.open_osfhandle(stderr_read, os.O_RDONLY | os.O_TEXT)
with os.fdopen(fd_err, 'r') as f_err:
ret['stderr'] = f_err.read()
# Get Return Code
# Wait for the process to exit and get it's return code.
if win32event.WaitForSingleObject(hProcess, win32event.INFINITE) == win32con.WAIT_OBJECT_0:
exitcode = win32process.GetExitCodeProcess(hProcess)
ret['retcode'] = exitcode
# Close handle to process
win32api.CloseHandle(hProcess)
# Read standard out
fd_out = msvcrt.open_osfhandle(stdout_read.handle, os.O_RDONLY | os.O_TEXT)
with os.fdopen(fd_out, 'r') as f_out:
stdout = f_out.read()
ret['stdout'] = stdout
# Unload the User Profile
win32profile.UnloadUserProfile(token, handle_reg)
# Read standard error
fd_err = msvcrt.open_osfhandle(stderr_read.handle, os.O_RDONLY | os.O_TEXT)
with os.fdopen(fd_err, 'r') as f_err:
stderr = f_err.read()
ret['stderr'] = stderr
win32profile.UnloadUserProfile(user_token, handle_reg)
salt.platform.win.kernel32.CloseHandle(hProcess)
win32api.CloseHandle(user_token)
if impersonation_token:
win32security.RevertToSelf()
win32api.CloseHandle(impersonation_token)
return ret
def runas(cmd, username, password, cwd=None):
# This only works when not running under the system account
# Debug mode for example
if salt.utils.win_functions.get_current_user() == 'SYSTEM':
return runas_system(cmd, username, password)
def runas_unpriv(cmd, username, password, cwd=None):
'''
Runas that works for non-priviledged users
'''
# Create a pipe to set as stdout in the child. The write handle needs to be
# inheritable.
c2pread, c2pwrite = CreatePipe(inherit_read=False, inherit_write=True)
errread, errwrite = CreatePipe(inherit_read=False, inherit_write=True)
c2pread, c2pwrite = salt.platform.win.CreatePipe(
inherit_read=False, inherit_write=True,
)
errread, errwrite = salt.platform.win.CreatePipe(
inherit_read=False, inherit_write=True,
)
# Create inheritable copy of the stdin
stdin = kernel32.GetStdHandle(STD_INPUT_HANDLE)
dupin = DuplicateHandle(srchandle=stdin, inherit=True)
stdin = salt.platform.win.kernel32.GetStdHandle(
salt.platform.win.STD_INPUT_HANDLE,
)
dupin = salt.platform.win.DuplicateHandle(srchandle=stdin, inherit=True)
# Get startup info structure
startup_info = STARTUPINFO(dwFlags=win32con.STARTF_USESTDHANDLES,
hStdInput=dupin,
hStdOutput=c2pwrite,
hStdError=errwrite)
startup_info = salt.platform.win.STARTUPINFO(
dwFlags=win32con.STARTF_USESTDHANDLES,
hStdInput=dupin,
hStdOutput=c2pwrite,
hStdError=errwrite,
)
# Build command
cmd = 'cmd /c {0}'.format(cmd)
# Check for a domain
domain = None
if '@' in username:
username, domain = username.split('@')
if '\\' in username:
domain, username = username.split('\\')
username, domain = split_username(username)
# Run command and return process info structure
process_info = CreateProcessWithLogonW(username=username,
domain=domain,
password=password,
logonflags=LOGON_WITH_PROFILE,
commandline=cmd,
startupinfo=startup_info,
currentdirectory=cwd)
process_info = salt.platform.win.CreateProcessWithLogonW(
username=username,
domain=domain,
password=password,
logonflags=salt.platform.win.LOGON_WITH_PROFILE,
commandline=cmd,
startupinfo=startup_info,
currentdirectory=cwd)
kernel32.CloseHandle(dupin)
kernel32.CloseHandle(c2pwrite)
kernel32.CloseHandle(errwrite)
kernel32.CloseHandle(process_info.hThread)
salt.platform.win.kernel32.CloseHandle(dupin)
salt.platform.win.kernel32.CloseHandle(c2pwrite)
salt.platform.win.kernel32.CloseHandle(errwrite)
salt.platform.win.kernel32.CloseHandle(process_info.hThread)
# Initialize ret and set first element
ret = {'pid': process_info.dwProcessId}
@ -452,14 +279,14 @@ def runas(cmd, username, password, cwd=None):
ret['stderr'] = f_err.read()
# Get Return Code
if kernel32.WaitForSingleObject(process_info.hProcess, INFINITE) == \
if salt.platform.win.kernel32.WaitForSingleObject(process_info.hProcess, win32event.INFINITE) == \
win32con.WAIT_OBJECT_0:
exitcode = wintypes.DWORD()
kernel32.GetExitCodeProcess(process_info.hProcess,
exitcode = salt.platform.win.wintypes.DWORD()
salt.platform.win.kernel32.GetExitCodeProcess(process_info.hProcess,
ctypes.byref(exitcode))
ret['retcode'] = exitcode.value
# Close handle to process
kernel32.CloseHandle(process_info.hProcess)
salt.platform.win.kernel32.CloseHandle(process_info.hProcess)
return ret

View File

@ -0,0 +1,618 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import textwrap
import subprocess
import socket
import inspect
import io
# Service manager imports
import sys
import os
import logging
import time
import threading
import win32service
import win32serviceutil
import win32event
import servicemanager
import traceback
import time
import yaml
from tests.support.case import ModuleCase
from tests.support.paths import CODE_DIR
from tests.support.helpers import (
with_system_user,
)
import salt.utils.win_runas
import salt.ext.six
try:
import win32api
CODE_DIR = win32api.GetLongPathName(CODE_DIR)
except ImportError:
pass
logger = logging.getLogger(__name__)
PASSWORD = 'P@ssW0rd'
NOPRIV_STDERR = 'ERROR: Logged-on user does not have administrative privilege.\n'
PRIV_STDOUT = (
'\nINFO: The system global flag \'maintain objects list\' needs\n '
'to be enabled to see local opened files.\n See Openfiles '
'/? for more information.\n\n\nFiles opened remotely via local share '
'points:\n---------------------------------------------\n\n'
'INFO: No shared open files found.\n'
)
RUNAS_PATH = os.path.abspath(os.path.join(CODE_DIR, 'runas.py'))
RUNAS_OUT = os.path.abspath(os.path.join(CODE_DIR, 'runas.out'))
def default_target(service, *args, **kwargs):
while service.active:
time.sleep(service.timeout)
class _ServiceManager(win32serviceutil.ServiceFramework):
'''
A windows service manager
'''
_svc_name_ = "Service Manager"
_svc_display_name_ = "Service Manager"
_svc_description_ = "A Service Manager"
run_in_foreground = False
target = default_target
def __init__(self, args, target=None, timeout=60, active=True):
win32serviceutil.ServiceFramework.__init__(self, args)
self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
self.timeout = timeout
self.active = active
if target is not None:
self.target = target
@classmethod
def log_error(cls, msg):
if cls.run_in_foreground:
logger.error(msg)
servicemanager.LogErrorMsg(msg)
@classmethod
def log_info(cls, msg):
if cls.run_in_foreground:
logger.info(msg)
servicemanager.LogInfoMsg(msg)
@classmethod
def log_exception(cls, msg):
if cls.run_in_foreground:
logger.exception(msg)
exc_info = sys.exc_info()
tb = traceback.format_tb(exc_info[2])
servicemanager.LogErrorMsg("{} {} {}".format(msg, exc_info[1], tb))
@property
def timeout_ms(self):
return self.timeout * 1000
def SvcStop(self):
"""
Stop the service by; terminating any subprocess call, notify
windows internals of the stop event, set the instance's active
attribute to 'False' so the run loops stop.
"""
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
win32event.SetEvent(self.hWaitStop)
self.active = False
def SvcDoRun(self):
"""
Run the monitor in a separete thread so the main thread is
free to react to events sent to the windows service.
"""
servicemanager.LogMsg(
servicemanager.EVENTLOG_INFORMATION_TYPE,
servicemanager.PYS_SERVICE_STARTED,
(self._svc_name_, ''),
)
self.log_info("Starting Service {}".format(self._svc_name_))
monitor_thread = threading.Thread(target=self.target_thread)
monitor_thread.start()
while self.active:
rc = win32event.WaitForSingleObject(self.hWaitStop, self.timeout_ms)
if rc == win32event.WAIT_OBJECT_0:
# Stop signal encountered
self.log_info("Stopping Service")
break
if not monitor_thread.is_alive():
self.log_info("Update Thread Died, Stopping Service")
break
def target_thread(self, *args, **kwargs):
"""
Target Thread, handles any exception in the target method and
logs them.
"""
self.log_info("Monitor")
try:
self.target(self, *args, **kwargs)
except Exception as exc:
# TODO: Add traceback info to windows event log objects
self.log_exception("Exception In Target")
@classmethod
def install(cls, username=None, password=None, start_type=None):
if hasattr(cls, '_svc_reg_class_'):
svc_class = cls._svc_reg_class_
else:
svc_class = win32serviceutil.GetServiceClassString(cls)
win32serviceutil.InstallService(
svc_class,
cls._svc_name_,
cls._svc_display_name_,
description=cls._svc_description_,
userName=username,
password=password,
startType=start_type,
)
@classmethod
def remove(cls):
win32serviceutil.RemoveService(
cls._svc_name_
)
@classmethod
def start(cls):
win32serviceutil.StartService(
cls._svc_name_
)
@classmethod
def restart(cls):
win32serviceutil.RestartService(
cls._svc_name_
)
@classmethod
def stop(cls):
win32serviceutil.StopService(
cls._svc_name_
)
def service_class_factory(cls_name, name, target=default_target, display_name='', description='', run_in_foreground=False):
frm = inspect.stack()[1]
mod = inspect.getmodule(frm[0])
if salt.ext.six.PY2:
cls_name = cls_name.encode()
return type(
cls_name,
(_ServiceManager, object),
{
'__module__': mod.__name__,
'_svc_name_': name,
'_svc_display_name_': display_name or name,
'_svc_description_': description,
'run_in_foreground': run_in_foreground,
'target': target,
},
)
test_service = service_class_factory('test_service', 'test service')
SERVICE_SOURCE = '''
from __future__ import absolute_import, unicode_literals
import logging
logger = logging.getLogger()
logging.basicConfig(level=logging.DEBUG, format="%(message)s")
from tests.integration.utils.test_win_runas import service_class_factory
import salt.utils.win_runas
import sys
import yaml
OUTPUT = {}
USERNAME = '{}'
PASSWORD = '{}'
def target(service, *args, **kwargs):
service.log_info("target start")
if PASSWORD:
ret = salt.utils.win_runas.runas(
'cmd.exe /C OPENFILES',
username=USERNAME,
password=PASSWORD,
)
else:
ret = salt.utils.win_runas.runas(
'cmd.exe /C OPENFILES',
username=USERNAME,
)
service.log_info("win_runas returned %s" % ret)
with open(OUTPUT, 'w') as fp:
yaml.dump(ret, fp)
service.log_info("target stop")
# This class will get imported and run as the service
test_service = service_class_factory('test_service', 'test service', target=target)
if __name__ == '__main__':
try:
test_service.stop()
except Exception as exc:
logger.debug("stop service failed, this is ok.")
try:
test_service.remove()
except Exception as exc:
logger.debug("remove service failed, this os ok.")
test_service.install()
sys.exit(0)
'''
def wait_for_service(name, timeout=200):
start = time.time()
while True:
status = win32serviceutil.QueryServiceStatus(name)
if status[1] == win32service.SERVICE_STOPPED:
break
if time.time() - start > timeout:
raise TimeoutError("Timeout waiting for service") # pylint: disable=undefined-variable
time.sleep(.3)
class RunAsTest(ModuleCase):
@classmethod
def setUpClass(cls):
super(RunAsTest, cls).setUpClass()
cls.hostname = socket.gethostname()
@with_system_user('test-runas', on_existing='delete', delete=True,
password=PASSWORD)
def test_runas(self, username):
ret = salt.utils.win_runas.runas('cmd.exe /C OPENFILES', username, PASSWORD)
self.assertEqual(ret['stdout'], '')
self.assertEqual(ret['stderr'], NOPRIV_STDERR)
self.assertEqual(ret['retcode'], 1)
@with_system_user('test-runas', on_existing='delete', delete=True,
password=PASSWORD)
def test_runas_no_pass(self, username):
ret = salt.utils.win_runas.runas('cmd.exe /C OPENFILES', username)
self.assertEqual(ret['stdout'], '')
self.assertEqual(ret['stderr'], NOPRIV_STDERR)
self.assertEqual(ret['retcode'], 1)
@with_system_user('test-runas-admin', on_existing='delete', delete=True,
password=PASSWORD, groups=['Administrators'])
def test_runas_admin(self, username):
ret = salt.utils.win_runas.runas('cmd.exe /C OPENFILES', username, PASSWORD)
self.assertEqual(ret['stdout'], PRIV_STDOUT)
self.assertEqual(ret['stderr'], '')
self.assertEqual(ret['retcode'], 0)
@with_system_user('test-runas-admin', on_existing='delete', delete=True,
password=PASSWORD, groups=['Administrators'])
def test_runas_admin_no_pass(self, username):
ret = salt.utils.win_runas.runas('cmd.exe /C OPENFILES', username)
self.assertEqual(ret['stdout'], PRIV_STDOUT)
self.assertEqual(ret['stderr'], '')
self.assertEqual(ret['retcode'], 0)
def test_runas_system_user(self):
ret = salt.utils.win_runas.runas('cmd.exe /C OPENFILES', 'SYSTEM')
self.assertEqual(ret['stdout'], PRIV_STDOUT)
self.assertEqual(ret['stderr'], '')
self.assertEqual(ret['retcode'], 0)
def test_runas_network_service(self):
ret = salt.utils.win_runas.runas('cmd.exe /C OPENFILES', 'NETWORK SERVICE')
self.assertEqual(ret['stdout'], '')
self.assertEqual(ret['stderr'], NOPRIV_STDERR)
self.assertEqual(ret['retcode'], 1)
def test_runas_local_service(self):
ret = salt.utils.win_runas.runas('cmd.exe /C OPENFILES', 'LOCAL SERVICE')
self.assertEqual(ret['stdout'], '')
self.assertEqual(ret['stderr'], NOPRIV_STDERR)
self.assertEqual(ret['retcode'], 1)
@with_system_user('test-runas', on_existing='delete', delete=True,
password=PASSWORD)
def test_runas_winrs(self, username):
runaspy = textwrap.dedent('''
import sys
import salt.utils.win_runas
username = '{}'
password = '{}'
sys.exit(salt.utils.win_runas.runas('cmd.exe /C OPENFILES', username, password)['retcode'])
'''.format(username, PASSWORD))
with salt.utils.fopen(RUNAS_PATH, 'w') as fp:
fp.write(runaspy)
ret = subprocess.call("cmd.exe /C winrs /r:{} python {}".format(
self.hostname, RUNAS_PATH), shell=True)
self.assertEqual(ret, 1)
@with_system_user('test-runas', on_existing='delete', delete=True,
password=PASSWORD)
def test_runas_winrs_no_pass(self, username):
runaspy = textwrap.dedent('''
import sys
import salt.utils.win_runas
username = '{}'
sys.exit(salt.utils.win_runas.runas('cmd.exe /C OPENFILES', username)['retcode'])
'''.format(username))
with salt.utils.fopen(RUNAS_PATH, 'w') as fp:
fp.write(runaspy)
ret = subprocess.call("cmd.exe /C winrs /r:{} python {}".format(
self.hostname, RUNAS_PATH), shell=True)
self.assertEqual(ret, 1)
@with_system_user('test-runas-admin', on_existing='delete', delete=True,
password=PASSWORD, groups=['Administrators'])
def test_runas_winrs_admin(self, username):
runaspy = textwrap.dedent('''
import sys
import salt.utils.win_runas
username = '{}'
password = '{}'
sys.exit(salt.utils.win_runas.runas('cmd.exe /C OPENFILES', username, password)['retcode'])
'''.format(username, PASSWORD))
with salt.utils.fopen(RUNAS_PATH, 'w') as fp:
fp.write(runaspy)
ret = subprocess.call("cmd.exe /C winrs /r:{} python {}".format(
self.hostname, RUNAS_PATH), shell=True)
self.assertEqual(ret, 0)
@with_system_user('test-runas-admin', on_existing='delete', delete=True,
password=PASSWORD, groups=['Administrators'])
def test_runas_winrs_admin_no_pass(self, username):
runaspy = textwrap.dedent('''
import sys
import salt.utils.win_runas
username = '{}'
sys.exit(salt.utils.win_runas.runas('cmd.exe /C OPENFILES', username)['retcode'])
'''.format(username))
with salt.utils.fopen(RUNAS_PATH, 'w') as fp:
fp.write(runaspy)
ret = subprocess.call("cmd.exe /C winrs /r:{} python {}".format(
self.hostname, RUNAS_PATH), shell=True)
self.assertEqual(ret, 0)
def test_runas_winrs_system_user(self):
runaspy = textwrap.dedent('''
import sys
import salt.utils.win_runas
sys.exit(salt.utils.win_runas.runas('cmd.exe /C OPENFILES', 'SYSTEM')['retcode'])
''')
with salt.utils.fopen(RUNAS_PATH, 'w') as fp:
fp.write(runaspy)
ret = subprocess.call("cmd.exe /C winrs /r:{} python {}".format(
self.hostname, RUNAS_PATH), shell=True)
self.assertEqual(ret, 0)
def test_runas_winrs_network_service_user(self):
runaspy = textwrap.dedent('''
import sys
import salt.utils.win_runas
sys.exit(salt.utils.win_runas.runas('cmd.exe /C OPENFILES', 'NETWORK SERVICE')['retcode'])
''')
with salt.utils.fopen(RUNAS_PATH, 'w') as fp:
fp.write(runaspy)
ret = subprocess.call("cmd.exe /C winrs /r:{} python {}".format(
self.hostname, RUNAS_PATH), shell=True)
self.assertEqual(ret, 1)
def test_runas_winrs_local_service_user(self):
runaspy = textwrap.dedent('''
import sys
import salt.utils.win_runas
sys.exit(salt.utils.win_runas.runas('cmd.exe /C OPENFILES', 'LOCAL SERVICE')['retcode'])
''')
with salt.utils.fopen(RUNAS_PATH, 'w') as fp:
fp.write(runaspy)
ret = subprocess.call("cmd.exe /C winrs /r:{} python {}".format(
self.hostname, RUNAS_PATH), shell=True)
self.assertEqual(ret, 1)
@with_system_user('test-runas', on_existing='delete', delete=True,
password=PASSWORD)
def test_runas_powershell_remoting(self, username):
psrp_wrap = (
'powershell Invoke-Command -ComputerName {} -ScriptBlock {{ {} }}'
)
runaspy = textwrap.dedent('''
import sys
import salt.utils.win_runas
username = '{}'
password = '{}'
sys.exit(salt.utils.win_runas.runas('cmd.exe /C OPENFILES', username, password)['retcode'])
'''.format(username, PASSWORD))
with salt.utils.fopen(RUNAS_PATH, 'w') as fp:
fp.write(runaspy)
cmd = 'python.exe {}'.format(RUNAS_PATH)
ret = subprocess.call(
psrp_wrap.format(self.hostname, cmd),
shell=True
)
self.assertEqual(ret, 1)
@with_system_user('test-runas', on_existing='delete', delete=True,
password=PASSWORD)
def test_runas_powershell_remoting_no_pass(self, username):
psrp_wrap = (
'powershell Invoke-Command -ComputerName {} -ScriptBlock {{ {} }}'
)
runaspy = textwrap.dedent('''
import sys
import salt.utils.win_runas
username = '{}'
sys.exit(salt.utils.win_runas.runas('cmd.exe /C OPENFILES', username)['retcode'])
'''.format(username))
with salt.utils.fopen(RUNAS_PATH, 'w') as fp:
fp.write(runaspy)
cmd = 'python.exe {}'.format(RUNAS_PATH)
ret = subprocess.call(
psrp_wrap.format(self.hostname, cmd),
shell=True
)
self.assertEqual(ret, 1)
@with_system_user('test-runas-admin', on_existing='delete', delete=True,
password=PASSWORD, groups=['Administrators'])
def test_runas_powershell_remoting_admin(self, username):
psrp_wrap = (
'powershell Invoke-Command -ComputerName {} -ScriptBlock {{ {} }}; exit $LASTEXITCODE'
)
runaspy = textwrap.dedent('''
import sys
import salt.utils.win_runas
username = '{}'
password = '{}'
ret = salt.utils.win_runas.runas('cmd.exe /C OPENFILES', username, password)
sys.exit(ret['retcode'])
'''.format(username, PASSWORD))
with salt.utils.fopen(RUNAS_PATH, 'w') as fp:
fp.write(runaspy)
cmd = 'python.exe {}; exit $LASTEXITCODE'.format(RUNAS_PATH)
ret = subprocess.call(
psrp_wrap.format(self.hostname, cmd),
shell=True
)
self.assertEqual(ret, 0)
@with_system_user('test-runas-admin', on_existing='delete', delete=True,
password=PASSWORD, groups=['Administrators'])
def test_runas_powershell_remoting_admin_no_pass(self, username):
psrp_wrap = (
'powershell Invoke-Command -ComputerName {} -ScriptBlock {{ {} }}; exit $LASTEXITCODE'
)
runaspy = textwrap.dedent('''
import sys
import salt.utils.win_runas
username = '{}'
sys.exit(salt.utils.win_runas.runas('cmd.exe /C OPENFILES', username)['retcode'])
'''.format(username))
with salt.utils.fopen(RUNAS_PATH, 'w') as fp:
fp.write(runaspy)
cmd = 'python.exe {}; exit $LASTEXITCODE'.format(RUNAS_PATH)
ret = subprocess.call(
psrp_wrap.format(self.hostname, cmd),
shell=True
)
self.assertEqual(ret, 0)
@with_system_user('test-runas', on_existing='delete', delete=True,
password=PASSWORD)
def test_runas_service(self, username, timeout=200):
if os.path.exists(RUNAS_OUT):
os.remove(RUNAS_OUT)
assert not os.path.exists(RUNAS_OUT)
runaspy = SERVICE_SOURCE.format(repr(RUNAS_OUT), username, PASSWORD)
with io.open(RUNAS_PATH, 'w', encoding='utf-8') as fp:
fp.write(runaspy)
cmd = 'python.exe {}'.format(RUNAS_PATH)
ret = subprocess.call(
cmd,
shell=True
)
self.assertEqual(ret, 0)
win32serviceutil.StartService('test service')
wait_for_service('test service')
with salt.utils.fopen(RUNAS_OUT, 'r') as fp:
ret = yaml.load(fp)
assert ret['retcode'] == 1, ret
@with_system_user('test-runas', on_existing='delete', delete=True,
password=PASSWORD)
def test_runas_service_no_pass(self, username, timeout=200):
if os.path.exists(RUNAS_OUT):
os.remove(RUNAS_OUT)
assert not os.path.exists(RUNAS_OUT)
runaspy = SERVICE_SOURCE.format(repr(RUNAS_OUT), username, '')
with io.open(RUNAS_PATH, 'w', encoding='utf-8') as fp:
fp.write(runaspy)
cmd = 'python.exe {}'.format(RUNAS_PATH)
ret = subprocess.call(
cmd,
shell=True
)
self.assertEqual(ret, 0)
win32serviceutil.StartService('test service')
wait_for_service('test service')
with salt.utils.fopen(RUNAS_OUT, 'r') as fp:
ret = yaml.load(fp)
assert ret['retcode'] == 1, ret
@with_system_user('test-runas-admin', on_existing='delete', delete=True,
password=PASSWORD, groups=['Administrators'])
def test_runas_service_admin(self, username, timeout=200):
if os.path.exists(RUNAS_OUT):
os.remove(RUNAS_OUT)
assert not os.path.exists(RUNAS_OUT)
runaspy = SERVICE_SOURCE.format(repr(RUNAS_OUT), username, PASSWORD)
with io.open(RUNAS_PATH, 'w', encoding='utf-8') as fp:
fp.write(runaspy)
cmd = 'python.exe {}'.format(RUNAS_PATH)
ret = subprocess.call(
cmd,
shell=True
)
self.assertEqual(ret, 0)
win32serviceutil.StartService('test service')
wait_for_service('test service')
with salt.utils.fopen(RUNAS_OUT, 'r') as fp:
ret = yaml.load(fp)
assert ret['retcode'] == 0, ret
@with_system_user('test-runas-admin', on_existing='delete', delete=True,
password=PASSWORD, groups=['Administrators'])
def test_runas_service_admin_no_pass(self, username, timeout=200):
if os.path.exists(RUNAS_OUT):
os.remove(RUNAS_OUT)
assert not os.path.exists(RUNAS_OUT)
runaspy = SERVICE_SOURCE.format(repr(RUNAS_OUT), username, '')
with io.open(RUNAS_PATH, 'w', encoding='utf-8') as fp:
fp.write(runaspy)
cmd = 'python.exe {}'.format(RUNAS_PATH)
ret = subprocess.call(
cmd,
shell=True
)
self.assertEqual(ret, 0)
win32serviceutil.StartService('test service')
wait_for_service('test service')
with salt.utils.fopen(RUNAS_OUT, 'r') as fp:
ret = yaml.load(fp)
assert ret['retcode'] == 0, ret
def test_runas_service_system_user(self):
if os.path.exists(RUNAS_OUT):
os.remove(RUNAS_OUT)
assert not os.path.exists(RUNAS_OUT)
runaspy = SERVICE_SOURCE.format(repr(RUNAS_OUT), 'SYSTEM', '')
with io.open(RUNAS_PATH, 'w', encoding='utf-8') as fp:
fp.write(runaspy)
cmd = 'python.exe {}'.format(RUNAS_PATH)
ret = subprocess.call(
cmd,
shell=True
)
self.assertEqual(ret, 0)
win32serviceutil.StartService('test service')
wait_for_service('test service')
with salt.utils.fopen(RUNAS_OUT, 'r') as fp:
ret = yaml.load(fp)
assert ret['retcode'] == 0, ret

View File

@ -618,7 +618,7 @@ def requires_network(only_local_network=False):
return decorator
def with_system_user(username, on_existing='delete', delete=True, password=None):
def with_system_user(username, on_existing='delete', delete=True, password=None, groups=None):
'''
Create and optionally destroy a system user to be used within a test
case. The system user is created using the ``user`` salt module.
@ -651,7 +651,7 @@ def with_system_user(username, on_existing='delete', delete=True, password=None)
# Let's add the user to the system.
log.debug('Creating system user {0!r}'.format(username))
kwargs = {'timeout': 60}
kwargs = {'timeout': 60, 'groups': groups}
if salt.utils.platform.is_windows():
kwargs.update({'password': password})
create_user = cls.run_function('user.add', [username], **kwargs)
@ -685,7 +685,7 @@ def with_system_user(username, on_existing='delete', delete=True, password=None)
username
)
)
create_user = cls.run_function('user.add', [username])
create_user = cls.run_function('user.add', [username], **kwargs)
if not create_user:
cls.skipTest(
'A user named {0!r} already existed, was deleted '