mixlib-shellout 2.2.7-universal-mingw32 → 2.3.0-universal-mingw32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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