summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBryan McLellan <btm@loftninjas.org>2017-07-19 18:19:15 -0400
committerGitHub <noreply@github.com>2017-07-19 18:19:15 -0400
commit1bd6f8ccbacab9a48b72f2900ce94d6f92f277fc (patch)
treec12b1d9f8212da905e501b46bc8a535408d5cdfd
parent2d3ca5cd7bfc119b463eaf3b331296c2e370c2de (diff)
parent57f409a0d49cb50ae4c7598264a90679ae7b9716 (diff)
downloadmixlib-shellout-1bd6f8ccbacab9a48b72f2900ce94d6f92f277fc.tar.gz
Merge pull request #149 from chef/btm/elevated
Added `elevated` option to launch process with elevated privileges
-rw-r--r--lib/mixlib/shellout.rb6
-rw-r--r--lib/mixlib/shellout/unix.rb4
-rw-r--r--lib/mixlib/shellout/windows.rb5
-rw-r--r--lib/mixlib/shellout/windows/core_ext.rb179
-rw-r--r--spec/mixlib/shellout_spec.rb8
5 files changed, 131 insertions, 71 deletions
diff --git a/lib/mixlib/shellout.rb b/lib/mixlib/shellout.rb
index ca1cd22..76e6959 100644
--- a/lib/mixlib/shellout.rb
+++ b/lib/mixlib/shellout.rb
@@ -109,6 +109,9 @@ module Mixlib
attr_reader :stdin_pipe, :stdout_pipe, :stderr_pipe, :process_status_pipe
+ # Runs windows process with elevated privileges. Required for Powershell commands which need elevated privileges
+ attr_accessor :elevated
+
# === Arguments:
# Takes a single command, or a list of command fragments. These are used
# as arguments to Kernel.exec. See the Kernel.exec documentation for more
@@ -172,6 +175,7 @@ module Mixlib
@valid_exit_codes = [0]
@terminate_reason = nil
@timeout = nil
+ @elevated = false
if command_args.last.is_a?(Hash)
parse_options(command_args.pop)
@@ -339,6 +343,8 @@ module Mixlib
end
when "login"
self.login = setting
+ when "elevated"
+ self.elevated = setting
else
raise InvalidCommandOption, "option '#{option.inspect}' is not a valid option for #{self.class.name}"
end
diff --git a/lib/mixlib/shellout/unix.rb b/lib/mixlib/shellout/unix.rb
index 90e63e9..bc0fc69 100644
--- a/lib/mixlib/shellout/unix.rb
+++ b/lib/mixlib/shellout/unix.rb
@@ -27,7 +27,9 @@ module Mixlib
# Option validation that is unix specific
def validate_options(opts)
- # No options to validate, raise exceptions here if needed
+ if opts[:elevated]
+ raise InvalidCommandOption, "Option `elevated` is supported for Powershell commands only"
+ end
end
# Whether we're simulating a login shell
diff --git a/lib/mixlib/shellout/windows.rb b/lib/mixlib/shellout/windows.rb
index adb6583..b586a5f 100644
--- a/lib/mixlib/shellout/windows.rb
+++ b/lib/mixlib/shellout/windows.rb
@@ -35,6 +35,10 @@ module Mixlib
if opts[:user] && !opts[:password]
raise InvalidCommandOption, "You must supply a password when supplying a user in windows"
end
+
+ if opts[:elevated] && opts[:elevated] != true && opts[:elevated] != false
+ raise InvalidCommandOption, "Invalid value passed for `elevated`. Please provide true/false."
+ end
end
#--
@@ -71,6 +75,7 @@ module Mixlib
create_process_args[:domain] = domain.nil? ? "." : domain
create_process_args[:with_logon] = with_logon if with_logon
create_process_args[:password] = password if password
+ create_process_args[:elevated] = elevated if elevated
#
# Start the process
diff --git a/lib/mixlib/shellout/windows/core_ext.rb b/lib/mixlib/shellout/windows/core_ext.rb
index 17faff2..c795eab 100644
--- a/lib/mixlib/shellout/windows/core_ext.rb
+++ b/lib/mixlib/shellout/windows/core_ext.rb
@@ -24,7 +24,8 @@ module Process::Constants
private
LOGON32_LOGON_INTERACTIVE = 0x00000002
- LOGON32_PROVIDER_DEFAULT = 0x00000000
+ LOGON32_LOGON_BATCH = 0x00000004
+ LOGON32_PROVIDER_DEFAULT = 0x00000000
UOI_NAME = 0x00000002
WAIT_OBJECT_0 = 0
@@ -32,6 +33,9 @@ module Process::Constants
WAIT_ABANDONED = 128
WAIT_ABANDONED_0 = WAIT_ABANDONED
WAIT_FAILED = 0xFFFFFFFF
+
+ ERROR_PRIVILEGE_NOT_HELD = 1314
+ ERROR_LOGON_TYPE_NOT_GRANTED = 0x569
end
# Define the functions needed to check with Service windows station
@@ -71,7 +75,7 @@ module Process
valid_keys = %w{
app_name command_line inherit creation_flags cwd environment
startup_info thread_inherit process_inherit close_handles with_logon
- domain password
+ domain password elevated
}
valid_si_keys = %w{
@@ -234,82 +238,32 @@ module Process
inherit = hash["inherit"] ? 1 : 0
if hash["with_logon"]
- logon = hash["with_logon"].to_wide_string
-
- if hash["password"]
- passwd = hash["password"].to_wide_string
- else
- raise ArgumentError, "password must be specified if with_logon is used"
- end
-
- if hash["domain"]
- domain = hash["domain"].to_wide_string
- end
+ logon, passwd, domain = format_creds_from_hash(hash)
hash["creation_flags"] |= CREATE_UNICODE_ENVIRONMENT
- winsta_name = FFI::MemoryPointer.new(:char, 256)
- return_size = FFI::MemoryPointer.new(:ulong)
-
- bool = GetUserObjectInformationA(
- GetProcessWindowStation(), # Window station handle
- UOI_NAME, # Information to get
- winsta_name, # Buffer to receive information
- winsta_name.size, # Size of buffer
- return_size # Size filled into buffer
- )
-
- unless bool
- raise SystemCallError.new("GetUserObjectInformationA", FFI.errno)
- end
-
- winsta_name = winsta_name.read_string(return_size.read_ulong)
+ winsta_name = get_windows_station_name
# If running in the service windows station must do a log on to get
- # to the interactive desktop. Running process user account must have
+ # to the interactive desktop. The running process user account must have
# the 'Replace a process level token' permission. This is necessary as
# the logon (which happens with CreateProcessWithLogon) must have an
# interactive windows station to attach to, which is created with the
- # LogonUser cann with the LOGON32_LOGON_INTERACTIVE flag.
- if winsta_name =~ /^Service-0x0-.*$/i
- token = FFI::MemoryPointer.new(:ulong)
-
- bool = LogonUserW(
- logon, # User
- domain, # Domain
- passwd, # Password
- LOGON32_LOGON_INTERACTIVE, # Logon Type
- LOGON32_PROVIDER_DEFAULT, # Logon Provider
- token # User token handle
- )
-
- unless bool
- raise SystemCallError.new("LogonUserW", FFI.errno)
- end
-
- token = token.read_ulong
-
- begin
- bool = CreateProcessAsUserW(
- token, # User token handle
- app, # App name
- cmd, # Command line
- process_security, # Process attributes
- thread_security, # Thread attributes
- inherit, # Inherit handles
- hash["creation_flags"], # Creation Flags
- env, # Environment
- cwd, # Working directory
- startinfo, # Startup Info
- procinfo # Process Info
- )
- ensure
- CloseHandle(token)
- end
-
- unless bool
- raise SystemCallError.new("CreateProcessAsUserW (You must hold the 'Replace a process level token' permission)", FFI.errno)
- end
+ # LogonUser call with the LOGON32_LOGON_INTERACTIVE flag.
+ #
+ # User Access Control (UAC) only applies to interactive logons, so we
+ # can simulate running a command 'elevated' by running it under a separate
+ # logon as a batch process.
+ if hash["elevated"] || winsta_name =~ /^Service-0x0-.*$/i
+ logon_type = if hash["elevated"]
+ LOGON32_LOGON_BATCH
+ else
+ LOGON32_LOGON_INTERACTIVE
+ end
+
+ token = logon_user(logon, domain, passwd, logon_type)
+
+ create_process_as_user(token, app, cmd, process_security, thread_security, hash["creation_flags"], env, cwd, startinfo, procinfo)
else
bool = CreateProcessWithLogonW(
logon, # User
@@ -367,5 +321,90 @@ module Process
procinfo[:dwThreadId]
)
end
+
+ def logon_user(user, domain, passwd, type, provider = LOGON32_PROVIDER_DEFAULT)
+ token = FFI::MemoryPointer.new(:ulong)
+
+ bool = LogonUserW(
+ user, # User
+ domain, # Domain
+ passwd, # Password
+ type, # Logon Type
+ provider, # Logon Provider
+ token # User token handle
+ )
+
+ unless bool
+ if (FFI.errno == ERROR_LOGON_TYPE_NOT_GRANTED) && (type == LOGON32_LOGON_BATCH)
+ user_utf8 = user.encode( "UTF-8", invalid: :replace, undef: :replace, replace: "" ).delete("\0")
+ raise SystemCallError.new("LogonUserW (User '#{user_utf8}' must hold 'Log on as a batch job' permissions.)", FFI.errno)
+ else
+ raise SystemCallError.new("LogonUserW", FFI.errno)
+ end
+ end
+
+ token.read_ulong
+ end
+
+ def create_process_as_user(token, app, cmd, process_security, thread_security, inherit, creation_flags, env, cwd, startinfo, procinfo)
+ bool = CreateProcessAsUserW(
+ token, # User token handle
+ app, # App name
+ cmd, # Command line
+ process_security, # Process attributes
+ thread_security, # Thread attributes
+ inherit, # Inherit handles
+ creation_flags, # Creation Flags
+ env, # Environment
+ cwd, # Working directory
+ startinfo, # Startup Info
+ procinfo # Process Info
+ )
+
+ unless bool
+ if FFI.errno == ERROR_PRIVILEGE_NOT_HELD
+ raise SystemCallError.new("CreateProcessAsUserW (User '#{::ENV['USERNAME']}' must hold the 'Replace a process level token' and 'Adjust Memory Quotas for a process' permissions. Logoff the user after adding this right to make it effective.)", FFI.errno)
+ else
+ raise SystemCallError.new("CreateProcessAsUserW failed.", FFI.errno)
+ end
+ end
+ ensure
+ CloseHandle(token)
+ end
+
+ def get_windows_station_name
+ winsta_name = FFI::MemoryPointer.new(:char, 256)
+ return_size = FFI::MemoryPointer.new(:ulong)
+
+ bool = GetUserObjectInformationA(
+ GetProcessWindowStation(), # Window station handle
+ UOI_NAME, # Information to get
+ winsta_name, # Buffer to receive information
+ winsta_name.size, # Size of buffer
+ return_size # Size filled into buffer
+ )
+
+ unless bool
+ raise SystemCallError.new("GetUserObjectInformationA", FFI.errno)
+ end
+
+ winsta_name.read_string(return_size.read_ulong)
+ end
+
+ def format_creds_from_hash(hash)
+ logon = hash["with_logon"].to_wide_string
+
+ if hash["password"]
+ passwd = hash["password"].to_wide_string
+ else
+ raise ArgumentError, "password must be specified if with_logon is used"
+ end
+
+ if hash["domain"]
+ domain = hash["domain"].to_wide_string
+ end
+
+ [ logon, passwd, domain ]
+ end
end
end
diff --git a/spec/mixlib/shellout_spec.rb b/spec/mixlib/shellout_spec.rb
index a55b3c0..57c1403 100644
--- a/spec/mixlib/shellout_spec.rb
+++ b/spec/mixlib/shellout_spec.rb
@@ -655,6 +655,14 @@ describe Mixlib::ShellOut do
it "should run as specified user" do
expect(running_user).to eql("#{ENV['COMPUTERNAME'].downcase}\\#{user}")
end
+
+ context "when :elevated => true" do
+ let(:options) { { :user => user, :password => password, :elevated => true } }
+
+ it "raises error" do
+ expect { running_user }.to raise_error(/the user has not been granted the requested logon type at this computer/)
+ end
+ end
end
end