mixlib-shellout 2.4.4 → 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a77caa01ee050dc884322465862b5eaa2b197b78ebe07f3ac6a4e3526924ce8
4
- data.tar.gz: d09b463bd4e2ce05d35a34df03684c4e033af823f54ab510804c648d018391d0
3
+ metadata.gz: 3e6f0d990000b229cfa3d812040828440973f528374790b2eb261c28db29bc13
4
+ data.tar.gz: 23ff6a2702bcb423b56daa92a23938299f541f1724dfb5599059dd65aa4e957c
5
5
  SHA512:
6
- metadata.gz: 3f531586506e3461104c21931b216203e8e884549c5f069753bf6633882632b3a0330384d33a1c1b7fb1f628c9b4fefa0ec620ecd6929fec0630bbadc54f0c2c
7
- data.tar.gz: ae952774568dc2d26bd382ed40056bc7544353eb70538161449cb7f704811ef09462b992a8cc83b4a8ff72131f6e4d9925f93a90326ed7f3ededa045f5439ca4
6
+ metadata.gz: 6c76cc403ee32e7dae114f34e7684b3aa530eacfde72eb160ab0c007de6dbf3d8497f651d2894c873b6f3e734918de922ad57632965ada3fb1ef67418efb6d31
7
+ data.tar.gz: cad33035223cdd99827846282a9e9a201e63a6f99611f25430caaa6ddf72fd3da21f18e97840d77c70d1af9abd2da65bfeb732a20aa41b1f039ae494c8968b44
@@ -19,7 +19,7 @@
19
19
  require "etc"
20
20
  require "tmpdir"
21
21
  require "fcntl"
22
- require "mixlib/shellout/exceptions"
22
+ require_relative "shellout/exceptions"
23
23
 
24
24
  module Mixlib
25
25
 
@@ -29,10 +29,10 @@ module Mixlib
29
29
  DEFAULT_READ_TIMEOUT = 600
30
30
 
31
31
  if RUBY_PLATFORM =~ /mswin|mingw32|windows/
32
- require "mixlib/shellout/windows"
32
+ require_relative "shellout/windows"
33
33
  include ShellOut::Windows
34
34
  else
35
- require "mixlib/shellout/unix"
35
+ require_relative "shellout/unix"
36
36
  include ShellOut::Unix
37
37
  end
38
38
 
@@ -65,7 +65,7 @@ module Mixlib
65
65
  # as the subprocess is running.
66
66
  attr_accessor :live_stderr
67
67
 
68
- # ShellOut will push data from :input down the stdin of the subprocss.
68
+ # ShellOut will push data from :input down the stdin of the subprocess.
69
69
  # Normally set via options passed to new.
70
70
  # Default: nil
71
71
  attr_accessor :input
@@ -210,15 +210,17 @@ module Mixlib
210
210
  # TODO migrate to shellout/unix.rb
211
211
  def uid
212
212
  return nil unless user
213
- user.kind_of?(Integer) ? user : Etc.getpwnam(user.to_s).uid
213
+
214
+ user.is_a?(Integer) ? user : Etc.getpwnam(user.to_s).uid
214
215
  end
215
216
 
216
217
  # The gid that the subprocess will switch to. If the group attribute is
217
218
  # given as a group name, it is converted to a gid by Etc.getgrnam
218
219
  # TODO migrate to shellout/unix.rb
219
220
  def gid
220
- return group.kind_of?(Integer) ? group : Etc.getgrnam(group.to_s).gid if group
221
+ return group.is_a?(Integer) ? group : Etc.getgrnam(group.to_s).gid if group
221
222
  return Etc.getpwuid(uid).gid if using_login?
223
+
222
224
  nil
223
225
  end
224
226
 
@@ -231,6 +233,7 @@ module Mixlib
231
233
  # results when the command exited with an unexpected status.
232
234
  def format_for_exception
233
235
  return "Command execution failed. STDOUT/STDERR suppressed for sensitive resource" if sensitive
236
+
234
237
  msg = ""
235
238
  msg << "#{@terminate_reason}\n" if @terminate_reason
236
239
  msg << "---- Begin output of #{command} ----\n"
@@ -363,6 +366,7 @@ module Mixlib
363
366
  if login && !user
364
367
  raise InvalidCommandOption, "cannot set login without specifying a user"
365
368
  end
369
+
366
370
  super
367
371
  end
368
372
  end
@@ -0,0 +1,209 @@
1
+ #--
2
+ # Author:: Daniel DeLeo (<dan@chef.io>)
3
+ # Copyright:: Copyright (c) Chef Software Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require_relative "../shellout"
19
+ require "chef-utils"
20
+ require "chef-utils/dsl/path_sanity"
21
+ require "chef-utils/internal"
22
+
23
+ module Mixlib
24
+ class ShellOut
25
+ module Helper
26
+ include ChefUtils::Internal
27
+ include ChefUtils::DSL::PathSanity
28
+
29
+ # PREFERRED APIS:
30
+ #
31
+ # all consumers should now call shell_out!/shell_out.
32
+ #
33
+ # the shell_out_compacted/shell_out_compacted! APIs are private but are intended for use
34
+ # in rspec tests, and should ideally always be used to make code refactoring that do not
35
+ # change behavior easier:
36
+ #
37
+ # allow(provider).to receive(:shell_out_compacted!).with("foo", "bar", "baz")
38
+ # provider.shell_out!("foo", [ "bar", nil, "baz"])
39
+ # provider.shell_out!(["foo", nil, "bar" ], ["baz"])
40
+ #
41
+ # note that shell_out_compacted also includes adding the magical timeout option to force
42
+ # people to setup expectations on that value explicitly. it does not include the default_env
43
+ # mangling in order to avoid users having to setup an expectation on anything other than
44
+ # setting `default_env: false` and allow us to make tweak to the default_env without breaking
45
+ # a thousand unit tests.
46
+ #
47
+
48
+ def shell_out(*args, **options)
49
+ options = options.dup
50
+ options = __maybe_add_timeout(self, options)
51
+ if options.empty?
52
+ shell_out_compacted(*__clean_array(*args))
53
+ else
54
+ shell_out_compacted(*__clean_array(*args), **options)
55
+ end
56
+ end
57
+
58
+ def shell_out!(*args, **options)
59
+ options = options.dup
60
+ options = __maybe_add_timeout(self, options)
61
+ if options.empty?
62
+ shell_out_compacted!(*__clean_array(*args))
63
+ else
64
+ shell_out_compacted!(*__clean_array(*args), **options)
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ # helper sugar for resources that support passing timeouts to shell_out
71
+ #
72
+ # module method to not pollute namespaces, but that means we need self injected as an arg
73
+ # @api private
74
+ def __maybe_add_timeout(obj, options)
75
+ options = options.dup
76
+ # historically resources have not properly declared defaults on their timeouts, so a default default of 900s was enforced here
77
+ default_val = 900
78
+ return options if options.key?(:timeout)
79
+
80
+ # FIXME: need to nuke descendent tracker out of Chef::Provider so we can just define that class here without requiring the
81
+ # world, and then just use symbol lookup
82
+ if obj.class.ancestors.map(&:name).include?("Chef::Provider") && obj.respond_to?(:new_resource) && obj.new_resource.respond_to?(:timeout) && !options.key?(:timeout)
83
+ options[:timeout] = obj.new_resource.timeout ? obj.new_resource.timeout.to_f : default_val
84
+ end
85
+ options
86
+ end
87
+
88
+ # helper function to mangle options when `default_env` is true
89
+ #
90
+ # @api private
91
+ def __apply_default_env(options)
92
+ options = options.dup
93
+ default_env = options.delete(:default_env)
94
+ default_env = true if default_env.nil?
95
+ if default_env
96
+ env_key = options.key?(:env) ? :env : :environment
97
+ options[env_key] = {
98
+ "LC_ALL" => __config[:internal_locale],
99
+ "LANGUAGE" => __config[:internal_locale],
100
+ "LANG" => __config[:internal_locale],
101
+ __env_path_name => sanitized_path,
102
+ }.update(options[env_key] || {})
103
+ end
104
+ options
105
+ end
106
+
107
+ # this SHOULD be used for setting up expectations in rspec, see banner comment at top.
108
+ #
109
+ # the private constraint is meant to avoid code calling this directly, rspec expectations are fine.
110
+ #
111
+ def shell_out_compacted(*args, **options)
112
+ options = __apply_default_env(options)
113
+ if options.empty?
114
+ __shell_out_command(*args)
115
+ else
116
+ __shell_out_command(*args, **options)
117
+ end
118
+ end
119
+
120
+ # this SHOULD be used for setting up expectations in rspec, see banner comment at top.
121
+ #
122
+ # the private constraint is meant to avoid code calling this directly, rspec expectations are fine.
123
+ #
124
+ def shell_out_compacted!(*args, **options)
125
+ options = __apply_default_env(options)
126
+ cmd = if options.empty?
127
+ __shell_out_command(*args)
128
+ else
129
+ __shell_out_command(*args, **options)
130
+ end
131
+ cmd.error!
132
+ cmd
133
+ end
134
+
135
+ # Helper for subclasses to reject nil out of an array. It allows
136
+ # using the array form of shell_out (which avoids the need to surround arguments with
137
+ # quote marks to deal with shells).
138
+ #
139
+ # Usage:
140
+ # shell_out!(*clean_array("useradd", universal_options, useradd_options, new_resource.username))
141
+ #
142
+ # universal_options and useradd_options can be nil, empty array, empty string, strings or arrays
143
+ # and the result makes sense.
144
+ #
145
+ # keeping this separate from shell_out!() makes it a bit easier to write expectations against the
146
+ # shell_out args and be able to omit nils and such in the tests (and to test that the nils are
147
+ # being rejected correctly).
148
+ #
149
+ # @param args [String] variable number of string arguments
150
+ # @return [Array] array of strings with nil and null string rejection
151
+
152
+ def __clean_array(*args)
153
+ args.flatten.compact.map(&:to_s)
154
+ end
155
+
156
+ def __shell_out_command(*args, **options)
157
+ if __transport_connection
158
+ FakeShellOut.new(args, options, __transport_connection.run_command(args.join(" "))) # FIXME: train should accept run_command(*args)
159
+ else
160
+ cmd = if options.empty?
161
+ Mixlib::ShellOut.new(*args)
162
+ else
163
+ Mixlib::ShellOut.new(*args, **options)
164
+ end
165
+ cmd.live_stream ||= __io_for_live_stream
166
+ cmd.run_command
167
+ cmd
168
+ end
169
+ end
170
+
171
+ def __io_for_live_stream
172
+ if STDOUT.tty? && !__config[:daemon] && __log.debug?
173
+ STDOUT
174
+ else
175
+ nil
176
+ end
177
+ end
178
+
179
+ def __env_path_name
180
+ if ChefUtils.windows?
181
+ "Path"
182
+ else
183
+ "PATH"
184
+ end
185
+ end
186
+
187
+ class FakeShellOut
188
+ attr_reader :stdout, :stderr, :exitstatus, :status
189
+
190
+ def initialize(args, options, result)
191
+ @args = args
192
+ @options = options
193
+ @stdout = result.stdout
194
+ @stderr = result.stderr
195
+ @exitstatus = result.exit_status
196
+ @status = OpenStruct.new(success?: ( exitstatus == 0 ))
197
+ end
198
+
199
+ def error?
200
+ exitstatus != 0
201
+ end
202
+
203
+ def error!
204
+ raise Mixlib::ShellOut::ShellCommandFailed, "Unexpected exit status of #{exitstatus} running #{@args}" if error?
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -53,14 +53,16 @@ module Mixlib
53
53
  # to the user's secondary groups
54
54
  def sgids
55
55
  return nil unless using_login?
56
+
56
57
  user_name = Etc.getpwuid(uid).name
57
- all_seconderies.select { |g| g.mem.include?(user_name) }.map { |g| g.gid }
58
+ all_seconderies.select { |g| g.mem.include?(user_name) }.map(&:gid)
58
59
  end
59
60
 
60
61
  # The environment variables that are deduced from simulating logon
61
62
  # Only valid if login is used
62
63
  def logon_environment
63
64
  return {} unless using_login?
65
+
64
66
  entry = Etc.getpwuid(uid)
65
67
  # According to `man su`, the set fields are:
66
68
  # $HOME, $SHELL, $USER, $LOGNAME, $PATH, and $IFS
@@ -269,6 +271,7 @@ module Mixlib
269
271
  # Keep this unbuffered for now
270
272
  def write_to_child_stdin
271
273
  return unless input
274
+
272
275
  child_stdin << input
273
276
  child_stdin.close # Kick things off
274
277
  end
@@ -337,7 +340,7 @@ module Mixlib
337
340
  set_cwd
338
341
 
339
342
  begin
340
- command.kind_of?(Array) ? exec(*command, close_others: true) : exec(command, close_others: true)
343
+ command.is_a?(Array) ? exec(*command, close_others: true) : exec(command, close_others: true)
341
344
 
342
345
  raise "forty-two" # Should never get here
343
346
  rescue Exception => e
@@ -365,6 +368,7 @@ module Mixlib
365
368
 
366
369
  def reap_errant_child
367
370
  return if attempt_reap
371
+
368
372
  @terminate_reason = "Command exceeded allowed execution time, process terminated"
369
373
  logger.error("Command exceeded allowed execution time, sending TERM") if logger
370
374
  Process.kill(:TERM, child_pgid)
@@ -1,5 +1,5 @@
1
1
  module Mixlib
2
2
  class ShellOut
3
- VERSION = "2.4.4".freeze
3
+ VERSION = "3.1.1".freeze
4
4
  end
5
5
  end
@@ -2,7 +2,7 @@
2
2
  # Author:: Daniel DeLeo (<dan@chef.io>)
3
3
  # Author:: John Keiser (<jkeiser@chef.io>)
4
4
  # Author:: Ho-Sheng Hsiao (<hosh@chef.io>)
5
- # Copyright:: Copyright (c) 2011-2016 Chef Software, Inc.
5
+ # Copyright:: Copyright (c) 2011-2019, Chef Software Inc.
6
6
  # License:: Apache License, Version 2.0
7
7
  #
8
8
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,7 +19,7 @@
19
19
  #
20
20
 
21
21
  require "win32/process"
22
- require "mixlib/shellout/windows/core_ext"
22
+ require_relative "windows/core_ext"
23
23
 
24
24
  module Mixlib
25
25
  class ShellOut
@@ -66,7 +66,7 @@ module Mixlib
66
66
  #
67
67
  # Set cwd, environment, appname, etc.
68
68
  #
69
- app_name, command_line = command_to_run(command)
69
+ app_name, command_line = command_to_run(combine_args(*command))
70
70
  create_process_args = {
71
71
  app_name: app_name,
72
72
  command_line: command_line,
@@ -88,7 +88,7 @@ module Mixlib
88
88
  #
89
89
  # Start the process
90
90
  #
91
- process = Process.create(create_process_args)
91
+ process, profile, token = Process.create3(create_process_args)
92
92
  logger.debug(format_process(process, app_name, command_line, timeout)) if logger
93
93
  begin
94
94
  # Start pushing data into input
@@ -110,6 +110,7 @@ module Mixlib
110
110
  unless GetExitCodeProcess(process.process_handle, exit_code)
111
111
  raise get_last_error
112
112
  end
113
+
113
114
  @status = ThingThatLooksSortOfLikeAProcessStatus.new
114
115
  @status.exitstatus = exit_code.unpack("l").first
115
116
 
@@ -143,6 +144,8 @@ module Mixlib
143
144
  ensure
144
145
  CloseHandle(process.thread_handle) if process.thread_handle
145
146
  CloseHandle(process.process_handle) if process.process_handle
147
+ Process.unload_user_profile(token, profile) if profile
148
+ CloseHandle(token) if token
146
149
  end
147
150
 
148
151
  ensure
@@ -168,8 +171,9 @@ module Mixlib
168
171
 
169
172
  def consume_output(open_streams, stdout_read, stderr_read)
170
173
  return false if open_streams.length == 0
174
+
171
175
  ready = IO.select(open_streams, nil, nil, READ_WAIT_TIME)
172
- return true if ! ready
176
+ return true unless ready
173
177
 
174
178
  if ready.first.include?(stdout_read)
175
179
  begin
@@ -196,6 +200,47 @@ module Mixlib
196
200
  true
197
201
  end
198
202
 
203
+ # Use to support array passing semantics on windows
204
+ #
205
+ # 1. strings with whitespace or quotes in them need quotes around them.
206
+ # 2. interior quotes need to get backslash escaped (parser needs to know when it really ends).
207
+ # 3. random backlsashes in paths themselves remain untouched.
208
+ # 4. if the argument must be quoted by #1 and terminates in a sequence of backslashes then all the backlashes must themselves
209
+ # be backslash excaped (double the backslashes).
210
+ # 5. if an interior quote that must be escaped by #2 has a sequence of backslashes before it then all the backslashes must
211
+ # themselves be backslash excaped along with the backslash ecape of the interior quote (double plus one backslashes).
212
+ #
213
+ # And to restate. We are constructing a string which will be parsed by the windows parser into arguments, and we want those
214
+ # arguments to match the *args array we are passed here. So call the windows parser operation A then we need to apply A^-1 to
215
+ # our args to construct the string so that applying A gives windows back our *args.
216
+ #
217
+ # And when the windows parser sees a series of backslashes followed by a double quote, it has to determine if that double quote
218
+ # is terminating or not, and how many backslashes to insert in the args. So what it does is divide it by two (rounding down) to
219
+ # get the number of backslashes to insert. Then if it is even the double quotes terminate the argument. If it is even the
220
+ # double quotes are interior double quotes (the extra backslash quotes the double quote).
221
+ #
222
+ # We construct the inverse operation so interior double quotes preceeded by N backslashes get 2N+1 backslashes in front of the quote,
223
+ # while trailing N backslashes get 2N backslashes in front of the quote that terminates the argument.
224
+ #
225
+ # see: https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
226
+ #
227
+ # @api private
228
+ # @param args [Array<String>] array of command arguments
229
+ # @return String
230
+ def combine_args(*args)
231
+ return args[0] if args.length == 1
232
+
233
+ args.map do |arg|
234
+ if arg =~ /[ \t\n\v"]/
235
+ arg = arg.gsub(/(\\*)"/, '\1\1\"') # interior quotes with N preceeding backslashes need 2N+1 backslashes
236
+ arg = arg.sub(/(\\+)$/, '\1\1') # trailing N backslashes need to become 2N backslashes
237
+ "\"#{arg}\""
238
+ else
239
+ arg
240
+ end
241
+ end.join(" ")
242
+ end
243
+
199
244
  def command_to_run(command)
200
245
  return run_under_cmd(command) if should_run_under_cmd?(command)
201
246
 
@@ -279,10 +324,12 @@ module Mixlib
279
324
  return true unless quote
280
325
  when "%"
281
326
  return true if env
327
+
282
328
  env = env_first_char = true
283
329
  next
284
330
  else
285
331
  next unless env
332
+
286
333
  if env_first_char
287
334
  env_first_char = false
288
335
  (env = false) && next if c !~ /[A-Za-z_]/
@@ -328,6 +375,7 @@ module Mixlib
328
375
 
329
376
  def unsafe_process?(name, logger)
330
377
  return false unless system_required_processes.include? name
378
+
331
379
  logger.debug(
332
380
  "A request to kill a critical system process - #{name} - was received and skipped."
333
381
  )
@@ -341,6 +389,7 @@ module Mixlib
341
389
  def kill_process_tree(pid, wmi, logger)
342
390
  wmi.query("select * from Win32_Process where ParentProcessID=#{pid}").each do |instance|
343
391
  next if unsafe_process?(instance.wmi_ole_object.name, logger)
392
+
344
393
  child_pid = instance.wmi_ole_object.processid
345
394
  kill_process_tree(child_pid, wmi, logger)
346
395
  kill_process(instance, logger)
@@ -36,18 +36,56 @@ module Process::Constants
36
36
 
37
37
  ERROR_PRIVILEGE_NOT_HELD = 1314
38
38
  ERROR_LOGON_TYPE_NOT_GRANTED = 0x569
39
+
40
+ # Only documented in Userenv.h ???
41
+ # - ZERO (type Local) is assumed, no docs found
42
+ WIN32_PROFILETYPE_LOCAL = 0x00
43
+ WIN32_PROFILETYPE_PT_TEMPORARY = 0x01
44
+ WIN32_PROFILETYPE_PT_ROAMING = 0x02
45
+ WIN32_PROFILETYPE_PT_MANDATORY = 0x04
46
+ WIN32_PROFILETYPE_PT_ROAMING_PREEXISTING = 0x08
47
+
48
+ end
49
+
50
+ # Structs required for data handling
51
+ module Process::Structs
52
+
53
+ class PROFILEINFO < FFI::Struct
54
+ layout(
55
+ :dwSize, :dword,
56
+ :dwFlags, :dword,
57
+ :lpUserName, :pointer,
58
+ :lpProfilePath, :pointer,
59
+ :lpDefaultPath, :pointer,
60
+ :lpServerName, :pointer,
61
+ :lpPolicyPath, :pointer,
62
+ :hProfile, :handle
63
+ )
64
+ end
65
+
39
66
  end
40
67
 
41
68
  # Define the functions needed to check with Service windows station
42
69
  module Process::Functions
70
+ ffi_lib :userenv
71
+
72
+ attach_pfunc :GetProfileType,
73
+ [:pointer], :bool
74
+
75
+ attach_pfunc :LoadUserProfileW,
76
+ %i{handle pointer}, :bool
77
+
78
+ attach_pfunc :UnloadUserProfile,
79
+ %i{handle handle}, :bool
80
+
43
81
  ffi_lib :advapi32
44
82
 
45
83
  attach_pfunc :LogonUserW,
46
- [:buffer_in, :buffer_in, :buffer_in, :ulong, :ulong, :pointer], :bool
84
+ %i{buffer_in buffer_in buffer_in ulong ulong pointer}, :bool
47
85
 
48
86
  attach_pfunc :CreateProcessAsUserW,
49
- [:ulong, :buffer_in, :buffer_inout, :pointer, :pointer, :int,
50
- :ulong, :buffer_in, :buffer_in, :pointer, :pointer], :bool
87
+ %i{ulong buffer_in buffer_inout pointer pointer int
88
+ ulong buffer_in buffer_in pointer pointer}, :bool
51
89
 
52
90
  ffi_lib :user32
53
91
 
@@ -55,7 +93,7 @@ module Process::Functions
55
93
  [], :ulong
56
94
 
57
95
  attach_pfunc :GetUserObjectInformationA,
58
- [:ulong, :uint, :buffer_out, :ulong, :pointer], :bool
96
+ %i{ulong uint buffer_out ulong pointer}, :bool
59
97
  end
60
98
 
61
99
  # Override Process.create to check for running in the Service window station and doing
@@ -64,11 +102,18 @@ end
64
102
  # as of 2015-10-15 from commit cc066e5df25048f9806a610f54bf5f7f253e86f7
65
103
  module Process
66
104
 
105
+ class UnsupportedFeature < StandardError; end
106
+
67
107
  # Explicitly reopen singleton class so that class/constant declarations from
68
108
  # extensions are visible in Modules.nesting.
69
109
  class << self
110
+
70
111
  def create(args)
71
- unless args.kind_of?(Hash)
112
+ create3(args).first
113
+ end
114
+
115
+ def create3(args)
116
+ unless args.is_a?(Hash)
72
117
  raise TypeError, "hash keyword arguments expected"
73
118
  end
74
119
 
@@ -85,9 +130,9 @@ module Process
85
130
 
86
131
  # Set default values
87
132
  hash = {
88
- "app_name" => nil,
133
+ "app_name" => nil,
89
134
  "creation_flags" => 0,
90
- "close_handles" => true,
135
+ "close_handles" => true,
91
136
  }
92
137
 
93
138
  # Validate the keys, and convert symbols and case to lowercase strings.
@@ -96,6 +141,7 @@ module Process
96
141
  unless valid_keys.include?(key)
97
142
  raise ArgumentError, "invalid key '#{key}'"
98
143
  end
144
+
99
145
  hash[key] = val
100
146
  end
101
147
 
@@ -108,6 +154,7 @@ module Process
108
154
  unless valid_si_keys.include?(key)
109
155
  raise ArgumentError, "invalid startup_info key '#{key}'"
110
156
  end
157
+
111
158
  si_hash[key] = val
112
159
  end
113
160
  end
@@ -238,6 +285,7 @@ module Process
238
285
  inherit = hash["inherit"] ? 1 : 0
239
286
 
240
287
  if hash["with_logon"]
288
+
241
289
  logon, passwd, domain = format_creds_from_hash(hash)
242
290
 
243
291
  hash["creation_flags"] |= CREATE_UNICODE_ENVIRONMENT
@@ -255,51 +303,42 @@ module Process
255
303
  # can simulate running a command 'elevated' by running it under a separate
256
304
  # logon as a batch process.
257
305
  if hash["elevated"] || winsta_name =~ /^Service-0x0-.*$/i
258
- logon_type = if hash["elevated"]
259
- LOGON32_LOGON_BATCH
260
- else
261
- LOGON32_LOGON_INTERACTIVE
262
- end
263
306
 
264
- token = logon_user(logon, domain, passwd, logon_type)
307
+ logon_type = hash["elevated"] ? LOGON32_LOGON_BATCH : LOGON32_LOGON_INTERACTIVE
308
+ token = logon_user(logon, domain, passwd, logon_type)
309
+ logon_ptr = FFI::MemoryPointer.from_string(logon)
310
+ profile = PROFILEINFO.new.tap do |dat|
311
+ dat[:dwSize] = dat.size
312
+ dat[:dwFlags] = 1
313
+ dat[:lpUserName] = logon_ptr
314
+ end
315
+
316
+ if logon_has_roaming_profile?
317
+ msg = %w{
318
+ Mixlib does not currently support executing commands as users
319
+ configured with Roaming Profiles. [%s]
320
+ }.join(" ") % logon.encode("UTF-8").unpack("A*")
321
+ raise UnsupportedFeature.new(msg)
322
+ end
323
+
324
+ load_user_profile(token, profile.pointer)
325
+
326
+ create_process_as_user(token, app, cmd, process_security,
327
+ thread_security, inherit, hash["creation_flags"], env,
328
+ cwd, startinfo, procinfo)
265
329
 
266
- create_process_as_user(token, app, cmd, process_security, thread_security, inherit, hash["creation_flags"], env, cwd, startinfo, procinfo)
267
330
  else
268
- bool = CreateProcessWithLogonW(
269
- logon, # User
270
- domain, # Domain
271
- passwd, # Password
272
- LOGON_WITH_PROFILE, # Logon flags
273
- app, # App name
274
- cmd, # Command line
275
- hash["creation_flags"], # Creation flags
276
- env, # Environment
277
- cwd, # Working directory
278
- startinfo, # Startup Info
279
- procinfo # Process Info
280
- )
281
331
 
282
- unless bool
283
- raise SystemCallError.new("CreateProcessWithLogonW", FFI.errno)
284
- end
332
+ create_process_with_logon(logon, domain, passwd, LOGON_WITH_PROFILE,
333
+ app, cmd, hash["creation_flags"], env, cwd, startinfo, procinfo)
334
+
285
335
  end
336
+
286
337
  else
287
- bool = CreateProcessW(
288
- app, # App name
289
- cmd, # Command line
290
- process_security, # Process attributes
291
- thread_security, # Thread attributes
292
- inherit, # Inherit handles?
293
- hash["creation_flags"], # Creation flags
294
- env, # Environment
295
- cwd, # Working directory
296
- startinfo, # Startup Info
297
- procinfo # Process Info
298
- )
299
-
300
- unless bool
301
- raise SystemCallError.new("CreateProcessW", FFI.errno)
302
- end
338
+
339
+ create_process(app, cmd, process_security, thread_security, inherit,
340
+ hash["creation_flags"], env, cwd, startinfo, procinfo)
341
+
303
342
  end
304
343
 
305
344
  # Automatically close the process and thread handles in the
@@ -314,12 +353,122 @@ module Process
314
353
  procinfo[:hThread] = 0
315
354
  end
316
355
 
317
- ProcessInfo.new(
356
+ process = ProcessInfo.new(
318
357
  procinfo[:hProcess],
319
358
  procinfo[:hThread],
320
359
  procinfo[:dwProcessId],
321
360
  procinfo[:dwThreadId]
322
361
  )
362
+
363
+ [ process, profile, token ]
364
+ end
365
+
366
+ # See Process::Constants::WIN32_PROFILETYPE
367
+ def logon_has_roaming_profile?
368
+ get_profile_type >= 2
369
+ end
370
+
371
+ def get_profile_type
372
+ ptr = FFI::MemoryPointer.new(:uint)
373
+ unless GetProfileType(ptr)
374
+ raise SystemCallError.new("GetProfileType", FFI.errno)
375
+ end
376
+
377
+ ptr.read_uint
378
+ end
379
+
380
+ def load_user_profile(token, profile_ptr)
381
+ unless LoadUserProfileW(token, profile_ptr)
382
+ raise SystemCallError.new("LoadUserProfileW", FFI.errno)
383
+ end
384
+
385
+ true
386
+ end
387
+
388
+ def unload_user_profile(token, profile)
389
+ if profile[:hProfile] == 0
390
+ warn "\n\nWARNING: Profile not loaded\n"
391
+ else
392
+ unless UnloadUserProfile(token, profile[:hProfile])
393
+ raise SystemCallError.new("UnloadUserProfile", FFI.errno)
394
+ end
395
+ end
396
+ true
397
+ end
398
+
399
+ def create_process_as_user(token, app, cmd, process_security,
400
+ thread_security, inherit, creation_flags, env, cwd, startinfo, procinfo)
401
+
402
+ bool = CreateProcessAsUserW(
403
+ token, # User token handle
404
+ app, # App name
405
+ cmd, # Command line
406
+ process_security, # Process attributes
407
+ thread_security, # Thread attributes
408
+ inherit, # Inherit handles
409
+ creation_flags, # Creation Flags
410
+ env, # Environment
411
+ cwd, # Working directory
412
+ startinfo, # Startup Info
413
+ procinfo # Process Info
414
+ )
415
+
416
+ unless bool
417
+ msg = case FFI.errno
418
+ when ERROR_PRIVILEGE_NOT_HELD
419
+ [
420
+ %{CreateProcessAsUserW (User '%s' must hold the 'Replace a process},
421
+ %{level token' and 'Adjust Memory Quotas for a process' permissions.},
422
+ %{Logoff the user after adding this right to make it effective.)},
423
+ ].join(" ") % ::ENV["USERNAME"]
424
+ else
425
+ "CreateProcessAsUserW failed."
426
+ end
427
+ raise SystemCallError.new(msg, FFI.errno)
428
+ end
429
+ end
430
+
431
+ def create_process_with_logon(logon, domain, passwd, logon_flags, app, cmd,
432
+ creation_flags, env, cwd, startinfo, procinfo)
433
+
434
+ bool = CreateProcessWithLogonW(
435
+ logon, # User
436
+ domain, # Domain
437
+ passwd, # Password
438
+ logon_flags, # Logon flags
439
+ app, # App name
440
+ cmd, # Command line
441
+ creation_flags, # Creation flags
442
+ env, # Environment
443
+ cwd, # Working directory
444
+ startinfo, # Startup Info
445
+ procinfo # Process Info
446
+ )
447
+
448
+ unless bool
449
+ raise SystemCallError.new("CreateProcessWithLogonW", FFI.errno)
450
+ end
451
+ end
452
+
453
+ def create_process(app, cmd, process_security, thread_security, inherit,
454
+ creation_flags, env, cwd, startinfo, procinfo)
455
+
456
+ bool = CreateProcessW(
457
+ app, # App name
458
+ cmd, # Command line
459
+ process_security, # Process attributes
460
+ thread_security, # Thread attributes
461
+ inherit, # Inherit handles?
462
+ creation_flags, # Creation flags
463
+ env, # Environment
464
+ cwd, # Working directory
465
+ startinfo, # Startup Info
466
+ procinfo # Process Info
467
+ )
468
+
469
+ unless bool
470
+ raise SystemCallError.new("CreateProcessW", FFI.errno)
471
+ end
323
472
  end
324
473
 
325
474
  def logon_user(user, domain, passwd, type, provider = LOGON32_PROVIDER_DEFAULT)
@@ -346,32 +495,6 @@ module Process
346
495
  token.read_ulong
347
496
  end
348
497
 
349
- def create_process_as_user(token, app, cmd, process_security, thread_security, inherit, creation_flags, env, cwd, startinfo, procinfo)
350
- bool = CreateProcessAsUserW(
351
- token, # User token handle
352
- app, # App name
353
- cmd, # Command line
354
- process_security, # Process attributes
355
- thread_security, # Thread attributes
356
- inherit, # Inherit handles
357
- creation_flags, # Creation Flags
358
- env, # Environment
359
- cwd, # Working directory
360
- startinfo, # Startup Info
361
- procinfo # Process Info
362
- )
363
-
364
- unless bool
365
- if FFI.errno == ERROR_PRIVILEGE_NOT_HELD
366
- 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)
367
- else
368
- raise SystemCallError.new("CreateProcessAsUserW failed.", FFI.errno)
369
- end
370
- end
371
- ensure
372
- CloseHandle(token)
373
- end
374
-
375
498
  def get_windows_station_name
376
499
  winsta_name = FFI::MemoryPointer.new(:char, 256)
377
500
  return_size = FFI::MemoryPointer.new(:ulong)
@@ -406,5 +529,6 @@ module Process
406
529
 
407
530
  [ logon, passwd, domain ]
408
531
  end
532
+
409
533
  end
410
534
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mixlib-shellout
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.4
4
+ version: 3.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chef Software Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-12-12 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2020-07-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: chef-utils
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  description: Run external commands on Unix or Windows
14
28
  email: info@chef.io
15
29
  executables: []
@@ -19,6 +33,7 @@ files:
19
33
  - LICENSE
20
34
  - lib/mixlib/shellout.rb
21
35
  - lib/mixlib/shellout/exceptions.rb
36
+ - lib/mixlib/shellout/helper.rb
22
37
  - lib/mixlib/shellout/unix.rb
23
38
  - lib/mixlib/shellout/version.rb
24
39
  - lib/mixlib/shellout/windows.rb
@@ -34,15 +49,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
34
49
  requirements:
35
50
  - - ">="
36
51
  - !ruby/object:Gem::Version
37
- version: '2.2'
52
+ version: '2.4'
38
53
  required_rubygems_version: !ruby/object:Gem::Requirement
39
54
  requirements:
40
55
  - - ">="
41
56
  - !ruby/object:Gem::Version
42
57
  version: '0'
43
58
  requirements: []
44
- rubyforge_project:
45
- rubygems_version: 2.7.6
59
+ rubygems_version: 3.0.3
46
60
  signing_key:
47
61
  specification_version: 4
48
62
  summary: Run external commands on Unix or Windows