mixlib-shellout 2.2.7 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -17,14 +17,15 @@
17
17
  # limitations under the License.
18
18
  #
19
19
 
20
- require 'win32/process'
20
+ require "win32/process"
21
21
 
22
22
  # Add new constants for Logon
23
23
  module Process::Constants
24
24
  private
25
25
 
26
26
  LOGON32_LOGON_INTERACTIVE = 0x00000002
27
- LOGON32_PROVIDER_DEFAULT = 0x00000000
27
+ LOGON32_LOGON_BATCH = 0x00000004
28
+ LOGON32_PROVIDER_DEFAULT = 0x00000000
28
29
  UOI_NAME = 0x00000002
29
30
 
30
31
  WAIT_OBJECT_0 = 0
@@ -32,6 +33,9 @@ module Process::Constants
32
33
  WAIT_ABANDONED = 128
33
34
  WAIT_ABANDONED_0 = WAIT_ABANDONED
34
35
  WAIT_FAILED = 0xFFFFFFFF
36
+
37
+ ERROR_PRIVILEGE_NOT_HELD = 1314
38
+ ERROR_LOGON_TYPE_NOT_GRANTED = 0x569
35
39
  end
36
40
 
37
41
  # Define the functions needed to check with Service windows station
@@ -65,78 +69,78 @@ module Process
65
69
  class << self
66
70
  def create(args)
67
71
  unless args.kind_of?(Hash)
68
- raise TypeError, 'hash keyword arguments expected'
72
+ raise TypeError, "hash keyword arguments expected"
69
73
  end
70
74
 
71
- valid_keys = %w[
75
+ valid_keys = %w{
72
76
  app_name command_line inherit creation_flags cwd environment
73
77
  startup_info thread_inherit process_inherit close_handles with_logon
74
- domain password
75
- ]
78
+ domain password elevated
79
+ }
76
80
 
77
- valid_si_keys = %w[
81
+ valid_si_keys = %w{
78
82
  startf_flags desktop title x y x_size y_size x_count_chars
79
83
  y_count_chars fill_attribute sw_flags stdin stdout stderr
80
- ]
84
+ }
81
85
 
82
86
  # Set default values
83
87
  hash = {
84
- 'app_name' => nil,
85
- 'creation_flags' => 0,
86
- 'close_handles' => true
88
+ "app_name" => nil,
89
+ "creation_flags" => 0,
90
+ "close_handles" => true,
87
91
  }
88
92
 
89
93
  # Validate the keys, and convert symbols and case to lowercase strings.
90
- args.each{ |key, val|
94
+ args.each do |key, val|
91
95
  key = key.to_s.downcase
92
96
  unless valid_keys.include?(key)
93
97
  raise ArgumentError, "invalid key '#{key}'"
94
98
  end
95
99
  hash[key] = val
96
- }
100
+ end
97
101
 
98
102
  si_hash = {}
99
103
 
100
104
  # If the startup_info key is present, validate its subkeys
101
- if hash['startup_info']
102
- hash['startup_info'].each{ |key, val|
105
+ if hash["startup_info"]
106
+ hash["startup_info"].each do |key, val|
103
107
  key = key.to_s.downcase
104
108
  unless valid_si_keys.include?(key)
105
109
  raise ArgumentError, "invalid startup_info key '#{key}'"
106
110
  end
107
111
  si_hash[key] = val
108
- }
112
+ end
109
113
  end
110
114
 
111
115
  # The +command_line+ key is mandatory unless the +app_name+ key
112
116
  # is specified.
113
- unless hash['command_line']
114
- if hash['app_name']
115
- hash['command_line'] = hash['app_name']
116
- hash['app_name'] = nil
117
+ unless hash["command_line"]
118
+ if hash["app_name"]
119
+ hash["command_line"] = hash["app_name"]
120
+ hash["app_name"] = nil
117
121
  else
118
- raise ArgumentError, 'command_line or app_name must be specified'
122
+ raise ArgumentError, "command_line or app_name must be specified"
119
123
  end
120
124
  end
121
125
 
122
126
  env = nil
123
127
 
124
128
  # The env string should be passed as a string of ';' separated paths.
125
- if hash['environment']
126
- env = hash['environment']
129
+ if hash["environment"]
130
+ env = hash["environment"]
127
131
 
128
132
  unless env.respond_to?(:join)
129
- env = hash['environment'].split(File::PATH_SEPARATOR)
133
+ env = hash["environment"].split(File::PATH_SEPARATOR)
130
134
  end
131
135
 
132
- env = env.map{ |e| e + 0.chr }.join('') + 0.chr
133
- env.to_wide_string! if hash['with_logon']
136
+ env = env.map { |e| e + 0.chr }.join("") + 0.chr
137
+ env.to_wide_string! if hash["with_logon"]
134
138
  end
135
139
 
136
140
  # Process SECURITY_ATTRIBUTE structure
137
141
  process_security = nil
138
142
 
139
- if hash['process_inherit']
143
+ if hash["process_inherit"]
140
144
  process_security = SECURITY_ATTRIBUTES.new
141
145
  process_security[:nLength] = 12
142
146
  process_security[:bInheritHandle] = 1
@@ -145,7 +149,7 @@ module Process
145
149
  # Thread SECURITY_ATTRIBUTE structure
146
150
  thread_security = nil
147
151
 
148
- if hash['thread_inherit']
152
+ if hash["thread_inherit"]
149
153
  thread_security = SECURITY_ATTRIBUTES.new
150
154
  thread_security[:nLength] = 12
151
155
  thread_security[:bInheritHandle] = 1
@@ -156,7 +160,7 @@ module Process
156
160
  # will not work on JRuby because of the way it handles internal file
157
161
  # descriptors.
158
162
  #
159
- ['stdin', 'stdout', 'stderr'].each{ |io|
163
+ %w{stdin stdout stderr}.each do |io|
160
164
  if si_hash[io]
161
165
  if si_hash[io].respond_to?(:fileno)
162
166
  handle = get_osfhandle(si_hash[io].fileno)
@@ -187,129 +191,79 @@ module Process
187
191
  raise SystemCallError.new("SetHandleInformation", FFI.errno) unless bool
188
192
 
189
193
  si_hash[io] = handle
190
- si_hash['startf_flags'] ||= 0
191
- si_hash['startf_flags'] |= STARTF_USESTDHANDLES
192
- hash['inherit'] = true
194
+ si_hash["startf_flags"] ||= 0
195
+ si_hash["startf_flags"] |= STARTF_USESTDHANDLES
196
+ hash["inherit"] = true
193
197
  end
194
- }
198
+ end
195
199
 
196
200
  procinfo = PROCESS_INFORMATION.new
197
201
  startinfo = STARTUPINFO.new
198
202
 
199
203
  unless si_hash.empty?
200
204
  startinfo[:cb] = startinfo.size
201
- startinfo[:lpDesktop] = si_hash['desktop'] if si_hash['desktop']
202
- startinfo[:lpTitle] = si_hash['title'] if si_hash['title']
203
- startinfo[:dwX] = si_hash['x'] if si_hash['x']
204
- startinfo[:dwY] = si_hash['y'] if si_hash['y']
205
- startinfo[:dwXSize] = si_hash['x_size'] if si_hash['x_size']
206
- startinfo[:dwYSize] = si_hash['y_size'] if si_hash['y_size']
207
- startinfo[:dwXCountChars] = si_hash['x_count_chars'] if si_hash['x_count_chars']
208
- startinfo[:dwYCountChars] = si_hash['y_count_chars'] if si_hash['y_count_chars']
209
- startinfo[:dwFillAttribute] = si_hash['fill_attribute'] if si_hash['fill_attribute']
210
- startinfo[:dwFlags] = si_hash['startf_flags'] if si_hash['startf_flags']
211
- startinfo[:wShowWindow] = si_hash['sw_flags'] if si_hash['sw_flags']
205
+ startinfo[:lpDesktop] = si_hash["desktop"] if si_hash["desktop"]
206
+ startinfo[:lpTitle] = si_hash["title"] if si_hash["title"]
207
+ startinfo[:dwX] = si_hash["x"] if si_hash["x"]
208
+ startinfo[:dwY] = si_hash["y"] if si_hash["y"]
209
+ startinfo[:dwXSize] = si_hash["x_size"] if si_hash["x_size"]
210
+ startinfo[:dwYSize] = si_hash["y_size"] if si_hash["y_size"]
211
+ startinfo[:dwXCountChars] = si_hash["x_count_chars"] if si_hash["x_count_chars"]
212
+ startinfo[:dwYCountChars] = si_hash["y_count_chars"] if si_hash["y_count_chars"]
213
+ startinfo[:dwFillAttribute] = si_hash["fill_attribute"] if si_hash["fill_attribute"]
214
+ startinfo[:dwFlags] = si_hash["startf_flags"] if si_hash["startf_flags"]
215
+ startinfo[:wShowWindow] = si_hash["sw_flags"] if si_hash["sw_flags"]
212
216
  startinfo[:cbReserved2] = 0
213
- startinfo[:hStdInput] = si_hash['stdin'] if si_hash['stdin']
214
- startinfo[:hStdOutput] = si_hash['stdout'] if si_hash['stdout']
215
- startinfo[:hStdError] = si_hash['stderr'] if si_hash['stderr']
217
+ startinfo[:hStdInput] = si_hash["stdin"] if si_hash["stdin"]
218
+ startinfo[:hStdOutput] = si_hash["stdout"] if si_hash["stdout"]
219
+ startinfo[:hStdError] = si_hash["stderr"] if si_hash["stderr"]
216
220
  end
217
221
 
218
222
  app = nil
219
223
  cmd = nil
220
224
 
221
225
  # Convert strings to wide character strings if present
222
- if hash['app_name']
223
- app = hash['app_name'].to_wide_string
226
+ if hash["app_name"]
227
+ app = hash["app_name"].to_wide_string
224
228
  end
225
229
 
226
- if hash['command_line']
227
- cmd = hash['command_line'].to_wide_string
230
+ if hash["command_line"]
231
+ cmd = hash["command_line"].to_wide_string
228
232
  end
229
233
 
230
- if hash['cwd']
231
- cwd = hash['cwd'].to_wide_string
234
+ if hash["cwd"]
235
+ cwd = hash["cwd"].to_wide_string
232
236
  end
233
237
 
234
- inherit = hash['inherit'] ? 1 : 0
235
-
236
- if hash['with_logon']
237
- logon = hash['with_logon'].to_wide_string
238
-
239
- if hash['password']
240
- passwd = hash['password'].to_wide_string
241
- else
242
- raise ArgumentError, 'password must be specified if with_logon is used'
243
- end
244
-
245
- if hash['domain']
246
- domain = hash['domain'].to_wide_string
247
- end
248
-
249
- hash['creation_flags'] |= CREATE_UNICODE_ENVIRONMENT
238
+ inherit = hash["inherit"] ? 1 : 0
250
239
 
251
- winsta_name = FFI::MemoryPointer.new(:char, 256)
252
- return_size = FFI::MemoryPointer.new(:ulong)
240
+ if hash["with_logon"]
241
+ logon, passwd, domain = format_creds_from_hash(hash)
253
242
 
254
- bool = GetUserObjectInformationA(
255
- GetProcessWindowStation(), # Window station handle
256
- UOI_NAME, # Information to get
257
- winsta_name, # Buffer to receive information
258
- winsta_name.size, # Size of buffer
259
- return_size # Size filled into buffer
260
- )
261
-
262
- unless bool
263
- raise SystemCallError.new("GetUserObjectInformationA", FFI.errno)
264
- end
243
+ hash["creation_flags"] |= CREATE_UNICODE_ENVIRONMENT
265
244
 
266
- winsta_name = winsta_name.read_string(return_size.read_ulong)
245
+ winsta_name = get_windows_station_name
267
246
 
268
247
  # If running in the service windows station must do a log on to get
269
- # to the interactive desktop. Running process user account must have
248
+ # to the interactive desktop. The running process user account must have
270
249
  # the 'Replace a process level token' permission. This is necessary as
271
250
  # the logon (which happens with CreateProcessWithLogon) must have an
272
251
  # interactive windows station to attach to, which is created with the
273
- # LogonUser cann with the LOGON32_LOGON_INTERACTIVE flag.
274
- if winsta_name =~ /^Service-0x0-.*$/i
275
- token = FFI::MemoryPointer.new(:ulong)
276
-
277
- bool = LogonUserW(
278
- logon, # User
279
- domain, # Domain
280
- passwd, # Password
281
- LOGON32_LOGON_INTERACTIVE, # Logon Type
282
- LOGON32_PROVIDER_DEFAULT, # Logon Provider
283
- token # User token handle
284
- )
285
-
286
- unless bool
287
- raise SystemCallError.new("LogonUserW", FFI.errno)
288
- end
289
-
290
- token = token.read_ulong
291
-
292
- begin
293
- bool = CreateProcessAsUserW(
294
- token, # User token handle
295
- app, # App name
296
- cmd, # Command line
297
- process_security, # Process attributes
298
- thread_security, # Thread attributes
299
- inherit, # Inherit handles
300
- hash['creation_flags'], # Creation Flags
301
- env, # Environment
302
- cwd, # Working directory
303
- startinfo, # Startup Info
304
- procinfo # Process Info
305
- )
306
- ensure
307
- CloseHandle(token)
308
- end
309
-
310
- unless bool
311
- raise SystemCallError.new("CreateProcessAsUserW (You must hold the 'Replace a process level token' permission)", FFI.errno)
312
- end
252
+ # LogonUser call with the LOGON32_LOGON_INTERACTIVE flag.
253
+ #
254
+ # User Access Control (UAC) only applies to interactive logons, so we
255
+ # can simulate running a command 'elevated' by running it under a separate
256
+ # logon as a batch process.
257
+ 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
+
264
+ token = logon_user(logon, domain, passwd, logon_type)
265
+
266
+ create_process_as_user(token, app, cmd, process_security, thread_security, hash["creation_flags"], env, cwd, startinfo, procinfo)
313
267
  else
314
268
  bool = CreateProcessWithLogonW(
315
269
  logon, # User
@@ -318,7 +272,7 @@ module Process
318
272
  LOGON_WITH_PROFILE, # Logon flags
319
273
  app, # App name
320
274
  cmd, # Command line
321
- hash['creation_flags'], # Creation flags
275
+ hash["creation_flags"], # Creation flags
322
276
  env, # Environment
323
277
  cwd, # Working directory
324
278
  startinfo, # Startup Info
@@ -336,7 +290,7 @@ module Process
336
290
  process_security, # Process attributes
337
291
  thread_security, # Thread attributes
338
292
  inherit, # Inherit handles?
339
- hash['creation_flags'], # Creation flags
293
+ hash["creation_flags"], # Creation flags
340
294
  env, # Environment
341
295
  cwd, # Working directory
342
296
  startinfo, # Startup Info
@@ -350,7 +304,7 @@ module Process
350
304
 
351
305
  # Automatically close the process and thread handles in the
352
306
  # PROCESS_INFORMATION struct unless explicitly told not to.
353
- if hash['close_handles']
307
+ if hash["close_handles"]
354
308
  CloseHandle(procinfo[:hProcess])
355
309
  CloseHandle(procinfo[:hThread])
356
310
  # Clear these fields so callers don't attempt to close the handle
@@ -367,5 +321,90 @@ module Process
367
321
  procinfo[:dwThreadId]
368
322
  )
369
323
  end
324
+
325
+ def logon_user(user, domain, passwd, type, provider = LOGON32_PROVIDER_DEFAULT)
326
+ token = FFI::MemoryPointer.new(:ulong)
327
+
328
+ bool = LogonUserW(
329
+ user, # User
330
+ domain, # Domain
331
+ passwd, # Password
332
+ type, # Logon Type
333
+ provider, # Logon Provider
334
+ token # User token handle
335
+ )
336
+
337
+ unless bool
338
+ if (FFI.errno == ERROR_LOGON_TYPE_NOT_GRANTED) && (type == LOGON32_LOGON_BATCH)
339
+ user_utf8 = user.encode( "UTF-8", invalid: :replace, undef: :replace, replace: "" ).delete("\0")
340
+ raise SystemCallError.new("LogonUserW (User '#{user_utf8}' must hold 'Log on as a batch job' permissions.)", FFI.errno)
341
+ else
342
+ raise SystemCallError.new("LogonUserW", FFI.errno)
343
+ end
344
+ end
345
+
346
+ token.read_ulong
347
+ end
348
+
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
+ def get_windows_station_name
376
+ winsta_name = FFI::MemoryPointer.new(:char, 256)
377
+ return_size = FFI::MemoryPointer.new(:ulong)
378
+
379
+ bool = GetUserObjectInformationA(
380
+ GetProcessWindowStation(), # Window station handle
381
+ UOI_NAME, # Information to get
382
+ winsta_name, # Buffer to receive information
383
+ winsta_name.size, # Size of buffer
384
+ return_size # Size filled into buffer
385
+ )
386
+
387
+ unless bool
388
+ raise SystemCallError.new("GetUserObjectInformationA", FFI.errno)
389
+ end
390
+
391
+ winsta_name.read_string(return_size.read_ulong)
392
+ end
393
+
394
+ def format_creds_from_hash(hash)
395
+ logon = hash["with_logon"].to_wide_string
396
+
397
+ if hash["password"]
398
+ passwd = hash["password"].to_wide_string
399
+ else
400
+ raise ArgumentError, "password must be specified if with_logon is used"
401
+ end
402
+
403
+ if hash["domain"]
404
+ domain = hash["domain"].to_wide_string
405
+ end
406
+
407
+ [ logon, passwd, domain ]
408
+ end
370
409
  end
371
410
  end
@@ -1,6 +1,6 @@
1
1
  gemspec = eval(File.read(File.expand_path("../mixlib-shellout.gemspec", __FILE__)))
2
2
 
3
- gemspec.platform = Gem::Platform.new(["universal", "mingw32"])
3
+ gemspec.platform = Gem::Platform.new(%w{universal mingw32})
4
4
 
5
5
  gemspec.add_dependency "win32-process", "~> 0.8.2"
6
6
  gemspec.add_dependency "wmi-lite", "~> 1.0"
@@ -1,8 +1,8 @@
1
- $:.unshift(File.dirname(__FILE__) + '/lib')
2
- require 'mixlib/shellout/version'
1
+ $:.unshift(File.dirname(__FILE__) + "/lib")
2
+ require "mixlib/shellout/version"
3
3
 
4
4
  Gem::Specification.new do |s|
5
- s.name = 'mixlib-shellout'
5
+ s.name = "mixlib-shellout"
6
6
  s.version = Mixlib::ShellOut::VERSION
7
7
  s.platform = Gem::Platform::RUBY
8
8
  s.extra_rdoc_files = ["README.md", "LICENSE" ]
@@ -12,13 +12,14 @@ Gem::Specification.new do |s|
12
12
  s.email = "info@chef.io"
13
13
  s.homepage = "https://www.chef.io/"
14
14
 
15
- s.required_ruby_version = ">= 1.9.3"
15
+ s.required_ruby_version = ">= 2.2"
16
16
 
17
17
  s.add_development_dependency "rspec", "~> 3.0"
18
+ s.add_development_dependency "chefstyle"
18
19
 
19
20
  s.bindir = "bin"
20
21
  s.executables = []
21
- s.require_path = 'lib'
22
- s.files = %w(Gemfile Rakefile LICENSE README.md) + Dir.glob("*.gemspec") +
23
- Dir.glob("lib/**/*", File::FNM_DOTMATCH).reject {|f| File.directory?(f) }
22
+ s.require_path = "lib"
23
+ s.files = %w{Gemfile Rakefile LICENSE README.md} + Dir.glob("*.gemspec") +
24
+ Dir.glob("lib/**/*", File::FNM_DOTMATCH).reject { |f| File.directory?(f) }
24
25
  end