diff --git a/lib/puppet/util/colors.rb b/lib/puppet/util/colors.rb
index ba8a3933b..016d070b4 100644
--- a/lib/puppet/util/colors.rb
+++ b/lib/puppet/util/colors.rb
@@ -1,202 +1,216 @@
require 'puppet/util/platform'
module Puppet::Util::Colors
BLACK = {:console => "\e[0;30m", :html => "color: #FFA0A0" }
RED = {:console => "\e[0;31m", :html => "color: #FFA0A0" }
GREEN = {:console => "\e[0;32m", :html => "color: #00CD00" }
YELLOW = {:console => "\e[0;33m", :html => "color: #FFFF60" }
BLUE = {:console => "\e[0;34m", :html => "color: #80A0FF" }
MAGENTA = {:console => "\e[0;35m", :html => "color: #FFA500" }
CYAN = {:console => "\e[0;36m", :html => "color: #40FFFF" }
WHITE = {:console => "\e[0;37m", :html => "color: #FFFFFF" }
HBLACK = {:console => "\e[1;30m", :html => "color: #FFA0A0" }
HRED = {:console => "\e[1;31m", :html => "color: #FFA0A0" }
HGREEN = {:console => "\e[1;32m", :html => "color: #00CD00" }
HYELLOW = {:console => "\e[1;33m", :html => "color: #FFFF60" }
HBLUE = {:console => "\e[1;34m", :html => "color: #80A0FF" }
HMAGENTA = {:console => "\e[1;35m", :html => "color: #FFA500" }
HCYAN = {:console => "\e[1;36m", :html => "color: #40FFFF" }
HWHITE = {:console => "\e[1;37m", :html => "color: #FFFFFF" }
BG_RED = {:console => "\e[0;41m", :html => "background: #FFA0A0"}
BG_GREEN = {:console => "\e[0;42m", :html => "background: #00CD00"}
BG_YELLOW = {:console => "\e[0;43m", :html => "background: #FFFF60"}
BG_BLUE = {:console => "\e[0;44m", :html => "background: #80A0FF"}
BG_MAGENTA = {:console => "\e[0;45m", :html => "background: #FFA500"}
BG_CYAN = {:console => "\e[0;46m", :html => "background: #40FFFF"}
BG_WHITE = {:console => "\e[0;47m", :html => "background: #FFFFFF"}
BG_HRED = {:console => "\e[1;41m", :html => "background: #FFA0A0"}
BG_HGREEN = {:console => "\e[1;42m", :html => "background: #00CD00"}
BG_HYELLOW = {:console => "\e[1;43m", :html => "background: #FFFF60"}
BG_HBLUE = {:console => "\e[1;44m", :html => "background: #80A0FF"}
BG_HMAGENTA = {:console => "\e[1;45m", :html => "background: #FFA500"}
BG_HCYAN = {:console => "\e[1;46m", :html => "background: #40FFFF"}
BG_HWHITE = {:console => "\e[1;47m", :html => "background: #FFFFFF"}
RESET = {:console => "\e[0m", :html => "" }
Colormap = {
:debug => WHITE,
:info => GREEN,
:notice => CYAN,
:warning => YELLOW,
:err => HMAGENTA,
:alert => RED,
:emerg => HRED,
:crit => HRED,
:black => BLACK,
:red => RED,
:green => GREEN,
:yellow => YELLOW,
:blue => BLUE,
:magenta => MAGENTA,
:cyan => CYAN,
:white => WHITE,
:hblack => HBLACK,
:hred => HRED,
:hgreen => HGREEN,
:hyellow => HYELLOW,
:hblue => HBLUE,
:hmagenta => HMAGENTA,
:hcyan => HCYAN,
:hwhite => HWHITE,
:bg_red => BG_RED,
:bg_green => BG_GREEN,
:bg_yellow => BG_YELLOW,
:bg_blue => BG_BLUE,
:bg_magenta => BG_MAGENTA,
:bg_cyan => BG_CYAN,
:bg_white => BG_WHITE,
:bg_hred => BG_HRED,
:bg_hgreen => BG_HGREEN,
:bg_hyellow => BG_HYELLOW,
:bg_hblue => BG_HBLUE,
:bg_hmagenta => BG_HMAGENTA,
:bg_hcyan => BG_HCYAN,
:bg_hwhite => BG_HWHITE,
:reset => { :console => "\e[m", :html => "" }
}
# We define console_has_color? at load time since it's checking the
# underlying platform which will not change, and we don't want to perform
# the check every time we use logging
if Puppet::Util::Platform.windows?
# We're on windows, need win32console for color to work
begin
require 'ffi'
require 'win32console'
# The win32console gem uses ANSI functions for writing to the console
# which doesn't work for unicode strings, e.g. module tool. Ruby 1.9
# does the same thing, but doesn't account for ANSI escape sequences
class WideConsole < Win32::Console
extend FFI::Library
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms687401(v=vs.85).aspx
# BOOL WINAPI WriteConsole(
# _In_ HANDLE hConsoleOutput,
# _In_ const VOID *lpBuffer,
# _In_ DWORD nNumberOfCharsToWrite,
# _Out_ LPDWORD lpNumberOfCharsWritten,
# _Reserved_ LPVOID lpReserved
# );
ffi_lib :kernel32
attach_function_private :WriteConsoleW,
[:handle, :lpcwstr, :dword, :lpdword, :lpvoid], :win32_bool
# typedef struct _COORD {
# SHORT X;
# SHORT Y;
# } COORD, *PCOORD;
class COORD < FFI::Struct
layout :X, :short,
:Y, :short
end
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms687410(v=vs.85).aspx
# BOOL WINAPI WriteConsoleOutputCharacter(
# _In_ HANDLE hConsoleOutput,
# _In_ LPCTSTR lpCharacter,
# _In_ DWORD nLength,
# _In_ COORD dwWriteCoord,
# _Out_ LPDWORD lpNumberOfCharsWritten
# );
ffi_lib :kernel32
attach_function_private :WriteConsoleOutputCharacterW,
[:handle, :lpcwstr, :dword, COORD, :lpdword], :win32_bool
def initialize(t = nil)
super(t)
end
def WriteChar(str, col, row)
writeCoord = COORD.new()
writeCoord[:X] = row
writeCoord[:Y] = col
- numberOfCharsWritten_ptr = FFI::MemoryPointer.new(:dword, 1)
- WriteConsoleOutputCharacterW(@handle, FFI::MemoryPointer.from_string_to_wide_string(str),
- str.length, writeCoord, numberOfCharsWritten_ptr)
- numberOfCharsWritten_ptr.read_dword
+ chars_written = 0
+ FFI::MemoryPointer.from_string_to_wide_string(str) do |msg_ptr|
+ FFI::MemoryPointer.new(:dword, 1) do |numberOfCharsWritten_ptr|
+ WriteConsoleOutputCharacterW(@handle, msg_ptr,
+ str.length, writeCoord, numberOfCharsWritten_ptr)
+ chars_written = numberOfCharsWritten_ptr.read_dword
+ end
+ end
+
+ chars_written
end
def Write(str)
- WriteConsoleW(@handle, FFI::MemoryPointer.from_string_to_wide_string(str),
- str.length, FFI::MemoryPointer.new(:dword, 1), FFI::MemoryPointer::NULL)
+ result = false
+ FFI::MemoryPointer.from_string_to_wide_string(str) do |msg_ptr|
+ FFI::MemoryPointer.new(:dword, 1) do |numberOfCharsWritten_ptr|
+ result = WriteConsoleW(@handle, msg_ptr,
+ str.length, FFI::MemoryPointer.new(:dword, 1),
+ FFI::MemoryPointer::NULL) != FFI::WIN32_FALSE
+ end
+ end
+
+ result
end
end
# Override the win32console's IO class so we can supply
# our own Console class
class WideIO < Win32::Console::ANSI::IO
def initialize(fd_std = :stdout)
super(fd_std)
handle = FD_STD_MAP[fd_std][1]
@Out = WideConsole.new(handle)
end
end
$stdout = WideIO.new(:stdout)
$stderr = WideIO.new(:stderr)
rescue LoadError
def console_has_color?
false
end
else
def console_has_color?
true
end
end
else
# On a posix system we can just enable it
def console_has_color?
true
end
end
def colorize(color, str)
case Puppet[:color]
when true, :ansi, "ansi", "yes"
if console_has_color?
console_color(color, str)
else
str
end
when :html, "html"
html_color(color, str)
else
str
end
end
def console_color(color, str)
Colormap[color][:console] +
str.gsub(RESET[:console], Colormap[color][:console]) +
RESET[:console]
end
def html_color(color, str)
span = '' % Colormap[color][:html]
"#{span}%s" % str.gsub(//, "\\0#{span}")
end
end
diff --git a/lib/puppet/util/execution.rb b/lib/puppet/util/execution.rb
index fb03510a9..c998d166c 100644
--- a/lib/puppet/util/execution.rb
+++ b/lib/puppet/util/execution.rb
@@ -1,315 +1,315 @@
module Puppet
require 'rbconfig'
require 'puppet/error'
# A command failed to execute.
# @api public
class ExecutionFailure < Puppet::Error
end
end
# This module defines methods for execution of system commands. It is intented for inclusion
# in classes that needs to execute system commands.
# @api public
module Puppet::Util::Execution
# This is the full output from a process. The object itself (a String) is the
# stdout of the process.
#
# @api public
class ProcessOutput < String
# @return [Integer] The exit status of the process
# @api public
attr_reader :exitstatus
# @api private
def initialize(value,exitstatus)
super(value)
@exitstatus = exitstatus
end
end
# The command can be a simple string, which is executed as-is, or an Array,
# which is treated as a set of command arguments to pass through.
#
# In either case, the command is passed directly to the shell, STDOUT and
# STDERR are connected together, and STDOUT will be streamed to the yielded
# pipe.
#
# @param command [String, Array] the command to execute as one string,
# or as parts in an array. The parts of the array are joined with one
# separating space between each entry when converting to the command line
# string to execute.
# @param failonfail [Boolean] (true) if the execution should fail with
# Exception on failure or not.
# @yield [pipe] to a block executing a subprocess
# @yieldparam pipe [IO] the opened pipe
# @yieldreturn [String] the output to return
# @raise [Puppet::ExecutionFailure] if the executed chiled process did not
# exit with status == 0 and `failonfail` is `true`.
# @return [String] a string with the output from the subprocess executed by
# the given block
#
# @see Kernel#open for `mode` values
# @api public
def self.execpipe(command, failonfail = true)
# Paste together an array with spaces. We used to paste directly
# together, no spaces, which made for odd invocations; the user had to
# include whitespace between arguments.
#
# Having two spaces is really not a big drama, since this passes to the
# shell anyhow, while no spaces makes for a small developer cost every
# time this is invoked. --daniel 2012-02-13
command_str = command.respond_to?(:join) ? command.join(' ') : command
if respond_to? :debug
debug "Executing '#{command_str}'"
else
Puppet.debug "Executing '#{command_str}'"
end
# force the run of the command with
# the user/system locale to "C" (via environment variables LANG and LC_*)
# it enables to have non localized output for some commands and therefore
# a predictable output
english_env = ENV.to_hash.merge( {'LANG' => 'C', 'LC_ALL' => 'C'} )
output = Puppet::Util.withenv(english_env) do
open("| #{command_str} 2>&1") do |pipe|
yield pipe
end
end
if failonfail
unless $CHILD_STATUS == 0
raise Puppet::ExecutionFailure, output
end
end
output
end
# Wraps execution of {execute} with mapping of exception to given exception (and output as argument).
# @raise [exception] under same conditions as {execute}, but raises the given `exception` with the output as argument
# @return (see execute)
# @api public
def self.execfail(command, exception)
output = execute(command)
return output
rescue Puppet::ExecutionFailure
raise exception, output, exception.backtrace
end
# Default empty options for {execute}
NoOptionsSpecified = {}
# Executes the desired command, and return the status and output.
# def execute(command, options)
# @param command [Array, String] the command to execute. If it is
# an Array the first element should be the executable and the rest of the
# elements should be the individual arguments to that executable.
# @param options [Hash] a Hash of options
# @option options [Boolean] :failonfail if this value is set to true, then this method will raise an error if the
# command is not executed successfully.
# @option options [Integer, String] :uid (nil) the user id of the user that the process should be run as
# @option options [Integer, String] :gid (nil) the group id of the group that the process should be run as
# @option options [Boolean] :combine sets whether or not to combine stdout/stderr in the output
# @option options [String] :stdinfile (nil) sets a file that can be used for stdin. Passing a string for stdin is not currently
# supported.
# @option options [Boolean] :squelch (true) if true, ignore stdout / stderr completely.
# @option options [Boolean] :override_locale (true) by default (and if this option is set to true), we will temporarily override
# the user/system locale to "C" (via environment variables LANG and LC_*) while we are executing the command.
# This ensures that the output of the command will be formatted consistently, making it predictable for parsing.
# Passing in a value of false for this option will allow the command to be executed using the user/system locale.
# @option options [Hash<{String => String}>] :custom_environment ({}) a hash of key/value pairs to set as environment variables for the duration
# of the command.
# @return [Puppet::Util::Execution::ProcessOutput] output as specified by options
# @raise [Puppet::ExecutionFailure] if the executed chiled process did not exit with status == 0 and `failonfail` is
# `true`.
# @note Unfortunately, the default behavior for failonfail and combine (since
# 0.22.4 and 0.24.7, respectively) depend on whether options are specified
# or not. If specified, then failonfail and combine default to false (even
# when the options specified are neither failonfail nor combine). If no
# options are specified, then failonfail and combine default to true.
# @comment See commits efe9a833c and d32d7f30
# @api public
#
def self.execute(command, options = NoOptionsSpecified)
# specifying these here rather than in the method signature to allow callers to pass in a partial
# set of overrides without affecting the default values for options that they don't pass in
default_options = {
:failonfail => NoOptionsSpecified.equal?(options),
:uid => nil,
:gid => nil,
:combine => NoOptionsSpecified.equal?(options),
:stdinfile => nil,
:squelch => false,
:override_locale => true,
:custom_environment => {},
}
options = default_options.merge(options)
if command.is_a?(Array)
command = command.flatten.map(&:to_s)
str = command.join(" ")
elsif command.is_a?(String)
str = command
end
if respond_to? :debug
debug "Executing '#{str}'"
else
Puppet.debug "Executing '#{str}'"
end
null_file = Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null'
stdin = File.open(options[:stdinfile] || null_file, 'r')
stdout = options[:squelch] ? File.open(null_file, 'w') : Tempfile.new('puppet')
stderr = options[:combine] ? stdout : File.open(null_file, 'w')
exec_args = [command, options, stdin, stdout, stderr]
if execution_stub = Puppet::Util::ExecutionStub.current_value
return execution_stub.call(*exec_args)
elsif Puppet.features.posix?
child_pid = execute_posix(*exec_args)
exit_status = Process.waitpid2(child_pid).last.exitstatus
elsif Puppet.features.microsoft_windows?
process_info = execute_windows(*exec_args)
begin
exit_status = Puppet::Util::Windows::Process.wait_process(process_info.process_handle)
ensure
- Puppet::Util::Windows::Process.CloseHandle(process_info.process_handle)
- Puppet::Util::Windows::Process.CloseHandle(process_info.thread_handle)
+ FFI::WIN32.CloseHandle(process_info.process_handle)
+ FFI::WIN32.CloseHandle(process_info.thread_handle)
end
end
[stdin, stdout, stderr].each {|io| io.close rescue nil}
# read output in if required
unless options[:squelch]
output = wait_for_output(stdout)
Puppet.warning "Could not get output" unless output
end
if options[:failonfail] and exit_status != 0
raise Puppet::ExecutionFailure, "Execution of '#{str}' returned #{exit_status}: #{output.strip}"
end
Puppet::Util::Execution::ProcessOutput.new(output || '', exit_status)
end
# Returns the path to the ruby executable (available via Config object, even if
# it's not in the PATH... so this is slightly safer than just using Puppet::Util.which)
# @return [String] the path to the Ruby executable
# @api private
#
def self.ruby_path()
File.join(RbConfig::CONFIG['bindir'],
RbConfig::CONFIG['ruby_install_name'] + RbConfig::CONFIG['EXEEXT']).
sub(/.*\s.*/m, '"\&"')
end
# Because some modules provide their own version of this method.
class << self
alias util_execute execute
end
# This is private method.
# @comment see call to private_class_method after method definition
# @api private
#
def self.execute_posix(command, options, stdin, stdout, stderr)
child_pid = Puppet::Util.safe_posix_fork(stdin, stdout, stderr) do
# We can't just call Array(command), and rely on it returning
# things like ['foo'], when passed ['foo'], because
# Array(command) will call command.to_a internally, which when
# given a string can end up doing Very Bad Things(TM), such as
# turning "/tmp/foo;\r\n /bin/echo" into ["/tmp/foo;\r\n", " /bin/echo"]
command = [command].flatten
Process.setsid
begin
Puppet::Util::SUIDManager.change_privileges(options[:uid], options[:gid], true)
# if the caller has requested that we override locale environment variables,
if (options[:override_locale]) then
# loop over them and clear them
Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |name| ENV.delete(name) }
# set LANG and LC_ALL to 'C' so that the command will have consistent, predictable output
# it's OK to manipulate these directly rather than, e.g., via "withenv", because we are in
# a forked process.
ENV['LANG'] = 'C'
ENV['LC_ALL'] = 'C'
end
# unset all of the user-related environment variables so that different methods of starting puppet
# (automatic start during boot, via 'service', via /etc/init.d, etc.) won't have unexpected side
# effects relating to user / home dir environment vars.
# it's OK to manipulate these directly rather than, e.g., via "withenv", because we are in
# a forked process.
Puppet::Util::POSIX::USER_ENV_VARS.each { |name| ENV.delete(name) }
options[:custom_environment] ||= {}
Puppet::Util.withenv(options[:custom_environment]) do
Kernel.exec(*command)
end
rescue => detail
Puppet.log_exception(detail, "Could not execute posix command: #{detail}")
exit!(1)
end
end
child_pid
end
private_class_method :execute_posix
# This is private method.
# @comment see call to private_class_method after method definition
# @api private
#
def self.execute_windows(command, options, stdin, stdout, stderr)
command = command.map do |part|
part.include?(' ') ? %Q["#{part.gsub(/"/, '\"')}"] : part
end.join(" ") if command.is_a?(Array)
options[:custom_environment] ||= {}
Puppet::Util.withenv(options[:custom_environment]) do
Puppet::Util::Windows::Process.execute(command, options, stdin, stdout, stderr)
end
end
private_class_method :execute_windows
# This is private method.
# @comment see call to private_class_method after method definition
# @api private
#
def self.wait_for_output(stdout)
# Make sure the file's actually been written. This is basically a race
# condition, and is probably a horrible way to handle it, but, well, oh
# well.
# (If this method were treated as private / inaccessible from outside of this file, we shouldn't have to worry
# about a race condition because all of the places that we call this from are preceded by a call to "waitpid2",
# meaning that the processes responsible for writing the file have completed before we get here.)
2.times do |try|
if Puppet::FileSystem.exist?(stdout.path)
stdout.open
begin
return stdout.read
ensure
stdout.close
stdout.unlink
end
else
time_to_sleep = try / 2.0
Puppet.warning "Waiting for output; will sleep #{time_to_sleep} seconds"
sleep(time_to_sleep)
end
end
nil
end
private_class_method :wait_for_output
end
diff --git a/lib/puppet/util/windows/adsi.rb b/lib/puppet/util/windows/adsi.rb
index 4335d3d99..85172c83f 100644
--- a/lib/puppet/util/windows/adsi.rb
+++ b/lib/puppet/util/windows/adsi.rb
@@ -1,392 +1,394 @@
module Puppet::Util::Windows::ADSI
require 'ffi'
class << self
extend FFI::Library
def connectable?(uri)
begin
!! connect(uri)
rescue
false
end
end
def connect(uri)
begin
WIN32OLE.connect(uri)
rescue Exception => e
raise Puppet::Error.new( "ADSI connection error: #{e}", e )
end
end
def create(name, resource_type)
Puppet::Util::Windows::ADSI.connect(computer_uri).Create(resource_type, name)
end
def delete(name, resource_type)
Puppet::Util::Windows::ADSI.connect(computer_uri).Delete(resource_type, name)
end
# taken from winbase.h
MAX_COMPUTERNAME_LENGTH = 31
def computer_name
unless @computer_name
max_length = MAX_COMPUTERNAME_LENGTH + 1 # NULL terminated
- buffer = FFI::MemoryPointer.new(max_length * 2) # wide string
- buffer_size = FFI::MemoryPointer.new(:dword, 1)
- buffer_size.write_dword(max_length) # length in TCHARs
-
- if GetComputerNameW(buffer, buffer_size) == FFI::WIN32_FALSE
- raise Puppet::Util::Windows::Error.new("Failed to get computer name")
+ FFI::MemoryPointer.new(max_length * 2) do |buffer| # wide string
+ FFI::MemoryPointer.new(:dword, 1) do |buffer_size|
+ buffer_size.write_dword(max_length) # length in TCHARs
+
+ if GetComputerNameW(buffer, buffer_size) == FFI::WIN32_FALSE
+ raise Puppet::Util::Windows::Error.new("Failed to get computer name")
+ end
+ @computer_name = buffer.read_wide_string(buffer_size.read_dword)
+ end
end
- @computer_name = buffer.read_wide_string(buffer_size.read_dword)
end
@computer_name
end
def computer_uri(host = '.')
"WinNT://#{host}"
end
def wmi_resource_uri( host = '.' )
"winmgmts:{impersonationLevel=impersonate}!//#{host}/root/cimv2"
end
# @api private
def sid_uri_safe(sid)
return sid_uri(sid) if sid.kind_of?(Win32::Security::SID)
begin
sid = Win32::Security::SID.new(Win32::Security::SID.string_to_sid(sid))
sid_uri(sid)
rescue Win32::Security::SID::Error
return nil
end
end
def sid_uri(sid)
raise Puppet::Error.new( "Must use a valid SID object" ) if !sid.kind_of?(Win32::Security::SID)
"WinNT://#{sid.to_s}"
end
def uri(resource_name, resource_type, host = '.')
"#{computer_uri(host)}/#{resource_name},#{resource_type}"
end
def wmi_connection
connect(wmi_resource_uri)
end
def execquery(query)
wmi_connection.execquery(query)
end
def sid_for_account(name)
Puppet.deprecation_warning "Puppet::Util::Windows::ADSI.sid_for_account is deprecated and will be removed in 3.0, use Puppet::Util::Windows::SID.name_to_sid instead."
Puppet::Util::Windows::Security.name_to_sid(name)
end
ffi_convention :stdcall
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms724295(v=vs.85).aspx
# BOOL WINAPI GetComputerName(
# _Out_ LPTSTR lpBuffer,
# _Inout_ LPDWORD lpnSize
# );
ffi_lib :kernel32
attach_function_private :GetComputerNameW,
[:lpwstr, :lpdword], :win32_bool
end
class User
extend Enumerable
attr_accessor :native_user
attr_reader :name, :sid
def initialize(name, native_user = nil)
@name = name
@native_user = native_user
end
def self.parse_name(name)
if name =~ /\//
raise Puppet::Error.new( "Value must be in DOMAIN\\user style syntax" )
end
matches = name.scan(/((.*)\\)?(.*)/)
domain = matches[0][1] || '.'
account = matches[0][2]
return account, domain
end
def native_user
@native_user ||= Puppet::Util::Windows::ADSI.connect(self.class.uri(*self.class.parse_name(@name)))
end
def sid
@sid ||= Puppet::Util::Windows::Security.octet_string_to_sid_object(native_user.objectSID)
end
def self.uri(name, host = '.')
if sid_uri = Puppet::Util::Windows::ADSI.sid_uri_safe(name) then return sid_uri end
host = '.' if ['NT AUTHORITY', 'BUILTIN', Socket.gethostname].include?(host)
Puppet::Util::Windows::ADSI.uri(name, 'user', host)
end
def uri
self.class.uri(sid.account, sid.domain)
end
def self.logon(name, password)
Puppet::Util::Windows::User.password_is?(name, password)
end
def [](attribute)
native_user.Get(attribute)
end
def []=(attribute, value)
native_user.Put(attribute, value)
end
def commit
begin
native_user.SetInfo unless native_user.nil?
rescue Exception => e
raise Puppet::Error.new( "User update failed: #{e}", e )
end
self
end
def password_is?(password)
self.class.logon(name, password)
end
def add_flag(flag_name, value)
flag = native_user.Get(flag_name) rescue 0
native_user.Put(flag_name, flag | value)
commit
end
def password=(password)
native_user.SetPassword(password)
commit
fADS_UF_DONT_EXPIRE_PASSWD = 0x10000
add_flag("UserFlags", fADS_UF_DONT_EXPIRE_PASSWD)
end
def groups
# WIN32OLE objects aren't enumerable, so no map
groups = []
native_user.Groups.each {|g| groups << g.Name} rescue nil
groups
end
def add_to_groups(*group_names)
group_names.each do |group_name|
Puppet::Util::Windows::ADSI::Group.new(group_name).add_member_sids(sid)
end
end
alias add_to_group add_to_groups
def remove_from_groups(*group_names)
group_names.each do |group_name|
Puppet::Util::Windows::ADSI::Group.new(group_name).remove_member_sids(sid)
end
end
alias remove_from_group remove_from_groups
def set_groups(desired_groups, minimum = true)
return if desired_groups.nil? or desired_groups.empty?
desired_groups = desired_groups.split(',').map(&:strip)
current_groups = self.groups
# First we add the user to all the groups it should be in but isn't
groups_to_add = desired_groups - current_groups
add_to_groups(*groups_to_add)
# Then we remove the user from all groups it is in but shouldn't be, if
# that's been requested
groups_to_remove = current_groups - desired_groups
remove_from_groups(*groups_to_remove) unless minimum
end
def self.create(name)
# Windows error 1379: The specified local group already exists.
raise Puppet::Error.new( "Cannot create user if group '#{name}' exists." ) if Puppet::Util::Windows::ADSI::Group.exists? name
new(name, Puppet::Util::Windows::ADSI.create(name, 'user'))
end
def self.exists?(name)
Puppet::Util::Windows::ADSI::connectable?(User.uri(*User.parse_name(name)))
end
def self.delete(name)
Puppet::Util::Windows::ADSI.delete(name, 'user')
end
def self.each(&block)
wql = Puppet::Util::Windows::ADSI.execquery('select name from win32_useraccount where localaccount = "TRUE"')
users = []
wql.each do |u|
users << new(u.name)
end
users.each(&block)
end
end
class UserProfile
def self.delete(sid)
begin
Puppet::Util::Windows::ADSI.wmi_connection.Delete("Win32_UserProfile.SID='#{sid}'")
rescue => e
# http://social.technet.microsoft.com/Forums/en/ITCG/thread/0f190051-ac96-4bf1-a47f-6b864bfacee5
# Prior to Vista SP1, there's no builtin way to programmatically
# delete user profiles (except for delprof.exe). So try to delete
# but warn if we fail
raise e unless e.message.include?('80041010')
Puppet.warning "Cannot delete user profile for '#{sid}' prior to Vista SP1"
end
end
end
class Group
extend Enumerable
attr_accessor :native_group
attr_reader :name
def initialize(name, native_group = nil)
@name = name
@native_group = native_group
end
def uri
self.class.uri(name)
end
def self.uri(name, host = '.')
if sid_uri = Puppet::Util::Windows::ADSI.sid_uri_safe(name) then return sid_uri end
Puppet::Util::Windows::ADSI.uri(name, 'group', host)
end
def native_group
@native_group ||= Puppet::Util::Windows::ADSI.connect(uri)
end
def commit
begin
native_group.SetInfo unless native_group.nil?
rescue Exception => e
raise Puppet::Error.new( "Group update failed: #{e}", e )
end
self
end
def self.name_sid_hash(names)
return [] if names.nil? or names.empty?
sids = names.map do |name|
sid = Puppet::Util::Windows::Security.name_to_sid_object(name)
raise Puppet::Error.new( "Could not resolve username: #{name}" ) if !sid
[sid.to_s, sid]
end
Hash[ sids ]
end
def add_members(*names)
Puppet.deprecation_warning('Puppet::Util::Windows::ADSI::Group#add_members is deprecated; please use Puppet::Util::Windows::ADSI::Group#add_member_sids')
sids = self.class.name_sid_hash(names)
add_member_sids(*sids.values)
end
alias add_member add_members
def remove_members(*names)
Puppet.deprecation_warning('Puppet::Util::Windows::ADSI::Group#remove_members is deprecated; please use Puppet::Util::Windows::ADSI::Group#remove_member_sids')
sids = self.class.name_sid_hash(names)
remove_member_sids(*sids.values)
end
alias remove_member remove_members
def add_member_sids(*sids)
sids.each do |sid|
native_group.Add(Puppet::Util::Windows::ADSI.sid_uri(sid))
end
end
def remove_member_sids(*sids)
sids.each do |sid|
native_group.Remove(Puppet::Util::Windows::ADSI.sid_uri(sid))
end
end
def members
# WIN32OLE objects aren't enumerable, so no map
members = []
native_group.Members.each {|m| members << m.Name}
members
end
def member_sids
sids = []
native_group.Members.each do |m|
sids << Puppet::Util::Windows::Security.octet_string_to_sid_object(m.objectSID)
end
sids
end
def set_members(desired_members)
return if desired_members.nil? or desired_members.empty?
current_hash = Hash[ self.member_sids.map { |sid| [sid.to_s, sid] } ]
desired_hash = self.class.name_sid_hash(desired_members)
# First we add all missing members
members_to_add = (desired_hash.keys - current_hash.keys).map { |sid| desired_hash[sid] }
add_member_sids(*members_to_add)
# Then we remove all extra members
members_to_remove = (current_hash.keys - desired_hash.keys).map { |sid| current_hash[sid] }
remove_member_sids(*members_to_remove)
end
def self.create(name)
# Windows error 2224: The account already exists.
raise Puppet::Error.new( "Cannot create group if user '#{name}' exists." ) if Puppet::Util::Windows::ADSI::User.exists? name
new(name, Puppet::Util::Windows::ADSI.create(name, 'group'))
end
def self.exists?(name)
Puppet::Util::Windows::ADSI.connectable?(Group.uri(name))
end
def self.delete(name)
Puppet::Util::Windows::ADSI.delete(name, 'group')
end
def self.each(&block)
wql = Puppet::Util::Windows::ADSI.execquery( 'select name from win32_group where localaccount = "TRUE"' )
groups = []
wql.each do |g|
groups << new(g.name)
end
groups.each(&block)
end
end
end
diff --git a/lib/puppet/util/windows/api_types.rb b/lib/puppet/util/windows/api_types.rb
index 8f236060f..c109a34f1 100644
--- a/lib/puppet/util/windows/api_types.rb
+++ b/lib/puppet/util/windows/api_types.rb
@@ -1,121 +1,162 @@
require 'ffi'
require 'puppet/util/windows/string'
module Puppet::Util::Windows::APITypes
module ::FFI
WIN32_FALSE = 0
end
module ::FFI::Library
# Wrapper method for attach_function + private
def attach_function_private(*args)
attach_function(*args)
private args[0]
end
end
class ::FFI::Pointer
NULL_HANDLE = 0
NULL_TERMINATOR_WCHAR = 0
- def self.from_string_to_wide_string(str)
+ def self.from_string_to_wide_string(str, &block)
str = Puppet::Util::Windows::String.wide_string(str)
- ptr = FFI::MemoryPointer.new(:byte, str.bytesize)
- # uchar here is synonymous with byte
- ptr.put_array_of_uchar(0, str.bytes.to_a)
+ FFI::MemoryPointer.new(:byte, str.bytesize) do |ptr|
+ # uchar here is synonymous with byte
+ ptr.put_array_of_uchar(0, str.bytes.to_a)
- ptr
+ yield ptr
+ end
+
+ # ptr has already had free called, so nothing to return
+ nil
end
def read_win32_bool
# BOOL is always a 32-bit integer in Win32
# some Win32 APIs return 1 for true, while others are non-0
read_int32 != FFI::WIN32_FALSE
end
alias_method :read_dword, :read_uint32
def read_handle
type_size == 4 ? read_uint32 : read_uint64
end
alias_method :read_wchar, :read_uint16
def read_wide_string(char_length)
# char_length is number of wide chars (typically excluding NULLs), *not* bytes
str = get_bytes(0, char_length * 2).force_encoding('UTF-16LE')
str.encode(Encoding.default_external)
end
def read_arbitrary_wide_string_up_to(max_char_length = 512)
# max_char_length is number of wide chars (typically excluding NULLs), *not* bytes
# use a pointer to read one UTF-16LE char (2 bytes) at a time
wchar_ptr = FFI::Pointer.new(:wchar, address)
# now iterate 2 bytes at a time until an offset lower than max_char_length is found
0.upto(max_char_length - 1) do |i|
if wchar_ptr[i].read_wchar == NULL_TERMINATOR_WCHAR
return read_wide_string(i)
end
end
read_wide_string(max_char_length)
end
+ def read_win32_local_pointer(&block)
+ ptr = nil
+ begin
+ ptr = read_pointer
+ yield ptr
+ ensure
+ if ptr && ! ptr.null?
+ if FFI::WIN32::LocalFree(ptr.address) != FFI::Pointer::NULL_HANDLE
+ Puppet.debug "LocalFree memory leak"
+ end
+ end
+ end
+
+ # ptr has already had LocalFree called, so nothing to return
+ nil
+ end
+
alias_method :write_dword, :write_uint32
end
# FFI Types
# https://github.com/ffi/ffi/wiki/Types
# Windows - Common Data Types
# http://msdn.microsoft.com/en-us/library/cc230309.aspx
# Windows Data Types
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa383751(v=vs.85).aspx
FFI.typedef :uint16, :word
FFI.typedef :uint32, :dword
# uintptr_t is defined in an FFI conf as platform specific, either
# ulong_long on x64 or just ulong on x86
FFI.typedef :uintptr_t, :handle
# buffer_inout is similar to pointer (platform specific), but optimized for buffers
FFI.typedef :buffer_inout, :lpwstr
# buffer_in is similar to pointer (platform specific), but optimized for CONST read only buffers
FFI.typedef :buffer_in, :lpcwstr
# string is also similar to pointer, but should be used for const char *
# NOTE that this is not wide, useful only for A suffixed functions
FFI.typedef :string, :lpcstr
# pointer in FFI is platform specific
# NOTE: for API calls with reserved lpvoid parameters, pass a FFI::Pointer::NULL
FFI.typedef :pointer, :lpcvoid
FFI.typedef :pointer, :lpvoid
FFI.typedef :pointer, :lpword
FFI.typedef :pointer, :lpdword
FFI.typedef :pointer, :pdword
FFI.typedef :pointer, :phandle
FFI.typedef :pointer, :ulong_ptr
FFI.typedef :pointer, :pbool
# any time LONG / ULONG is in a win32 API definition DO NOT USE platform specific width
# which is what FFI uses by default
# instead create new aliases for these very special cases
# NOTE: not a good idea to redefine FFI :ulong since other typedefs may rely on it
FFI.typedef :uint32, :win32_ulong
FFI.typedef :int32, :win32_long
# FFI bool can be only 1 byte at times,
# Win32 BOOL is a signed int, and is always 4 bytes, even on x64
# http://blogs.msdn.com/b/oldnewthing/archive/2011/03/28/10146459.aspx
FFI.typedef :int32, :win32_bool
# NOTE: FFI already defines (u)short as a 16-bit (un)signed like this:
# FFI.typedef :uint16, :ushort
# FFI.typedef :int16, :short
# 8 bits per byte
FFI.typedef :uchar, :byte
FFI.typedef :uint16, :wchar
+
+ module ::FFI::WIN32
+ extend ::FFI::Library
+
+ ffi_convention :stdcall
+
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/aa366730(v=vs.85).aspx
+ # HLOCAL WINAPI LocalFree(
+ # _In_ HLOCAL hMem
+ # );
+ ffi_lib :kernel32
+ attach_function :LocalFree, [:handle], :handle
+
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724211(v=vs.85).aspx
+ # BOOL WINAPI CloseHandle(
+ # _In_ HANDLE hObject
+ # );
+ ffi_lib :kernel32
+ attach_function_private :CloseHandle, [:handle], :win32_bool
+ end
end
diff --git a/lib/puppet/util/windows/error.rb b/lib/puppet/util/windows/error.rb
index 7d14307e4..913258624 100644
--- a/lib/puppet/util/windows/error.rb
+++ b/lib/puppet/util/windows/error.rb
@@ -1,98 +1,87 @@
require 'puppet/util/windows'
# represents an error resulting from a Win32 error code
class Puppet::Util::Windows::Error < Puppet::Error
require 'ffi'
extend FFI::Library
attr_reader :code
def initialize(message, code = @@GetLastError.call(), original = nil)
super(message + ": #{self.class.format_error_code(code)}", original)
@code = code
end
# Helper method that wraps FormatMessage that returns a human readable string.
def self.format_error_code(code)
# specifying 0 will look for LANGID in the following order
# 1.Language neutral
# 2.Thread LANGID, based on the thread's locale value
# 3.User default LANGID, based on the user's default locale value
# 4.System default LANGID, based on the system default locale value
# 5.US English
dwLanguageId = 0
flags = FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_ARGUMENT_ARRAY |
FORMAT_MESSAGE_IGNORE_INSERTS |
FORMAT_MESSAGE_MAX_WIDTH_MASK
- # this pointer actually points to a :lpwstr (pointer) since we're letting Windows allocate for us
- buffer_ptr = FFI::MemoryPointer.new(:pointer, 1)
+ error_string = ''
- begin
+ # this pointer actually points to a :lpwstr (pointer) since we're letting Windows allocate for us
+ FFI::MemoryPointer.new(:pointer, 1) do |buffer_ptr|
length = FormatMessageW(flags, FFI::Pointer::NULL, code, dwLanguageId,
buffer_ptr, 0, FFI::Pointer::NULL)
if length == FFI::WIN32_FALSE
# can't raise same error type here or potentially recurse infinitely
raise Puppet::Error.new("FormatMessageW could not format code #{code}")
end
# returns an FFI::Pointer with autorelease set to false, which is what we want
- wide_string_ptr = buffer_ptr.read_pointer
-
- if wide_string_ptr.null?
- raise Puppet::Error.new("FormatMessageW failed to allocate buffer for code #{code}")
- end
-
- return wide_string_ptr.read_wide_string(length)
- ensure
- if ! wide_string_ptr.nil? && ! wide_string_ptr.null?
- if LocalFree(wide_string_ptr.address) != FFI::Pointer::NULL_HANDLE
- Puppet.debug "LocalFree memory leak"
+ buffer_ptr.read_win32_local_pointer do |wide_string_ptr|
+ if wide_string_ptr.null?
+ raise Puppet::Error.new("FormatMessageW failed to allocate buffer for code #{code}")
end
+
+ error_string = wide_string_ptr.read_wide_string(length)
end
end
+
+ error_string
end
FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100
FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200
FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x00002000
FORMAT_MESSAGE_MAX_WIDTH_MASK = 0x000000FF
ffi_convention :stdcall
# NOTE: It seems like FFI.errno is already implemented as GetLastError... or is it?
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms679360(v=vs.85).aspx
# DWORD WINAPI GetLastError(void);
# HACK: unfortunately using FFI.errno or attach_function to hook GetLastError in
# FFI like the following will not work. Something internal to FFI appears to
# be stomping out the value of GetLastError when calling via FFI.
# attach_function_private :GetLastError, [], :dword
require 'Win32API'
@@GetLastError = Win32API.new('kernel32', 'GetLastError', [], 'L')
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms679351(v=vs.85).aspx
# DWORD WINAPI FormatMessage(
# _In_ DWORD dwFlags,
# _In_opt_ LPCVOID lpSource,
# _In_ DWORD dwMessageId,
# _In_ DWORD dwLanguageId,
# _Out_ LPTSTR lpBuffer,
# _In_ DWORD nSize,
# _In_opt_ va_list *Arguments
# );
# NOTE: since we're not preallocating the buffer, use a :pointer for lpBuffer
ffi_lib :kernel32
attach_function_private :FormatMessageW,
[:dword, :lpcvoid, :dword, :dword, :pointer, :dword, :pointer], :dword
-
- # http://msdn.microsoft.com/en-us/library/windows/desktop/aa366730(v=vs.85).aspx
- # HLOCAL WINAPI LocalFree(
- # _In_ HLOCAL hMem
- # );
- ffi_lib :kernel32
- attach_function_private :LocalFree, [:handle], :handle
end
diff --git a/lib/puppet/util/windows/file.rb b/lib/puppet/util/windows/file.rb
index 7998f7521..75c87550f 100644
--- a/lib/puppet/util/windows/file.rb
+++ b/lib/puppet/util/windows/file.rb
@@ -1,294 +1,309 @@
require 'puppet/util/windows'
module Puppet::Util::Windows::File
require 'ffi'
extend FFI::Library
extend Puppet::Util::Windows::String
def replace_file(target, source)
target_encoded = wide_string(target.to_s)
source_encoded = wide_string(source.to_s)
flags = 0x1
backup_file = nil
result = ReplaceFileW(
target_encoded,
source_encoded,
backup_file,
flags,
FFI::Pointer::NULL,
FFI::Pointer::NULL
)
return true if result != FFI::WIN32_FALSE
raise Puppet::Util::Windows::Error.new("ReplaceFile(#{target}, #{source})")
end
module_function :replace_file
def move_file_ex(source, target, flags = 0)
result = MoveFileExW(wide_string(source.to_s),
wide_string(target.to_s),
flags)
return true if result != FFI::WIN32_FALSE
raise Puppet::Util::Windows::Error.
new("MoveFileEx(#{source}, #{target}, #{flags.to_s(8)})")
end
module_function :move_file_ex
def symlink(target, symlink)
flags = File.directory?(target) ? 0x1 : 0x0
result = CreateSymbolicLinkW(wide_string(symlink.to_s),
wide_string(target.to_s), flags)
return true if result != FFI::WIN32_FALSE
raise Puppet::Util::Windows::Error.new(
"CreateSymbolicLink(#{symlink}, #{target}, #{flags.to_s(8)})")
end
module_function :symlink
INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF #define INVALID_FILE_ATTRIBUTES (DWORD (-1))
def self.get_file_attributes(file_name)
result = GetFileAttributesW(wide_string(file_name.to_s))
return result unless result == INVALID_FILE_ATTRIBUTES
raise Puppet::Util::Windows::Error.new("GetFileAttributes(#{file_name})")
end
INVALID_HANDLE_VALUE = -1 #define INVALID_HANDLE_VALUE ((HANDLE)(LONG_PTR)-1)
def self.create_file(file_name, desired_access, share_mode, security_attributes,
creation_disposition, flags_and_attributes, template_file_handle)
result = CreateFileW(wide_string(file_name.to_s),
desired_access, share_mode, security_attributes, creation_disposition,
flags_and_attributes, template_file_handle)
return result unless result == INVALID_HANDLE_VALUE
raise Puppet::Util::Windows::Error.new(
"CreateFile(#{file_name}, #{desired_access.to_s(8)}, #{share_mode.to_s(8)}, " +
"#{security_attributes}, #{creation_disposition.to_s(8)}, " +
"#{flags_and_attributes.to_s(8)}, #{template_file_handle})")
end
+ def self.get_reparse_point_data(handle, &block)
+ # must be multiple of 1024, min 10240
+ FFI::MemoryPointer.new(REPARSE_DATA_BUFFER.size) do |reparse_data_buffer_ptr|
+ device_io_control(handle, FSCTL_GET_REPARSE_POINT, nil, reparse_data_buffer_ptr)
+ yield REPARSE_DATA_BUFFER.new(reparse_data_buffer_ptr)
+ end
+
+ # underlying struct MemoryPointer has been cleaned up by this point, nothing to return
+ nil
+ end
+
def self.device_io_control(handle, io_control_code, in_buffer = nil, out_buffer = nil)
if out_buffer.nil?
raise Puppet::Util::Windows::Error.new("out_buffer is required")
end
- result = DeviceIoControl(
- handle,
- io_control_code,
- in_buffer, in_buffer.nil? ? 0 : in_buffer.size,
- out_buffer, out_buffer.size,
- FFI::MemoryPointer.new(:dword, 1),
- nil
- )
+ FFI::MemoryPointer.new(:dword, 1) do |bytes_returned_ptr|
+ result = DeviceIoControl(
+ handle,
+ io_control_code,
+ in_buffer, in_buffer.nil? ? 0 : in_buffer.size,
+ out_buffer, out_buffer.size,
+ bytes_returned_ptr,
+ nil
+ )
+
+ if result == FFI::WIN32_FALSE
+ raise Puppet::Util::Windows::Error.new(
+ "DeviceIoControl(#{handle}, #{io_control_code}, " +
+ "#{in_buffer}, #{in_buffer ? in_buffer.size : ''}, " +
+ "#{out_buffer}, #{out_buffer ? out_buffer.size : ''}")
+ end
+ end
- return out_buffer if result != FFI::WIN32_FALSE
- raise Puppet::Util::Windows::Error.new(
- "DeviceIoControl(#{handle}, #{io_control_code}, " +
- "#{in_buffer}, #{in_buffer ? in_buffer.size : ''}, " +
- "#{out_buffer}, #{out_buffer ? out_buffer.size : ''}")
+ out_buffer
end
FILE_ATTRIBUTE_REPARSE_POINT = 0x400
def symlink?(file_name)
begin
attributes = get_file_attributes(file_name)
(attributes & FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT
rescue
# raised INVALID_FILE_ATTRIBUTES is equivalent to file not found
false
end
end
module_function :symlink?
GENERIC_READ = 0x80000000
FILE_SHARE_READ = 1
OPEN_EXISTING = 3
FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
def self.open_symlink(link_name)
begin
yield handle = create_file(
- wide_string(link_name.to_s),
+ link_name,
GENERIC_READ,
FILE_SHARE_READ,
nil, # security_attributes
OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
0) # template_file
ensure
- CloseHandle(handle) if handle
+ FFI::WIN32.CloseHandle(handle) if handle
end
+
+ # handle has had CloseHandle called against it, so nothing to return
+ nil
end
def readlink(link_name)
+ link = nil
open_symlink(link_name) do |handle|
- resolve_symlink(handle)
+ link = resolve_symlink(handle)
end
+
+ link
end
module_function :readlink
def stat(file_name)
file_name = file_name.to_s # accomodate PathName or String
stat = File.stat(file_name)
singleton_class = class << stat; self; end
target_path = file_name
if symlink?(file_name)
target_path = readlink(file_name)
link_ftype = File.stat(target_path).ftype
# sigh, monkey patch instance method for instance, and close over link_ftype
singleton_class.send(:define_method, :ftype) do
link_ftype
end
end
singleton_class.send(:define_method, :mode) do
Puppet::Util::Windows::Security.get_mode(target_path)
end
stat
end
module_function :stat
def lstat(file_name)
file_name = file_name.to_s # accomodate PathName or String
# monkey'ing around!
stat = File.lstat(file_name)
singleton_class = class << stat; self; end
singleton_class.send(:define_method, :mode) do
Puppet::Util::Windows::Security.get_mode(file_name)
end
if symlink?(file_name)
def stat.ftype
"link"
end
end
stat
end
module_function :lstat
private
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa364571(v=vs.85).aspx
FSCTL_GET_REPARSE_POINT = 0x900a8
def self.resolve_symlink(handle)
- # must be multiple of 1024, min 10240
- out_buffer = FFI::MemoryPointer.new(REPARSE_DATA_BUFFER.size)
- device_io_control(handle, FSCTL_GET_REPARSE_POINT, nil, out_buffer)
+ path = nil
+ get_reparse_point_data(handle) do |reparse_data|
+ offset = reparse_data[:PrintNameOffset]
+ length = reparse_data[:PrintNameLength]
- reparse_data = REPARSE_DATA_BUFFER.new(out_buffer)
- offset = reparse_data[:PrintNameOffset]
- length = reparse_data[:PrintNameLength]
+ ptr = reparse_data.pointer + reparse_data.offset_of(:PathBuffer) + offset
+ path = ptr.read_wide_string(length / 2) # length is bytes, need UTF-16 wchars
+ end
- result = reparse_data[:PathBuffer].to_a[offset, length].pack('C*')
- result.force_encoding('UTF-16LE').encode(Encoding.default_external)
+ path
end
ffi_convention :stdcall
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa365512(v=vs.85).aspx
# BOOL WINAPI ReplaceFile(
# _In_ LPCTSTR lpReplacedFileName,
# _In_ LPCTSTR lpReplacementFileName,
# _In_opt_ LPCTSTR lpBackupFileName,
# _In_ DWORD dwReplaceFlags - 0x1 REPLACEFILE_WRITE_THROUGH,
# 0x2 REPLACEFILE_IGNORE_MERGE_ERRORS,
# 0x4 REPLACEFILE_IGNORE_ACL_ERRORS
# _Reserved_ LPVOID lpExclude,
# _Reserved_ LPVOID lpReserved
# );
ffi_lib :kernel32
attach_function_private :ReplaceFileW,
[:lpcwstr, :lpcwstr, :lpcwstr, :dword, :lpvoid, :lpvoid], :win32_bool
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa365240(v=vs.85).aspx
# BOOL WINAPI MoveFileEx(
# _In_ LPCTSTR lpExistingFileName,
# _In_opt_ LPCTSTR lpNewFileName,
# _In_ DWORD dwFlags
# );
ffi_lib :kernel32
attach_function_private :MoveFileExW,
[:lpcwstr, :lpcwstr, :dword], :win32_bool
# BOOLEAN WINAPI CreateSymbolicLink(
# _In_ LPTSTR lpSymlinkFileName, - symbolic link to be created
# _In_ LPTSTR lpTargetFileName, - name of target for symbolic link
# _In_ DWORD dwFlags - 0x0 target is a file, 0x1 target is a directory
# );
# rescue on Windows < 6.0 so that code doesn't explode
begin
ffi_lib :kernel32
attach_function_private :CreateSymbolicLinkW,
[:lpwstr, :lpwstr, :dword], :win32_bool
rescue LoadError
end
# DWORD WINAPI GetFileAttributes(
# _In_ LPCTSTR lpFileName
# );
ffi_lib :kernel32
attach_function_private :GetFileAttributesW,
[:lpcwstr], :dword
# HANDLE WINAPI CreateFile(
# _In_ LPCTSTR lpFileName,
# _In_ DWORD dwDesiredAccess,
# _In_ DWORD dwShareMode,
# _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
# _In_ DWORD dwCreationDisposition,
# _In_ DWORD dwFlagsAndAttributes,
# _In_opt_ HANDLE hTemplateFile
# );
ffi_lib :kernel32
attach_function_private :CreateFileW,
[:lpcwstr, :dword, :dword, :pointer, :dword, :dword, :handle], :handle
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa363216(v=vs.85).aspx
# BOOL WINAPI DeviceIoControl(
# _In_ HANDLE hDevice,
# _In_ DWORD dwIoControlCode,
# _In_opt_ LPVOID lpInBuffer,
# _In_ DWORD nInBufferSize,
# _Out_opt_ LPVOID lpOutBuffer,
# _In_ DWORD nOutBufferSize,
# _Out_opt_ LPDWORD lpBytesReturned,
# _Inout_opt_ LPOVERLAPPED lpOverlapped
# );
ffi_lib :kernel32
attach_function_private :DeviceIoControl,
[:handle, :dword, :lpvoid, :dword, :lpvoid, :dword, :lpdword, :pointer], :win32_bool
MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16384
# REPARSE_DATA_BUFFER
# http://msdn.microsoft.com/en-us/library/cc232006.aspx
# http://msdn.microsoft.com/en-us/library/windows/hardware/ff552012(v=vs.85).aspx
# struct is always MAXIMUM_REPARSE_DATA_BUFFER_SIZE bytes
class REPARSE_DATA_BUFFER < FFI::Struct
layout :ReparseTag, :win32_ulong,
:ReparseDataLength, :ushort,
:Reserved, :ushort,
:SubstituteNameOffset, :ushort,
:SubstituteNameLength, :ushort,
:PrintNameOffset, :ushort,
:PrintNameLength, :ushort,
:Flags, :win32_ulong,
# max less above fields dword / uint 4 bytes, ushort 2 bytes
# technically a WCHAR buffer, but we care about size in bytes here
:PathBuffer, [:byte, MAXIMUM_REPARSE_DATA_BUFFER_SIZE - 20]
end
-
- # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724211(v=vs.85).aspx
- # BOOL WINAPI CloseHandle(
- # _In_ HANDLE hObject
- # );
- ffi_lib :kernel32
- attach_function_private :CloseHandle, [:handle], :win32_bool
end
diff --git a/lib/puppet/util/windows/process.rb b/lib/puppet/util/windows/process.rb
index dfe5c81e5..29d8ef165 100644
--- a/lib/puppet/util/windows/process.rb
+++ b/lib/puppet/util/windows/process.rb
@@ -1,312 +1,336 @@
require 'puppet/util/windows'
require 'win32/process'
require 'ffi'
module Puppet::Util::Windows::Process
extend Puppet::Util::Windows::String
extend FFI::Library
WAIT_TIMEOUT = 0x102
def execute(command, arguments, stdin, stdout, stderr)
Process.create( :command_line => command, :startup_info => {:stdin => stdin, :stdout => stdout, :stderr => stderr}, :close_handles => false )
end
module_function :execute
def wait_process(handle)
while WaitForSingleObject(handle, 0) == WAIT_TIMEOUT
sleep(1)
end
- exit_status = FFI::MemoryPointer.new(:dword, 1)
- if GetExitCodeProcess(handle, exit_status) == FFI::WIN32_FALSE
- raise Puppet::Util::Windows::Error.new("Failed to get child process exit code")
+ exit_status = -1
+ FFI::MemoryPointer.new(:dword, 1) do |exit_status_ptr|
+ if GetExitCodeProcess(handle, exit_status_ptr) == FFI::WIN32_FALSE
+ raise Puppet::Util::Windows::Error.new("Failed to get child process exit code")
+ end
+ exit_status = exit_status_ptr.read_dword
+
+ # $CHILD_STATUS is not set when calling win32/process Process.create
+ # and since it's read-only, we can't set it. But we can execute a
+ # a shell that simply returns the desired exit status, which has the
+ # desired effect.
+ %x{#{ENV['COMSPEC']} /c exit #{exit_status}}
end
- exit_status = exit_status.read_dword
-
- # $CHILD_STATUS is not set when calling win32/process Process.create
- # and since it's read-only, we can't set it. But we can execute a
- # a shell that simply returns the desired exit status, which has the
- # desired effect.
- %x{#{ENV['COMSPEC']} /c exit #{exit_status}}
exit_status
end
module_function :wait_process
def get_current_process
# this pseudo-handle does not require closing per MSDN docs
GetCurrentProcess()
end
module_function :get_current_process
- def open_process_token(handle, desired_access)
- token_handle_ptr = FFI::MemoryPointer.new(:handle, 1)
- result = OpenProcessToken(handle, desired_access, token_handle_ptr)
- if result == FFI::WIN32_FALSE
- raise Puppet::Util::Windows::Error.new(
- "OpenProcessToken(#{handle}, #{desired_access.to_s(8)}, #{token_handle_ptr})")
- end
-
+ def open_process_token(handle, desired_access, &block)
+ token_handle = nil
begin
- yield token_handle = token_handle_ptr.read_handle
+ FFI::MemoryPointer.new(:handle, 1) do |token_handle_ptr|
+ result = OpenProcessToken(handle, desired_access, token_handle_ptr)
+ if result == FFI::WIN32_FALSE
+ raise Puppet::Util::Windows::Error.new(
+ "OpenProcessToken(#{handle}, #{desired_access.to_s(8)}, #{token_handle_ptr})")
+ end
+
+ yield token_handle = token_handle_ptr.read_handle
+ end
+
+ token_handle
ensure
- CloseHandle(token_handle)
+ FFI::WIN32.CloseHandle(token_handle) if token_handle
end
+
+ # token_handle has had CloseHandle called against it, so nothing to return
+ nil
end
module_function :open_process_token
- def lookup_privilege_value(name, system_name = '')
- luid = FFI::MemoryPointer.new(LUID.size)
- result = LookupPrivilegeValueW(
- wide_string(system_name),
- wide_string(name.to_s),
- luid
- )
-
- return LUID.new(luid) if result != FFI::WIN32_FALSE
- raise Puppet::Util::Windows::Error.new(
- "LookupPrivilegeValue(#{system_name}, #{name}, #{luid})")
- end
- module_function :lookup_privilege_value
+ def lookup_privilege_value(name, system_name = '', &block)
+ FFI::MemoryPointer.new(LUID.size) do |luid_ptr|
+ result = LookupPrivilegeValueW(
+ wide_string(system_name),
+ wide_string(name.to_s),
+ luid_ptr
+ )
- def get_token_information(token_handle, token_information)
- # to determine buffer size
- return_length_ptr = FFI::MemoryPointer.new(:dword, 1)
- result = GetTokenInformation(token_handle, token_information, nil, 0, return_length_ptr)
- return_length = return_length_ptr.read_dword
+ if result == FFI::WIN32_FALSE
+ raise Puppet::Util::Windows::Error.new(
+ "LookupPrivilegeValue(#{system_name}, #{name}, #{luid_ptr})")
+ end
- if return_length <= 0
- raise Puppet::Util::Windows::Error.new(
- "GetTokenInformation(#{token_handle}, #{token_information}, nil, 0, #{return_length_ptr})")
+ yield LUID.new(luid_ptr)
end
- # re-call API with properly sized buffer for all results
- token_information_buf = FFI::MemoryPointer.new(return_length)
- result = GetTokenInformation(token_handle, token_information,
- token_information_buf, return_length, return_length_ptr)
+ # the underlying MemoryPointer for LUID is cleaned up by this point
+ nil
+ end
+ module_function :lookup_privilege_value
- if result == FFI::WIN32_FALSE
- raise Puppet::Util::Windows::Error.new(
- "GetTokenInformation(#{token_handle}, #{token_information}, #{token_information_buf}, " +
- "#{return_length}, #{return_length_ptr})")
+ def get_token_information(token_handle, token_information, &block)
+ # to determine buffer size
+ FFI::MemoryPointer.new(:dword, 1) do |return_length_ptr|
+ result = GetTokenInformation(token_handle, token_information, nil, 0, return_length_ptr)
+ return_length = return_length_ptr.read_dword
+
+ if return_length <= 0
+ raise Puppet::Util::Windows::Error.new(
+ "GetTokenInformation(#{token_handle}, #{token_information}, nil, 0, #{return_length_ptr})")
+ end
+
+ # re-call API with properly sized buffer for all results
+ FFI::MemoryPointer.new(return_length) do |token_information_buf|
+ result = GetTokenInformation(token_handle, token_information,
+ token_information_buf, return_length, return_length_ptr)
+
+ if result == FFI::WIN32_FALSE
+ raise Puppet::Util::Windows::Error.new(
+ "GetTokenInformation(#{token_handle}, #{token_information}, #{token_information_buf}, " +
+ "#{return_length}, #{return_length_ptr})")
+ end
+
+ yield token_information_buf
+ end
end
- token_information_buf
+ # GetTokenInformation buffer has been cleaned up by this point, nothing to return
+ nil
end
module_function :get_token_information
def parse_token_information_as_token_privileges(token_information_buf)
raw_privileges = TOKEN_PRIVILEGES.new(token_information_buf)
privileges = { :count => raw_privileges[:PrivilegeCount], :privileges => [] }
offset = token_information_buf + TOKEN_PRIVILEGES.offset_of(:Privileges)
privilege_ptr = FFI::Pointer.new(LUID_AND_ATTRIBUTES, offset)
# extract each instance of LUID_AND_ATTRIBUTES
0.upto(privileges[:count] - 1) do |i|
privileges[:privileges] << LUID_AND_ATTRIBUTES.new(privilege_ptr[i])
end
privileges
end
module_function :parse_token_information_as_token_privileges
def parse_token_information_as_token_elevation(token_information_buf)
TOKEN_ELEVATION.new(token_information_buf)
end
module_function :parse_token_information_as_token_elevation
TOKEN_ALL_ACCESS = 0xF01FF
ERROR_NO_SUCH_PRIVILEGE = 1313
def process_privilege_symlink?
+ privilege_symlink = false
handle = get_current_process
open_process_token(handle, TOKEN_ALL_ACCESS) do |token_handle|
- luid = lookup_privilege_value('SeCreateSymbolicLinkPrivilege')
- token_info = get_token_information(token_handle, :TokenPrivileges)
- token_privileges = parse_token_information_as_token_privileges(token_info)
- token_privileges[:privileges].any? { |p| p[:Luid].values == luid.values }
+ lookup_privilege_value('SeCreateSymbolicLinkPrivilege') do |luid|
+ get_token_information(token_handle, :TokenPrivileges) do |token_info|
+ token_privileges = parse_token_information_as_token_privileges(token_info)
+ privilege_symlink = token_privileges[:privileges].any? { |p| p[:Luid].values == luid.values }
+ end
+ end
end
+
+ privilege_symlink
rescue Puppet::Util::Windows::Error => e
if e.code == ERROR_NO_SUCH_PRIVILEGE
false # pre-Vista
else
raise e
end
end
module_function :process_privilege_symlink?
TOKEN_QUERY = 0x0008
# Returns whether or not the owner of the current process is running
# with elevated security privileges.
#
# Only supported on Windows Vista or later.
#
def elevated_security?
- handle = get_current_process
- open_process_token(handle, TOKEN_QUERY) do |token_handle|
- token_info = get_token_information(token_handle, :TokenElevation)
- token_elevation = parse_token_information_as_token_elevation(token_info)
- # TokenIsElevated member of the TOKEN_ELEVATION struct
- token_elevation[:TokenIsElevated] != 0
- end
- rescue Puppet::Util::Windows::Error => e
- if e.code == ERROR_NO_SUCH_PRIVILEGE
- false # pre-Vista
- else
- raise e
+ # default / pre-Vista
+ elevated = false
+ handle = nil
+
+ begin
+ handle = get_current_process
+ open_process_token(handle, TOKEN_QUERY) do |token_handle|
+ get_token_information(token_handle, :TokenElevation) do |token_info|
+ token_elevation = parse_token_information_as_token_elevation(token_info)
+ # TokenIsElevated member of the TOKEN_ELEVATION struct
+ elevated = token_elevation[:TokenIsElevated] != 0
+ end
+ end
+
+ elevated
+ rescue Puppet::Util::Windows::Error => e
+ raise e if e.code != ERROR_NO_SUCH_PRIVILEGE
+ ensure
+ FFI::WIN32.CloseHandle(handle) if handle
end
- ensure
- CloseHandle(handle)
end
module_function :elevated_security?
ffi_convention :stdcall
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms687032(v=vs.85).aspx
# DWORD WINAPI WaitForSingleObject(
# _In_ HANDLE hHandle,
# _In_ DWORD dwMilliseconds
# );
ffi_lib :kernel32
attach_function_private :WaitForSingleObject,
[:handle, :dword], :dword
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms683189(v=vs.85).aspx
# BOOL WINAPI GetExitCodeProcess(
# _In_ HANDLE hProcess,
# _Out_ LPDWORD lpExitCode
# );
ffi_lib :kernel32
attach_function_private :GetExitCodeProcess,
[:handle, :lpdword], :win32_bool
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms683179(v=vs.85).aspx
# HANDLE WINAPI GetCurrentProcess(void);
ffi_lib :kernel32
attach_function_private :GetCurrentProcess, [], :handle
- # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724211(v=vs.85).aspx
- # BOOL WINAPI CloseHandle(
- # _In_ HANDLE hObject
- # );
- ffi_lib :kernel32
- attach_function_private :CloseHandle, [:handle], :win32_bool
-
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa379295(v=vs.85).aspx
# BOOL WINAPI OpenProcessToken(
# _In_ HANDLE ProcessHandle,
# _In_ DWORD DesiredAccess,
# _Out_ PHANDLE TokenHandle
# );
ffi_lib :advapi32
attach_function_private :OpenProcessToken,
[:handle, :dword, :phandle], :win32_bool
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa379261(v=vs.85).aspx
# typedef struct _LUID {
# DWORD LowPart;
# LONG HighPart;
# } LUID, *PLUID;
class LUID < FFI::Struct
layout :LowPart, :dword,
:HighPart, :win32_long
end
# http://msdn.microsoft.com/en-us/library/Windows/desktop/aa379180(v=vs.85).aspx
# BOOL WINAPI LookupPrivilegeValue(
# _In_opt_ LPCTSTR lpSystemName,
# _In_ LPCTSTR lpName,
# _Out_ PLUID lpLuid
# );
ffi_lib :advapi32
attach_function_private :LookupPrivilegeValueW,
[:lpcwstr, :lpcwstr, :pointer], :win32_bool
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa379626(v=vs.85).aspx
TOKEN_INFORMATION_CLASS = enum(
:TokenUser, 1,
:TokenGroups,
:TokenPrivileges,
:TokenOwner,
:TokenPrimaryGroup,
:TokenDefaultDacl,
:TokenSource,
:TokenType,
:TokenImpersonationLevel,
:TokenStatistics,
:TokenRestrictedSids,
:TokenSessionId,
:TokenGroupsAndPrivileges,
:TokenSessionReference,
:TokenSandBoxInert,
:TokenAuditPolicy,
:TokenOrigin,
:TokenElevationType,
:TokenLinkedToken,
:TokenElevation,
:TokenHasRestrictions,
:TokenAccessInformation,
:TokenVirtualizationAllowed,
:TokenVirtualizationEnabled,
:TokenIntegrityLevel,
:TokenUIAccess,
:TokenMandatoryPolicy,
:TokenLogonSid,
:TokenIsAppContainer,
:TokenCapabilities,
:TokenAppContainerSid,
:TokenAppContainerNumber,
:TokenUserClaimAttributes,
:TokenDeviceClaimAttributes,
:TokenRestrictedUserClaimAttributes,
:TokenRestrictedDeviceClaimAttributes,
:TokenDeviceGroups,
:TokenRestrictedDeviceGroups,
:TokenSecurityAttributes,
:TokenIsRestricted,
:MaxTokenInfoClass
)
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa379263(v=vs.85).aspx
# typedef struct _LUID_AND_ATTRIBUTES {
# LUID Luid;
# DWORD Attributes;
# } LUID_AND_ATTRIBUTES, *PLUID_AND_ATTRIBUTES;
class LUID_AND_ATTRIBUTES < FFI::Struct
layout :Luid, LUID,
:Attributes, :dword
end
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa379630(v=vs.85).aspx
# typedef struct _TOKEN_PRIVILEGES {
# DWORD PrivilegeCount;
# LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY];
# } TOKEN_PRIVILEGES, *PTOKEN_PRIVILEGES;
class TOKEN_PRIVILEGES < FFI::Struct
layout :PrivilegeCount, :dword,
:Privileges, [LUID_AND_ATTRIBUTES, 1] # placeholder for offset
end
# http://msdn.microsoft.com/en-us/library/windows/desktop/bb530717(v=vs.85).aspx
# typedef struct _TOKEN_ELEVATION {
# DWORD TokenIsElevated;
# } TOKEN_ELEVATION, *PTOKEN_ELEVATION;
class TOKEN_ELEVATION < FFI::Struct
layout :TokenIsElevated, :dword
end
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa446671(v=vs.85).aspx
# BOOL WINAPI GetTokenInformation(
# _In_ HANDLE TokenHandle,
# _In_ TOKEN_INFORMATION_CLASS TokenInformationClass,
# _Out_opt_ LPVOID TokenInformation,
# _In_ DWORD TokenInformationLength,
# _Out_ PDWORD ReturnLength
# );
ffi_lib :advapi32
attach_function_private :GetTokenInformation,
[:handle, TOKEN_INFORMATION_CLASS, :lpvoid, :dword, :pdword ], :win32_bool
end
diff --git a/lib/puppet/util/windows/security.rb b/lib/puppet/util/windows/security.rb
index 38cf018b0..6d07bbb1d 100644
--- a/lib/puppet/util/windows/security.rb
+++ b/lib/puppet/util/windows/security.rb
@@ -1,661 +1,662 @@
# This class maps POSIX owner, group, and modes to the Windows
# security model, and back.
#
# The primary goal of this mapping is to ensure that owner, group, and
# modes can be round-tripped in a consistent and deterministic
# way. Otherwise, Puppet might think file resources are out-of-sync
# every time it runs. A secondary goal is to provide equivalent
# permissions for common use-cases. For example, setting the owner to
# "Administrators", group to "Users", and mode to 750 (which also
# denies access to everyone else.
#
# There are some well-known problems mapping windows and POSIX
# permissions due to differences between the two security
# models. Search for "POSIX permission mapping leak". In POSIX, access
# to a file is determined solely based on the most specific class
# (user, group, other). So a mode of 460 would deny write access to
# the owner even if they are a member of the group. But in Windows,
# the entire access control list is walked until the user is
# explicitly denied or allowed (denied take precedence, and if neither
# occurs they are denied). As a result, a user could be allowed access
# based on their group membership. To solve this problem, other people
# have used deny access control entries to more closely model POSIX,
# but this introduces a lot of complexity.
#
# In general, this implementation only supports "typical" permissions,
# where group permissions are a subset of user, and other permissions
# are a subset of group, e.g. 754, but not 467. However, there are
# some Windows quirks to be aware of.
#
# * The owner can be either a user or group SID, and most system files
# are owned by the Administrators group.
# * The group can be either a user or group SID.
# * Unexpected results can occur if the owner and group are the
# same, but the user and group classes are different, e.g. 750. In
# this case, it is not possible to allow write access to the owner,
# but not the group. As a result, the actual permissions set on the
# file would be 770.
# * In general, only privileged users can set the owner, group, or
# change the mode for files they do not own. In 2003, the user must
# be a member of the Administrators group. In Vista/2008, the user
# must be running with elevated privileges.
# * A file/dir can be deleted by anyone with the DELETE access right
# OR by anyone that has the FILE_DELETE_CHILD access right for the
# parent. See http://support.microsoft.com/kb/238018. But on Unix,
# the user must have write access to the file/dir AND execute access
# to all of the parent path components.
# * Many access control entries are inherited from parent directories,
# and it is common for file/dirs to have more than 3 entries,
# e.g. Users, Power Users, Administrators, SYSTEM, etc, which cannot
# be mapped into the 3 class POSIX model. The get_mode method will
# set the S_IEXTRA bit flag indicating that an access control entry
# was found whose SID is neither the owner, group, or other. This
# enables Puppet to detect when file/dirs are out-of-sync,
# especially those that Puppet did not create, but is attempting
# to manage.
# * A special case of this is S_ISYSTEM_MISSING, which is set when the
# SYSTEM permissions are *not* present on the DACL.
# * On Unix, the owner and group can be modified without changing the
# mode. But on Windows, an access control entry specifies which SID
# it applies to. As a result, the set_owner and set_group methods
# automatically rebuild the access control list based on the new
# (and different) owner or group.
require 'puppet/util/windows'
require 'pathname'
require 'ffi'
require 'win32/security'
require 'windows/file'
require 'windows/handle'
require 'windows/security'
require 'windows/process'
require 'windows/memory'
require 'windows/msvcrt/buffer'
require 'windows/volume'
module Puppet::Util::Windows::Security
include ::Windows::File
include ::Windows::Handle
include ::Windows::Security
include ::Windows::Process
include ::Windows::Memory
include ::Windows::MSVCRT::Buffer
include ::Windows::Volume
include Puppet::Util::Windows::SID
extend Puppet::Util::Windows::Security
extend FFI::Library
# file modes
S_IRUSR = 0000400
S_IRGRP = 0000040
S_IROTH = 0000004
S_IWUSR = 0000200
S_IWGRP = 0000020
S_IWOTH = 0000002
S_IXUSR = 0000100
S_IXGRP = 0000010
S_IXOTH = 0000001
S_IRWXU = 0000700
S_IRWXG = 0000070
S_IRWXO = 0000007
S_ISVTX = 0001000
S_IEXTRA = 02000000 # represents an extra ace
S_ISYSTEM_MISSING = 04000000
# constants that are missing from Windows::Security
PROTECTED_DACL_SECURITY_INFORMATION = 0x80000000
UNPROTECTED_DACL_SECURITY_INFORMATION = 0x20000000
NO_INHERITANCE = 0x0
SE_DACL_PROTECTED = 0x1000
# Set the owner of the object referenced by +path+ to the specified
# +owner_sid+. The owner sid should be of the form "S-1-5-32-544"
# and can either be a user or group. Only a user with the
# SE_RESTORE_NAME privilege in their process token can overwrite the
# object's owner to something other than the current user.
def set_owner(owner_sid, path)
sd = get_security_descriptor(path)
if owner_sid != sd.owner
sd.owner = owner_sid
set_security_descriptor(path, sd)
end
end
# Get the owner of the object referenced by +path+. The returned
# value is a SID string, e.g. "S-1-5-32-544". Any user with read
# access to an object can get the owner. Only a user with the
# SE_BACKUP_NAME privilege in their process token can get the owner
# for objects they do not have read access to.
def get_owner(path)
return unless supports_acl?(path)
get_security_descriptor(path).owner
end
# Set the owner of the object referenced by +path+ to the specified
# +group_sid+. The group sid should be of the form "S-1-5-32-544"
# and can either be a user or group. Any user with WRITE_OWNER
# access to the object can change the group (regardless of whether
# the current user belongs to that group or not).
def set_group(group_sid, path)
sd = get_security_descriptor(path)
if group_sid != sd.group
sd.group = group_sid
set_security_descriptor(path, sd)
end
end
# Get the group of the object referenced by +path+. The returned
# value is a SID string, e.g. "S-1-5-32-544". Any user with read
# access to an object can get the group. Only a user with the
# SE_BACKUP_NAME privilege in their process token can get the group
# for objects they do not have read access to.
def get_group(path)
return unless supports_acl?(path)
get_security_descriptor(path).group
end
def supports_acl?(path)
flags = 0.chr * 4
root = Pathname.new(path).enum_for(:ascend).to_a.last.to_s
# 'A trailing backslash is required'
root = "#{root}\\" unless root =~ /[\/\\]$/
unless GetVolumeInformation(root, nil, 0, nil, nil, flags, nil, 0)
raise Puppet::Util::Windows::Error.new("Failed to get volume information")
end
(flags.unpack('L')[0] & Windows::File::FILE_PERSISTENT_ACLS) != 0
end
def get_attributes(path)
attributes = GetFileAttributes(path)
raise Puppet::Util::Windows::Error.new("Failed to get file attributes") if attributes == INVALID_FILE_ATTRIBUTES
attributes
end
def add_attributes(path, flags)
oldattrs = get_attributes(path)
if (oldattrs | flags) != oldattrs
set_attributes(path, oldattrs | flags)
end
end
def remove_attributes(path, flags)
oldattrs = get_attributes(path)
if (oldattrs & ~flags) != oldattrs
set_attributes(path, oldattrs & ~flags)
end
end
def set_attributes(path, flags)
raise Puppet::Util::Windows::Error.new("Failed to set file attributes") unless SetFileAttributes(path, flags)
end
MASK_TO_MODE = {
FILE_GENERIC_READ => S_IROTH,
FILE_GENERIC_WRITE => S_IWOTH,
(FILE_GENERIC_EXECUTE & ~FILE_READ_ATTRIBUTES) => S_IXOTH
}
def get_aces_for_path_by_sid(path, sid)
get_security_descriptor(path).dacl.select { |ace| ace.sid == sid }
end
# Get the mode of the object referenced by +path+. The returned
# integer value represents the POSIX-style read, write, and execute
# modes for the user, group, and other classes, e.g. 0640. Any user
# with read access to an object can get the mode. Only a user with
# the SE_BACKUP_NAME privilege in their process token can get the
# mode for objects they do not have read access to.
def get_mode(path)
return unless supports_acl?(path)
well_known_world_sid = Win32::Security::SID::Everyone
well_known_nobody_sid = Win32::Security::SID::Nobody
well_known_system_sid = Win32::Security::SID::LocalSystem
mode = S_ISYSTEM_MISSING
sd = get_security_descriptor(path)
sd.dacl.each do |ace|
next if ace.inherit_only?
case ace.sid
when sd.owner
MASK_TO_MODE.each_pair do |k,v|
if (ace.mask & k) == k
mode |= (v << 6)
end
end
when sd.group
MASK_TO_MODE.each_pair do |k,v|
if (ace.mask & k) == k
mode |= (v << 3)
end
end
when well_known_world_sid
MASK_TO_MODE.each_pair do |k,v|
if (ace.mask & k) == k
mode |= (v << 6) | (v << 3) | v
end
end
if File.directory?(path) && (ace.mask & (FILE_WRITE_DATA | FILE_EXECUTE | FILE_DELETE_CHILD)) == (FILE_WRITE_DATA | FILE_EXECUTE)
mode |= S_ISVTX;
end
when well_known_nobody_sid
if (ace.mask & FILE_APPEND_DATA).nonzero?
mode |= S_ISVTX
end
when well_known_system_sid
else
#puts "Warning, unable to map SID into POSIX mode: #{ace.sid}"
mode |= S_IEXTRA
end
if ace.sid == well_known_system_sid
mode &= ~S_ISYSTEM_MISSING
end
# if owner and group the same, then user and group modes are the OR of both
if sd.owner == sd.group
mode |= ((mode & S_IRWXG) << 3) | ((mode & S_IRWXU) >> 3)
#puts "owner: #{sd.group}, 0x#{ace.mask.to_s(16)}, #{mode.to_s(8)}"
end
end
#puts "get_mode: #{mode.to_s(8)}"
mode
end
MODE_TO_MASK = {
S_IROTH => FILE_GENERIC_READ,
S_IWOTH => FILE_GENERIC_WRITE,
S_IXOTH => (FILE_GENERIC_EXECUTE & ~FILE_READ_ATTRIBUTES),
}
# Set the mode of the object referenced by +path+ to the specified
# +mode+. The mode should be specified as POSIX-stye read, write,
# and execute modes for the user, group, and other classes,
# e.g. 0640. The sticky bit, S_ISVTX, is supported, but is only
# meaningful for directories. If set, group and others are not
# allowed to delete child objects for which they are not the owner.
# By default, the DACL is set to protected, meaning it does not
# inherit access control entries from parent objects. This can be
# changed by setting +protected+ to false. The owner of the object
# (with READ_CONTROL and WRITE_DACL access) can always change the
# mode. Only a user with the SE_BACKUP_NAME and SE_RESTORE_NAME
# privileges in their process token can change the mode for objects
# that they do not have read and write access to.
def set_mode(mode, path, protected = true)
sd = get_security_descriptor(path)
well_known_world_sid = Win32::Security::SID::Everyone
well_known_nobody_sid = Win32::Security::SID::Nobody
well_known_system_sid = Win32::Security::SID::LocalSystem
owner_allow = STANDARD_RIGHTS_ALL | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES
group_allow = STANDARD_RIGHTS_READ | FILE_READ_ATTRIBUTES | SYNCHRONIZE
other_allow = STANDARD_RIGHTS_READ | FILE_READ_ATTRIBUTES | SYNCHRONIZE
nobody_allow = 0
system_allow = 0
MODE_TO_MASK.each do |k,v|
if ((mode >> 6) & k) == k
owner_allow |= v
end
if ((mode >> 3) & k) == k
group_allow |= v
end
if (mode & k) == k
other_allow |= v
end
end
if (mode & S_ISVTX).nonzero?
nobody_allow |= FILE_APPEND_DATA;
end
# caller is NOT managing SYSTEM by using group or owner, so set to FULL
if ! [sd.owner, sd.group].include? well_known_system_sid
# we don't check S_ISYSTEM_MISSING bit, but automatically carry over existing SYSTEM perms
# by default set SYSTEM perms to full
system_allow = FILE_ALL_ACCESS
end
isdir = File.directory?(path)
if isdir
if (mode & (S_IWUSR | S_IXUSR)) == (S_IWUSR | S_IXUSR)
owner_allow |= FILE_DELETE_CHILD
end
if (mode & (S_IWGRP | S_IXGRP)) == (S_IWGRP | S_IXGRP) && (mode & S_ISVTX) == 0
group_allow |= FILE_DELETE_CHILD
end
if (mode & (S_IWOTH | S_IXOTH)) == (S_IWOTH | S_IXOTH) && (mode & S_ISVTX) == 0
other_allow |= FILE_DELETE_CHILD
end
end
# if owner and group the same, then map group permissions to the one owner ACE
isownergroup = sd.owner == sd.group
if isownergroup
owner_allow |= group_allow
end
# if any ACE allows write, then clear readonly bit, but do this before we overwrite
# the DACl and lose our ability to set the attribute
if ((owner_allow | group_allow | other_allow ) & FILE_WRITE_DATA) == FILE_WRITE_DATA
remove_attributes(path, FILE_ATTRIBUTE_READONLY)
end
dacl = Puppet::Util::Windows::AccessControlList.new
dacl.allow(sd.owner, owner_allow)
unless isownergroup
dacl.allow(sd.group, group_allow)
end
dacl.allow(well_known_world_sid, other_allow)
dacl.allow(well_known_nobody_sid, nobody_allow)
# TODO: system should be first?
dacl.allow(well_known_system_sid, system_allow)
# add inherit-only aces for child dirs and files that are created within the dir
if isdir
inherit = INHERIT_ONLY_ACE | CONTAINER_INHERIT_ACE
dacl.allow(Win32::Security::SID::CreatorOwner, owner_allow, inherit)
dacl.allow(Win32::Security::SID::CreatorGroup, group_allow, inherit)
inherit = INHERIT_ONLY_ACE | OBJECT_INHERIT_ACE
dacl.allow(Win32::Security::SID::CreatorOwner, owner_allow & ~FILE_EXECUTE, inherit)
dacl.allow(Win32::Security::SID::CreatorGroup, group_allow & ~FILE_EXECUTE, inherit)
end
new_sd = Puppet::Util::Windows::SecurityDescriptor.new(sd.owner, sd.group, dacl, protected)
set_security_descriptor(path, new_sd)
nil
end
def add_access_allowed_ace(acl, mask, sid, inherit = nil)
inherit ||= NO_INHERITANCE
string_to_sid_ptr(sid) do |sid_ptr|
if Puppet::Util::Windows::SID.IsValidSid(sid_ptr) == FFI::WIN32_FALSE
raise Puppet::Util::Windows::Error.new("Invalid SID")
end
unless AddAccessAllowedAceEx(acl, ACL_REVISION, inherit, mask, sid_ptr.address)
raise Puppet::Util::Windows::Error.new("Failed to add access control entry")
end
end
end
def add_access_denied_ace(acl, mask, sid, inherit = nil)
inherit ||= NO_INHERITANCE
string_to_sid_ptr(sid) do |sid_ptr|
if Puppet::Util::Windows::SID.IsValidSid(sid_ptr) == FFI::WIN32_FALSE
raise Puppet::Util::Windows::Error.new("Invalid SID")
end
unless AddAccessDeniedAceEx(acl, ACL_REVISION, inherit, mask, sid_ptr.address)
raise Puppet::Util::Windows::Error.new("Failed to add access control entry")
end
end
end
def parse_dacl(dacl_ptr)
# REMIND: need to handle NULL DACL
raise Puppet::Util::Windows::Error.new("Invalid DACL") unless IsValidAcl(dacl_ptr)
# ACL structure, size and count are the important parts. The
# size includes both the ACL structure and all the ACEs.
#
# BYTE AclRevision
# BYTE Padding1
# WORD AclSize
# WORD AceCount
# WORD Padding2
acl_buf = 0.chr * 8
memcpy(acl_buf, dacl_ptr, acl_buf.size)
ace_count = acl_buf.unpack('CCSSS')[3]
dacl = Puppet::Util::Windows::AccessControlList.new
# deny all
return dacl if ace_count == 0
0.upto(ace_count - 1) do |i|
ace_ptr = [0].pack('L')
next unless GetAce(dacl_ptr, i, ace_ptr)
# ACE structures vary depending on the type. All structures
# begin with an ACE header, which specifies the type, flags
# and size of what follows. We are only concerned with
# ACCESS_ALLOWED_ACE and ACCESS_DENIED_ACEs, which have the
# same structure:
#
# BYTE C AceType
# BYTE C AceFlags
# WORD S AceSize
# DWORD L ACCESS_MASK
# DWORD L Sid
# .. ...
# DWORD L Sid
ace_buf = 0.chr * 8
memcpy(ace_buf, ace_ptr.unpack('L')[0], ace_buf.size)
ace_type, ace_flags, size, mask = ace_buf.unpack('CCSL')
case ace_type
when ACCESS_ALLOWED_ACE_TYPE
sid_ptr = FFI::Pointer.new(:pointer, ace_ptr.unpack('L')[0] + 8) # address of ace_ptr->SidStart
if Puppet::Util::Windows::SID.IsValidSid(sid_ptr) == FFI::WIN32_FALSE
raise Puppet::Util::Windows::Error.new("Failed to read DACL, invalid SID")
end
sid = sid_ptr_to_string(sid_ptr)
dacl.allow(sid, mask, ace_flags)
when ACCESS_DENIED_ACE_TYPE
sid_ptr = FFI::Pointer.new(:pointer, ace_ptr.unpack('L')[0] + 8) # address of ace_ptr->SidStart
if Puppet::Util::Windows::SID.IsValidSid(sid_ptr) == FFI::WIN32_FALSE
raise Puppet::Util::Windows::Error.new("Failed to read DACL, invalid SID")
end
sid = sid_ptr_to_string(sid_ptr)
dacl.deny(sid, mask, ace_flags)
else
Puppet.warning "Unsupported access control entry type: 0x#{ace_type.to_s(16)}"
end
end
dacl
end
# Open an existing file with the specified access mode, and execute a
# block with the opened file HANDLE.
def open_file(path, access)
handle = CreateFile(
path,
access,
FILE_SHARE_READ | FILE_SHARE_WRITE,
0, # security_attributes
OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
0) # template
raise Puppet::Util::Windows::Error.new("Failed to open '#{path}'") if handle == INVALID_HANDLE_VALUE
begin
yield handle
ensure
CloseHandle(handle)
end
end
# Execute a block with the specified privilege enabled
def with_privilege(privilege)
set_privilege(privilege, true)
yield
ensure
set_privilege(privilege, false)
end
# Enable or disable a privilege. Note this doesn't add any privileges the
# user doesn't already has, it just enables privileges that are disabled.
def set_privilege(privilege, enable)
return unless Puppet.features.root?
with_process_token(TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY) do |token|
tmpLuid = 0.chr * 8
# Get the LUID for specified privilege.
unless LookupPrivilegeValue("", privilege, tmpLuid)
raise Puppet::Util::Windows::Error.new("Failed to lookup privilege")
end
# DWORD + [LUID + DWORD]
tkp = [1].pack('L') + tmpLuid + [enable ? SE_PRIVILEGE_ENABLED : 0].pack('L')
unless AdjustTokenPrivileges(token, 0, tkp, tkp.length , nil, nil)
raise Puppet::Util::Windows::Error.new("Failed to adjust process privileges")
end
end
end
# Execute a block with the current process token
def with_process_token(access)
token = 0.chr * 4
unless OpenProcessToken(GetCurrentProcess(), access, token)
raise Puppet::Util::Windows::Error.new("Failed to open process token")
end
begin
token = token.unpack('L')[0]
yield token
ensure
CloseHandle(token)
end
end
def get_security_descriptor(path)
sd = nil
with_privilege(SE_BACKUP_NAME) do
open_file(path, READ_CONTROL) do |handle|
owner_sid = [0].pack('L')
group_sid = [0].pack('L')
dacl = [0].pack('L')
ppsd = [0].pack('L')
rv = GetSecurityInfo(
handle,
SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
owner_sid,
group_sid,
dacl,
nil, #sacl
ppsd) #sec desc
raise Puppet::Util::Windows::Error.new("Failed to get security information") unless rv == ERROR_SUCCESS
begin
owner = sid_ptr_to_string(FFI::Pointer.new(:pointer, owner_sid.unpack('L')[0]))
group = sid_ptr_to_string(FFI::Pointer.new(:pointer, group_sid.unpack('L')[0]))
- control = FFI::MemoryPointer.new(:word, 1)
- revision = FFI::MemoryPointer.new(:dword, 1)
- ffsd = FFI::Pointer.new(:pointer, ppsd.unpack('L')[0])
+ FFI::MemoryPointer.new(:word, 1) do |control|
+ FFI::MemoryPointer.new(:dword, 1) do |revision|
+ ffsd = FFI::Pointer.new(:pointer, ppsd.unpack('L')[0])
- if GetSecurityDescriptorControl(ffsd, control, revision) == FFI::WIN32_FALSE
- raise Puppet::Util::Windows::Error.new("Failed to get security descriptor control")
- end
-
- protect = (control.read_uint16 & SE_DACL_PROTECTED) == SE_DACL_PROTECTED
+ if GetSecurityDescriptorControl(ffsd, control, revision) == FFI::WIN32_FALSE
+ raise Puppet::Util::Windows::Error.new("Failed to get security descriptor control")
+ end
- dacl = parse_dacl(dacl.unpack('L')[0])
- sd = Puppet::Util::Windows::SecurityDescriptor.new(owner, group, dacl, protect)
- ensure
+ protect = (control.read_uint16 & SE_DACL_PROTECTED) == SE_DACL_PROTECTED
+ dacl = parse_dacl(dacl.unpack('L')[0])
+ sd = Puppet::Util::Windows::SecurityDescriptor.new(owner, group, dacl, protect)
+ end
+ end
+ ensure
LocalFree(ppsd.unpack('L')[0])
end
end
end
sd
end
# setting DACL requires both READ_CONTROL and WRITE_DACL access rights,
# and their respective privileges, SE_BACKUP_NAME and SE_RESTORE_NAME.
def set_security_descriptor(path, sd)
# REMIND: FFI
acl = 0.chr * 1024 # This can be increased later as neede
unless InitializeAcl(acl, acl.size, ACL_REVISION)
raise Puppet::Util::Windows::Error.new("Failed to initialize ACL")
end
raise Puppet::Util::Windows::Error.new("Invalid DACL") unless IsValidAcl(acl)
with_privilege(SE_BACKUP_NAME) do
with_privilege(SE_RESTORE_NAME) do
open_file(path, READ_CONTROL | WRITE_DAC | WRITE_OWNER) do |handle|
string_to_sid_ptr(sd.owner) do |ownersid|
string_to_sid_ptr(sd.group) do |groupsid|
sd.dacl.each do |ace|
case ace.type
when ACCESS_ALLOWED_ACE_TYPE
#puts "ace: allow, sid #{sid_to_name(ace.sid)}, mask 0x#{ace.mask.to_s(16)}"
add_access_allowed_ace(acl, ace.mask, ace.sid, ace.flags)
when ACCESS_DENIED_ACE_TYPE
#puts "ace: deny, sid #{sid_to_name(ace.sid)}, mask 0x#{ace.mask.to_s(16)}"
add_access_denied_ace(acl, ace.mask, ace.sid, ace.flags)
else
raise "We should never get here"
# TODO: this should have been a warning in an earlier commit
end
end
# protected means the object does not inherit aces from its parent
flags = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION
flags |= sd.protect ? PROTECTED_DACL_SECURITY_INFORMATION : UNPROTECTED_DACL_SECURITY_INFORMATION
rv = SetSecurityInfo(handle,
SE_FILE_OBJECT,
flags,
ownersid.address,
groupsid.address,
acl,
nil)
raise Puppet::Util::Windows::Error.new("Failed to set security information") unless rv == ERROR_SUCCESS
end
end
end
end
end
end
ffi_convention :stdcall
# http://msdn.microsoft.com/en-us/library/windows/hardware/ff556610(v=vs.85).aspx
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa379561(v=vs.85).aspx
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa446647(v=vs.85).aspx
# typedef WORD SECURITY_DESCRIPTOR_CONTROL, *PSECURITY_DESCRIPTOR_CONTROL;
# BOOL WINAPI GetSecurityDescriptorControl(
# _In_ PSECURITY_DESCRIPTOR pSecurityDescriptor,
# _Out_ PSECURITY_DESCRIPTOR_CONTROL pControl,
# _Out_ LPDWORD lpdwRevision
# );
ffi_lib :advapi32
attach_function_private :GetSecurityDescriptorControl,
[:pointer, :lpword, :lpdword], :win32_bool
end
diff --git a/lib/puppet/util/windows/sid.rb b/lib/puppet/util/windows/sid.rb
index c51d9bebe..c582f78ed 100644
--- a/lib/puppet/util/windows/sid.rb
+++ b/lib/puppet/util/windows/sid.rb
@@ -1,164 +1,155 @@
require 'puppet/util/windows'
module Puppet::Util::Windows
module SID
require 'ffi'
extend FFI::Library
# missing from Windows::Error
ERROR_NONE_MAPPED = 1332
ERROR_INVALID_SID_STRUCTURE = 1337
# Convert an account name, e.g. 'Administrators' into a SID string,
# e.g. 'S-1-5-32-544'. The name can be specified as 'Administrators',
# 'BUILTIN\Administrators', or 'S-1-5-32-544', and will return the
# SID. Returns nil if the account doesn't exist.
def name_to_sid(name)
sid = name_to_sid_object(name)
sid ? sid.to_s : nil
end
# Convert an account name, e.g. 'Administrators' into a SID object,
# e.g. 'S-1-5-32-544'. The name can be specified as 'Administrators',
# 'BUILTIN\Administrators', or 'S-1-5-32-544', and will return the
# SID object. Returns nil if the account doesn't exist.
def name_to_sid_object(name)
# Apparently, we accept a symbol..
name = name.to_s.strip if name
# if it's in SID string form, convert to user
parsed_sid = Win32::Security::SID.string_to_sid(name) rescue nil
parsed_sid ? Win32::Security::SID.new(parsed_sid) : Win32::Security::SID.new(name)
rescue
nil
end
# Converts an octet string array of bytes to a SID object,
# e.g. [1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0] is the representation for
# S-1-5-18, the local 'SYSTEM' account.
# Raises an Error for nil or non-array input.
def octet_string_to_sid_object(bytes)
if !bytes || !bytes.respond_to?('pack') || bytes.empty?
raise Puppet::Util::Windows::Error.new("Octet string must be an array of bytes")
end
Win32::Security::SID.new(bytes.pack('C*'))
end
# Convert a SID string, e.g. "S-1-5-32-544" to a name,
# e.g. 'BUILTIN\Administrators'. Returns nil if an account
# for that SID does not exist.
def sid_to_name(value)
sid = Win32::Security::SID.new(Win32::Security::SID.string_to_sid(value))
if sid.domain and sid.domain.length > 0
"#{sid.domain}\\#{sid.account}"
else
sid.account
end
rescue
nil
end
# http://stackoverflow.com/a/1792930 - 68 bytes, 184 characters in a string
MAXIMUM_SID_STRING_LENGTH = 184
# Convert a SID pointer to a SID string, e.g. "S-1-5-32-544".
def sid_ptr_to_string(psid)
if ! psid.instance_of?(FFI::Pointer) || IsValidSid(psid) == FFI::WIN32_FALSE
raise Puppet::Util::Windows::Error.new("Invalid SID")
end
- buffer_ptr = FFI::MemoryPointer.new(:pointer, 1)
-
- begin
+ sid_string = nil
+ FFI::MemoryPointer.new(:pointer, 1) do |buffer_ptr|
if ConvertSidToStringSidW(psid, buffer_ptr) == FFI::WIN32_FALSE
raise Puppet::Util::Windows::Error.new("Failed to convert binary SID")
end
- wide_string_ptr = buffer_ptr.read_pointer
-
- if wide_string_ptr.null?
- raise Puppet::Error.new("ConvertSidToStringSidW failed to allocate buffer for sid")
- end
-
- return wide_string_ptr.read_arbitrary_wide_string_up_to(MAXIMUM_SID_STRING_LENGTH)
- ensure
- if ! wide_string_ptr.nil? && ! wide_string_ptr.null?
- if LocalFree(wide_string_ptr.address) != FFI::Pointer::NULL_HANDLE
- Puppet.debug "LocalFree memory leak"
+ buffer_ptr.read_win32_local_pointer do |wide_string_ptr|
+ if wide_string_ptr.null?
+ raise Puppet::Error.new("ConvertSidToStringSidW failed to allocate buffer for sid")
end
+
+ sid_string = wide_string_ptr.read_arbitrary_wide_string_up_to(MAXIMUM_SID_STRING_LENGTH)
end
end
+
+ sid_string
end
# Convert a SID string, e.g. "S-1-5-32-544" to a pointer (containing the
# address of the binary SID structure). The returned value can be used in
# Win32 APIs that expect a PSID, e.g. IsValidSid. The account for this
# SID may or may not exist.
def string_to_sid_ptr(string_sid, &block)
- lpcwstr = FFI::MemoryPointer.from_string_to_wide_string(string_sid)
- sid_ptr_ptr = FFI::MemoryPointer.new(:pointer, 1)
+ FFI::MemoryPointer.from_string_to_wide_string(string_sid) do |lpcwstr|
+ FFI::MemoryPointer.new(:pointer, 1) do |sid_ptr_ptr|
- if ConvertStringSidToSidW(lpcwstr, sid_ptr_ptr) == FFI::WIN32_FALSE
- raise Puppet::Util::Windows::Error.new("Failed to convert string SID: #{string_sid}")
- end
+ if ConvertStringSidToSidW(lpcwstr, sid_ptr_ptr) == FFI::WIN32_FALSE
+ raise Puppet::Util::Windows::Error.new("Failed to convert string SID: #{string_sid}")
+ end
- begin
- yield sid_ptr = sid_ptr_ptr.read_pointer
- ensure
- if LocalFree(sid_ptr.address) != FFI::Pointer::NULL_HANDLE
- Puppet.debug "LocalFree memory leak"
+ sid_ptr_ptr.read_win32_local_pointer do |sid_ptr|
+ yield sid_ptr
+ end
end
end
+
+ # yielded sid_ptr has already had LocalFree called, nothing to return
+ nil
end
# Return true if the string is a valid SID, e.g. "S-1-5-32-544", false otherwise.
def valid_sid?(string_sid)
- string_to_sid_ptr(string_sid) { |ptr| true }
- rescue Puppet::Util::Windows::Error => e
- if e.code == ERROR_INVALID_SID_STRUCTURE
- false
- else
- raise
+ valid = false
+
+ begin
+ string_to_sid_ptr(string_sid) { |ptr| valid = ! ptr.nil? && ! ptr.null? }
+ rescue Puppet::Util::Windows::Error => e
+ raise if e.code != ERROR_INVALID_SID_STRUCTURE
end
+
+ valid
end
ffi_convention :stdcall
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa379151(v=vs.85).aspx
# BOOL WINAPI IsValidSid(
# _In_ PSID pSid
# );
ffi_lib :advapi32
attach_function_private :IsValidSid,
[:pointer], :win32_bool
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa376399(v=vs.85).aspx
# BOOL ConvertSidToStringSid(
# _In_ PSID Sid,
# _Out_ LPTSTR *StringSid
# );
ffi_lib :advapi32
attach_function_private :ConvertSidToStringSidW,
[:pointer, :pointer], :win32_bool
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa376402(v=vs.85).aspx
# BOOL WINAPI ConvertStringSidToSid(
# _In_ LPCTSTR StringSid,
# _Out_ PSID *Sid
# );
ffi_lib :advapi32
attach_function_private :ConvertStringSidToSidW,
[:lpcwstr, :pointer], :win32_bool
-
- # http://msdn.microsoft.com/en-us/library/windows/desktop/aa366730(v=vs.85).aspx
- # HLOCAL WINAPI LocalFree(
- # _In_ HLOCAL hMem
- # );
- ffi_lib :kernel32
- attach_function_private :LocalFree, [:handle], :handle
end
end
diff --git a/lib/puppet/util/windows/user.rb b/lib/puppet/util/windows/user.rb
index 611584e67..d1639f47c 100644
--- a/lib/puppet/util/windows/user.rb
+++ b/lib/puppet/util/windows/user.rb
@@ -1,288 +1,292 @@
require 'puppet/util/windows'
require 'facter'
require 'ffi'
module Puppet::Util::Windows::User
extend Puppet::Util::Windows::String
extend FFI::Library
def admin?
majversion = Facter.value(:kernelmajversion)
return false unless majversion
# if Vista or later, check for unrestricted process token
return Puppet::Util::Windows::Process.elevated_security? unless majversion.to_f < 6.0
# otherwise 2003 or less
check_token_membership
end
module_function :admin?
# http://msdn.microsoft.com/en-us/library/windows/desktop/ee207397(v=vs.85).aspx
SECURITY_MAX_SID_SIZE = 68
def check_token_membership
- sid_pointer = FFI::MemoryPointer.new(:byte, SECURITY_MAX_SID_SIZE)
- size_pointer = FFI::MemoryPointer.new(:dword, 1)
- size_pointer.write_uint32(SECURITY_MAX_SID_SIZE)
+ is_admin = false
+ FFI::MemoryPointer.new(:byte, SECURITY_MAX_SID_SIZE) do |sid_pointer|
+ FFI::MemoryPointer.new(:dword, 1) do |size_pointer|
+ size_pointer.write_uint32(SECURITY_MAX_SID_SIZE)
- if CreateWellKnownSid(:WinBuiltinAdministratorsSid, FFI::Pointer::NULL, sid_pointer, size_pointer) == FFI::WIN32_FALSE
- raise Puppet::Util::Windows::Error.new("Failed to create administrators SID")
- end
+ if CreateWellKnownSid(:WinBuiltinAdministratorsSid, FFI::Pointer::NULL, sid_pointer, size_pointer) == FFI::WIN32_FALSE
+ raise Puppet::Util::Windows::Error.new("Failed to create administrators SID")
+ end
+ end
- if IsValidSid(sid_pointer) == FFI::WIN32_FALSE
- raise Puppet::Util::Windows::Error.new("Invalid SID")
- end
+ if IsValidSid(sid_pointer) == FFI::WIN32_FALSE
+ raise Puppet::Util::Windows::Error.new("Invalid SID")
+ end
- ismember_pointer = FFI::MemoryPointer.new(:win32_bool, 1)
- if CheckTokenMembership(FFI::Pointer::NULL_HANDLE, sid_pointer, ismember_pointer) == FFI::WIN32_FALSE
- raise Puppet::Util::Windows::Error.new("Failed to check membership")
+ FFI::MemoryPointer.new(:win32_bool, 1) do |ismember_pointer|
+ if CheckTokenMembership(FFI::Pointer::NULL_HANDLE, sid_pointer, ismember_pointer) == FFI::WIN32_FALSE
+ raise Puppet::Util::Windows::Error.new("Failed to check membership")
+ end
+
+ # Is administrators SID enabled in calling thread's access token?
+ is_admin = ismember_pointer.read_win32_bool != FFI::WIN32_FALSE
+ end
end
- # Is administrators SID enabled in calling thread's access token?
- ismember_pointer.read_win32_bool
+ is_admin
end
module_function :check_token_membership
def password_is?(name, password)
logon_user(name, password)
- true
rescue Puppet::Util::Windows::Error
false
end
module_function :password_is?
def logon_user(name, password, &block)
fLOGON32_LOGON_NETWORK = 3
fLOGON32_PROVIDER_DEFAULT = 0
- token_pointer = FFI::MemoryPointer.new(:handle, 1)
- if LogonUserW(wide_string(name), wide_string('.'), wide_string(password),
- fLOGON32_LOGON_NETWORK, fLOGON32_PROVIDER_DEFAULT, token_pointer) == FFI::WIN32_FALSE
- raise Puppet::Util::Windows::Error.new("Failed to logon user #{name.inspect}")
- end
-
- token = token_pointer.read_handle
+ token = nil
begin
- yield token if block_given?
+ FFI::MemoryPointer.new(:handle, 1) do |token_pointer|
+ if LogonUserW(wide_string(name), wide_string('.'), wide_string(password),
+ fLOGON32_LOGON_NETWORK, fLOGON32_PROVIDER_DEFAULT, token_pointer) == FFI::WIN32_FALSE
+ raise Puppet::Util::Windows::Error.new("Failed to logon user #{name.inspect}")
+ end
+
+ yield token = token_pointer.read_handle
+ end
ensure
- CloseHandle(token)
+ FFI::WIN32.CloseHandle(token) if token
end
+
+ # token has been closed by this point
+ true
end
module_function :logon_user
def load_profile(user, password)
logon_user(user, password) do |token|
- pi = PROFILEINFO.new
- pi[:dwSize] = PROFILEINFO.size
- pi[:dwFlags] = 1 # PI_NOUI - prevents display of profile error msgs
- pi[:lpUserName] = FFI::MemoryPointer.from_string_to_wide_string(user)
+ FFI::MemoryPointer.from_string_to_wide_string(user) do |lpUserName|
+ pi = PROFILEINFO.new
+ pi[:dwSize] = PROFILEINFO.size
+ pi[:dwFlags] = 1 # PI_NOUI - prevents display of profile error msgs
+ pi[:lpUserName] = lpUserName
- # Load the profile. Since it doesn't exist, it will be created
- if LoadUserProfileW(token, pi.pointer) == FFI::WIN32_FALSE
- raise Puppet::Util::Windows::Error.new("Failed to load user profile #{user.inspect}")
- end
+ # Load the profile. Since it doesn't exist, it will be created
+ if LoadUserProfileW(token, pi.pointer) == FFI::WIN32_FALSE
+ raise Puppet::Util::Windows::Error.new("Failed to load user profile #{user.inspect}")
+ end
- Puppet.debug("Loaded profile for #{user}")
+ Puppet.debug("Loaded profile for #{user}")
- if UnloadUserProfile(token, pi[:hProfile]) == FFI::WIN32_FALSE
- raise Puppet::Util::Windows::Error.new("Failed to unload user profile #{user.inspect}")
+ if UnloadUserProfile(token, pi[:hProfile]) == FFI::WIN32_FALSE
+ raise Puppet::Util::Windows::Error.new("Failed to unload user profile #{user.inspect}")
+ end
end
end
end
module_function :load_profile
ffi_convention :stdcall
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa378184(v=vs.85).aspx
# BOOL LogonUser(
# _In_ LPTSTR lpszUsername,
# _In_opt_ LPTSTR lpszDomain,
# _In_opt_ LPTSTR lpszPassword,
# _In_ DWORD dwLogonType,
# _In_ DWORD dwLogonProvider,
# _Out_ PHANDLE phToken
# );
ffi_lib :advapi32
attach_function_private :LogonUserW,
[:lpwstr, :lpwstr, :lpwstr, :dword, :dword, :phandle], :win32_bool
- # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724211(v=vs.85).aspx
- # BOOL WINAPI CloseHandle(
- # _In_ HANDLE hObject
- # );
- ffi_lib :kernel32
- attach_function_private :CloseHandle, [:handle], :win32_bool
-
# http://msdn.microsoft.com/en-us/library/windows/desktop/bb773378(v=vs.85).aspx
# typedef struct _PROFILEINFO {
# DWORD dwSize;
# DWORD dwFlags;
# LPTSTR lpUserName;
# LPTSTR lpProfilePath;
# LPTSTR lpDefaultPath;
# LPTSTR lpServerName;
# LPTSTR lpPolicyPath;
# HANDLE hProfile;
# } PROFILEINFO, *LPPROFILEINFO;
# technically
# NOTE: that for structs, buffer_* (lptstr alias) cannot be used
class PROFILEINFO < FFI::Struct
layout :dwSize, :dword,
:dwFlags, :dword,
:lpUserName, :pointer,
:lpProfilePath, :pointer,
:lpDefaultPath, :pointer,
:lpServerName, :pointer,
:lpPolicyPath, :pointer,
:hProfile, :handle
end
# http://msdn.microsoft.com/en-us/library/windows/desktop/bb762281(v=vs.85).aspx
# BOOL WINAPI LoadUserProfile(
# _In_ HANDLE hToken,
# _Inout_ LPPROFILEINFO lpProfileInfo
# );
ffi_lib :userenv
attach_function_private :LoadUserProfileW,
[:handle, :pointer], :win32_bool
# http://msdn.microsoft.com/en-us/library/windows/desktop/bb762282(v=vs.85).aspx
# BOOL WINAPI UnloadUserProfile(
# _In_ HANDLE hToken,
# _In_ HANDLE hProfile
# );
ffi_lib :userenv
attach_function_private :UnloadUserProfile,
[:handle, :handle], :win32_bool
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa376389(v=vs.85).aspx
# BOOL WINAPI CheckTokenMembership(
# _In_opt_ HANDLE TokenHandle,
# _In_ PSID SidToCheck,
# _Out_ PBOOL IsMember
# );
ffi_lib :advapi32
attach_function_private :CheckTokenMembership,
[:handle, :pointer, :pbool], :win32_bool
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa379650(v=vs.85).aspx
WELL_KNOWN_SID_TYPE = enum(
:WinNullSid , 0,
:WinWorldSid , 1,
:WinLocalSid , 2,
:WinCreatorOwnerSid , 3,
:WinCreatorGroupSid , 4,
:WinCreatorOwnerServerSid , 5,
:WinCreatorGroupServerSid , 6,
:WinNtAuthoritySid , 7,
:WinDialupSid , 8,
:WinNetworkSid , 9,
:WinBatchSid , 10,
:WinInteractiveSid , 11,
:WinServiceSid , 12,
:WinAnonymousSid , 13,
:WinProxySid , 14,
:WinEnterpriseControllersSid , 15,
:WinSelfSid , 16,
:WinAuthenticatedUserSid , 17,
:WinRestrictedCodeSid , 18,
:WinTerminalServerSid , 19,
:WinRemoteLogonIdSid , 20,
:WinLogonIdsSid , 21,
:WinLocalSystemSid , 22,
:WinLocalServiceSid , 23,
:WinNetworkServiceSid , 24,
:WinBuiltinDomainSid , 25,
:WinBuiltinAdministratorsSid , 26,
:WinBuiltinUsersSid , 27,
:WinBuiltinGuestsSid , 28,
:WinBuiltinPowerUsersSid , 29,
:WinBuiltinAccountOperatorsSid , 30,
:WinBuiltinSystemOperatorsSid , 31,
:WinBuiltinPrintOperatorsSid , 32,
:WinBuiltinBackupOperatorsSid , 33,
:WinBuiltinReplicatorSid , 34,
:WinBuiltinPreWindows2000CompatibleAccessSid , 35,
:WinBuiltinRemoteDesktopUsersSid , 36,
:WinBuiltinNetworkConfigurationOperatorsSid , 37,
:WinAccountAdministratorSid , 38,
:WinAccountGuestSid , 39,
:WinAccountKrbtgtSid , 40,
:WinAccountDomainAdminsSid , 41,
:WinAccountDomainUsersSid , 42,
:WinAccountDomainGuestsSid , 43,
:WinAccountComputersSid , 44,
:WinAccountControllersSid , 45,
:WinAccountCertAdminsSid , 46,
:WinAccountSchemaAdminsSid , 47,
:WinAccountEnterpriseAdminsSid , 48,
:WinAccountPolicyAdminsSid , 49,
:WinAccountRasAndIasServersSid , 50,
:WinNTLMAuthenticationSid , 51,
:WinDigestAuthenticationSid , 52,
:WinSChannelAuthenticationSid , 53,
:WinThisOrganizationSid , 54,
:WinOtherOrganizationSid , 55,
:WinBuiltinIncomingForestTrustBuildersSid , 56,
:WinBuiltinPerfMonitoringUsersSid , 57,
:WinBuiltinPerfLoggingUsersSid , 58,
:WinBuiltinAuthorizationAccessSid , 59,
:WinBuiltinTerminalServerLicenseServersSid , 60,
:WinBuiltinDCOMUsersSid , 61,
:WinBuiltinIUsersSid , 62,
:WinIUserSid , 63,
:WinBuiltinCryptoOperatorsSid , 64,
:WinUntrustedLabelSid , 65,
:WinLowLabelSid , 66,
:WinMediumLabelSid , 67,
:WinHighLabelSid , 68,
:WinSystemLabelSid , 69,
:WinWriteRestrictedCodeSid , 70,
:WinCreatorOwnerRightsSid , 71,
:WinCacheablePrincipalsGroupSid , 72,
:WinNonCacheablePrincipalsGroupSid , 73,
:WinEnterpriseReadonlyControllersSid , 74,
:WinAccountReadonlyControllersSid , 75,
:WinBuiltinEventLogReadersGroup , 76,
:WinNewEnterpriseReadonlyControllersSid , 77,
:WinBuiltinCertSvcDComAccessGroup , 78,
:WinMediumPlusLabelSid , 79,
:WinLocalLogonSid , 80,
:WinConsoleLogonSid , 81,
:WinThisOrganizationCertificateSid , 82,
:WinApplicationPackageAuthoritySid , 83,
:WinBuiltinAnyPackageSid , 84,
:WinCapabilityInternetClientSid , 85,
:WinCapabilityInternetClientServerSid , 86,
:WinCapabilityPrivateNetworkClientServerSid , 87,
:WinCapabilityPicturesLibrarySid , 88,
:WinCapabilityVideosLibrarySid , 89,
:WinCapabilityMusicLibrarySid , 90,
:WinCapabilityDocumentsLibrarySid , 91,
:WinCapabilitySharedUserCertificatesSid , 92,
:WinCapabilityEnterpriseAuthenticationSid , 93,
:WinCapabilityRemovableStorageSid , 94
)
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa446585(v=vs.85).aspx
# BOOL WINAPI CreateWellKnownSid(
# _In_ WELL_KNOWN_SID_TYPE WellKnownSidType,
# _In_opt_ PSID DomainSid,
# _Out_opt_ PSID pSid,
# _Inout_ DWORD *cbSid
# );
ffi_lib :advapi32
attach_function_private :CreateWellKnownSid,
[WELL_KNOWN_SID_TYPE, :pointer, :pointer, :lpdword], :win32_bool
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa379151(v=vs.85).aspx
# BOOL WINAPI IsValidSid(
# _In_ PSID pSid
# );
ffi_lib :advapi32
attach_function_private :IsValidSid,
[:pointer], :win32_bool
end
diff --git a/spec/integration/util/windows/process_spec.rb b/spec/integration/util/windows/process_spec.rb
index 0c3d124f0..60eae3443 100644
--- a/spec/integration/util/windows/process_spec.rb
+++ b/spec/integration/util/windows/process_spec.rb
@@ -1,33 +1,34 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'facter'
describe "Puppet::Util::Windows::Process", :if => Puppet.features.microsoft_windows? do
describe "as an admin" do
it "should have the SeCreateSymbolicLinkPrivilege necessary to create symlinks on Vista / 2008+",
:if => Facter.value(:kernelmajversion).to_f >= 6.0 && Puppet.features.microsoft_windows? do
# this is a bit of a lame duck test since it requires running user to be admin
# a better integration test would create a new user with the privilege and verify
Puppet::Util::Windows::User.should be_admin
Puppet::Util::Windows::Process.process_privilege_symlink?.should be_true
end
it "should not have the SeCreateSymbolicLinkPrivilege necessary to create symlinks on 2003 and earlier",
:if => Facter.value(:kernelmajversion).to_f < 6.0 && Puppet.features.microsoft_windows? do
Puppet::Util::Windows::User.should be_admin
Puppet::Util::Windows::Process.process_privilege_symlink?.should be_false
end
it "should be able to lookup a standard Windows process privilege" do
- luid = Puppet::Util::Windows::Process.lookup_privilege_value('SeShutdownPrivilege')
- luid.should_not be_nil
- luid.should be_instance_of(Puppet::Util::Windows::Process::LUID)
+ Puppet::Util::Windows::Process.lookup_privilege_value('SeShutdownPrivilege') do |luid|
+ luid.should_not be_nil
+ luid.should be_instance_of(Puppet::Util::Windows::Process::LUID)
+ end
end
it "should raise an error for an unknown privilege name" do
fail_msg = /LookupPrivilegeValue\(, foo, .*\): A specified privilege does not exist/
expect { Puppet::Util::Windows::Process.lookup_privilege_value('foo') }.to raise_error(Puppet::Util::Windows::Error, fail_msg)
end
end
end
diff --git a/spec/unit/util/execution_spec.rb b/spec/unit/util/execution_spec.rb
index 7c6238f9f..164523a38 100755
--- a/spec/unit/util/execution_spec.rb
+++ b/spec/unit/util/execution_spec.rb
@@ -1,637 +1,637 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Util::Execution do
include Puppet::Util::Execution
# utility method to help deal with some windows vs. unix differences
def process_status(exitstatus)
return exitstatus if Puppet.features.microsoft_windows?
stub('child_status', :exitstatus => exitstatus)
end
# utility methods to help us test some private methods without being quite so verbose
def call_exec_posix(command, arguments, stdin, stdout, stderr)
Puppet::Util::Execution.send(:execute_posix, command, arguments, stdin, stdout, stderr)
end
def call_exec_windows(command, arguments, stdin, stdout, stderr)
Puppet::Util::Execution.send(:execute_windows, command, arguments, stdin, stdout, stderr)
end
describe "execution methods" do
let(:pid) { 5501 }
let(:process_handle) { 0xDEADBEEF }
let(:thread_handle) { 0xCAFEBEEF }
let(:proc_info_stub) { stub 'processinfo', :process_handle => process_handle, :thread_handle => thread_handle, :process_id => pid}
let(:null_file) { Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null' }
def stub_process_wait(exitstatus)
if Puppet.features.microsoft_windows?
Puppet::Util::Windows::Process.stubs(:wait_process).with(process_handle).returns(exitstatus)
- Process.stubs(:CloseHandle).with(process_handle)
- Process.stubs(:CloseHandle).with(thread_handle)
+ FFI::WIN32.stubs(:CloseHandle).with(process_handle)
+ FFI::WIN32.stubs(:CloseHandle).with(thread_handle)
else
Process.stubs(:waitpid2).with(pid).returns([pid, stub('child_status', :exitstatus => exitstatus)])
end
end
describe "#execute_posix (stubs)", :unless => Puppet.features.microsoft_windows? do
before :each do
# Most of the things this method does are bad to do during specs. :/
Kernel.stubs(:fork).returns(pid).yields
Process.stubs(:setsid)
Kernel.stubs(:exec)
Puppet::Util::SUIDManager.stubs(:change_user)
Puppet::Util::SUIDManager.stubs(:change_group)
# ensure that we don't really close anything!
(0..256).each {|n| IO.stubs(:new) }
$stdin.stubs(:reopen)
$stdout.stubs(:reopen)
$stderr.stubs(:reopen)
@stdin = File.open(null_file, 'r')
@stdout = Tempfile.new('stdout')
@stderr = File.open(null_file, 'w')
# there is a danger here that ENV will be modified by exec_posix. Normally it would only affect the ENV
# of a forked process, but here, we're stubbing Kernel.fork, so the method has the ability to override the
# "real" ENV. To guard against this, we'll capture a snapshot of ENV before each test.
@saved_env = ENV.to_hash
# Now, we're going to effectively "mock" the magic ruby 'ENV' variable by creating a local definition of it
# inside of the module we're testing.
Puppet::Util::Execution::ENV = {}
end
after :each do
# And here we remove our "mock" version of 'ENV', which will allow us to validate that the real ENV has been
# left unharmed.
Puppet::Util::Execution.send(:remove_const, :ENV)
# capture the current environment and make sure it's the same as it was before the test
cur_env = ENV.to_hash
# we will get some fairly useless output if we just use the raw == operator on the hashes here, so we'll
# be a bit more explicit and laborious in the name of making the error more useful...
@saved_env.each_pair { |key,val| cur_env[key].should == val }
(cur_env.keys - @saved_env.keys).should == []
end
it "should fork a child process to execute the command" do
Kernel.expects(:fork).returns(pid).yields
Kernel.expects(:exec).with('test command')
call_exec_posix('test command', {}, @stdin, @stdout, @stderr)
end
it "should start a new session group" do
Process.expects(:setsid)
call_exec_posix('test command', {}, @stdin, @stdout, @stderr)
end
it "should permanently change to the correct user and group if specified" do
Puppet::Util::SUIDManager.expects(:change_group).with(55, true)
Puppet::Util::SUIDManager.expects(:change_user).with(50, true)
call_exec_posix('test command', {:uid => 50, :gid => 55}, @stdin, @stdout, @stderr)
end
it "should exit failure if there is a problem execing the command" do
Kernel.expects(:exec).with('test command').raises("failed to execute!")
Puppet::Util::Execution.stubs(:puts)
Puppet::Util::Execution.expects(:exit!).with(1)
call_exec_posix('test command', {}, @stdin, @stdout, @stderr)
end
it "should properly execute commands specified as arrays" do
Kernel.expects(:exec).with('test command', 'with', 'arguments')
call_exec_posix(['test command', 'with', 'arguments'], {:uid => 50, :gid => 55}, @stdin, @stdout, @stderr)
end
it "should properly execute string commands with embedded newlines" do
Kernel.expects(:exec).with("/bin/echo 'foo' ; \n /bin/echo 'bar' ;")
call_exec_posix("/bin/echo 'foo' ; \n /bin/echo 'bar' ;", {:uid => 50, :gid => 55}, @stdin, @stdout, @stderr)
end
it "should return the pid of the child process" do
call_exec_posix('test command', {}, @stdin, @stdout, @stderr).should == pid
end
end
describe "#execute_windows (stubs)", :if => Puppet.features.microsoft_windows? do
before :each do
Process.stubs(:create).returns(proc_info_stub)
stub_process_wait(0)
@stdin = File.open(null_file, 'r')
@stdout = Tempfile.new('stdout')
@stderr = File.open(null_file, 'w')
end
it "should create a new process for the command" do
Process.expects(:create).with(
:command_line => "test command",
:startup_info => {:stdin => @stdin, :stdout => @stdout, :stderr => @stderr},
:close_handles => false
).returns(proc_info_stub)
call_exec_windows('test command', {}, @stdin, @stdout, @stderr)
end
it "should return the process info of the child process" do
call_exec_windows('test command', {}, @stdin, @stdout, @stderr).should == proc_info_stub
end
it "should quote arguments containing spaces if command is specified as an array" do
Process.expects(:create).with do |args|
args[:command_line] == '"test command" with some "arguments \"with spaces"'
end.returns(proc_info_stub)
call_exec_windows(['test command', 'with', 'some', 'arguments "with spaces'], {}, @stdin, @stdout, @stderr)
end
end
describe "#execute (stubs)" do
before :each do
stub_process_wait(0)
end
describe "when an execution stub is specified" do
before :each do
Puppet::Util::ExecutionStub.set do |command,args,stdin,stdout,stderr|
"execution stub output"
end
end
it "should call the block on the stub" do
Puppet::Util::Execution.execute("/usr/bin/run_my_execute_stub").should == "execution stub output"
end
it "should not actually execute anything" do
Puppet::Util::Execution.expects(:execute_posix).never
Puppet::Util::Execution.expects(:execute_windows).never
Puppet::Util::Execution.execute("/usr/bin/run_my_execute_stub")
end
end
describe "when setting up input and output files" do
include PuppetSpec::Files
let(:executor) { Puppet.features.microsoft_windows? ? 'execute_windows' : 'execute_posix' }
let(:rval) { Puppet.features.microsoft_windows? ? proc_info_stub : pid }
before :each do
Puppet::Util::Execution.stubs(:wait_for_output)
end
it "should set stdin to the stdinfile if specified" do
input = tmpfile('stdin')
FileUtils.touch(input)
Puppet::Util::Execution.expects(executor).with do |_,_,stdin,_,_|
stdin.path == input
end.returns(rval)
Puppet::Util::Execution.execute('test command', :stdinfile => input)
end
it "should set stdin to the null file if not specified" do
Puppet::Util::Execution.expects(executor).with do |_,_,stdin,_,_|
stdin.path == null_file
end.returns(rval)
Puppet::Util::Execution.execute('test command')
end
describe "when squelch is set" do
it "should set stdout and stderr to the null file" do
Puppet::Util::Execution.expects(executor).with do |_,_,_,stdout,stderr|
stdout.path == null_file and stderr.path == null_file
end.returns(rval)
Puppet::Util::Execution.execute('test command', :squelch => true)
end
end
describe "when squelch is not set" do
it "should set stdout to a temporary output file" do
outfile = Tempfile.new('stdout')
Tempfile.stubs(:new).returns(outfile)
Puppet::Util::Execution.expects(executor).with do |_,_,_,stdout,_|
stdout.path == outfile.path
end.returns(rval)
Puppet::Util::Execution.execute('test command', :squelch => false)
end
it "should set stderr to the same file as stdout if combine is true" do
outfile = Tempfile.new('stdout')
Tempfile.stubs(:new).returns(outfile)
Puppet::Util::Execution.expects(executor).with do |_,_,_,stdout,stderr|
stdout.path == outfile.path and stderr.path == outfile.path
end.returns(rval)
Puppet::Util::Execution.execute('test command', :squelch => false, :combine => true)
end
it "should set stderr to the null device if combine is false" do
outfile = Tempfile.new('stdout')
Tempfile.stubs(:new).returns(outfile)
Puppet::Util::Execution.expects(executor).with do |_,_,_,stdout,stderr|
stdout.path == outfile.path and stderr.path == null_file
end.returns(rval)
Puppet::Util::Execution.execute('test command', :squelch => false, :combine => false)
end
it "should combine stdout and stderr if combine is true" do
outfile = Tempfile.new('stdout')
Tempfile.stubs(:new).returns(outfile)
Puppet::Util::Execution.expects(executor).with do |_,_,_,stdout,stderr|
stdout.path == outfile.path and stderr.path == outfile.path
end.returns(rval)
Puppet::Util::Execution.execute('test command', :combine => true)
end
it "should default combine to true when no options are specified" do
outfile = Tempfile.new('stdout')
Tempfile.stubs(:new).returns(outfile)
Puppet::Util::Execution.expects(executor).with do |_,_,_,stdout,stderr|
stdout.path == outfile.path and stderr.path == outfile.path
end.returns(rval)
Puppet::Util::Execution.execute('test command')
end
it "should default combine to false when options are specified, but combine is not" do
outfile = Tempfile.new('stdout')
Tempfile.stubs(:new).returns(outfile)
Puppet::Util::Execution.expects(executor).with do |_,_,_,stdout,stderr|
stdout.path == outfile.path and stderr.path == null_file
end.returns(rval)
Puppet::Util::Execution.execute('test command', :failonfail => false)
end
it "should default combine to false when an empty hash of options is specified" do
outfile = Tempfile.new('stdout')
Tempfile.stubs(:new).returns(outfile)
Puppet::Util::Execution.expects(executor).with do |_,_,_,stdout,stderr|
stdout.path == outfile.path and stderr.path == null_file
end.returns(rval)
Puppet::Util::Execution.execute('test command', {})
end
end
end
describe "on Windows", :if => Puppet.features.microsoft_windows? do
it "should always close the process and thread handles" do
Puppet::Util::Execution.stubs(:execute_windows).returns(proc_info_stub)
Puppet::Util::Windows::Process.expects(:wait_process).with(process_handle).raises('whatever')
- Puppet::Util::Windows::Process.expects(:CloseHandle).with(thread_handle)
- Puppet::Util::Windows::Process.expects(:CloseHandle).with(process_handle)
+ FFI::WIN32.expects(:CloseHandle).with(thread_handle)
+ FFI::WIN32.expects(:CloseHandle).with(process_handle)
expect { Puppet::Util::Execution.execute('test command') }.to raise_error(RuntimeError)
end
it "should return the correct exit status even when exit status is greater than 256" do
real_exit_status = 3010
Puppet::Util::Execution.stubs(:execute_windows).returns(proc_info_stub)
stub_process_wait(real_exit_status)
$CHILD_STATUS.stubs(:exitstatus).returns(real_exit_status % 256) # The exitstatus is changed to be mod 256 so that ruby can fit it into 8 bits.
Puppet::Util::Execution.execute('test command', :failonfail => false).exitstatus.should == real_exit_status
end
end
end
describe "#execute (posix locale)", :unless => Puppet.features.microsoft_windows? do
before :each do
# there is a danger here that ENV will be modified by exec_posix. Normally it would only affect the ENV
# of a forked process, but, in some of the previous tests in this file we're stubbing Kernel.fork., which could
# allow the method to override the "real" ENV. This shouldn't be a problem for these tests because they are
# not stubbing Kernel.fork, but, better safe than sorry... so, to guard against this, we'll capture a snapshot
# of ENV before each test.
@saved_env = ENV.to_hash
end
after :each do
# capture the current environment and make sure it's the same as it was before the test
cur_env = ENV.to_hash
# we will get some fairly useless output if we just use the raw == operator on the hashes here, so we'll
# be a bit more explicit and laborious in the name of making the error more useful...
@saved_env.each_pair { |key,val| cur_env[key].should == val }
(cur_env.keys - @saved_env.keys).should == []
end
# build up a printf-style string that contains a command to get the value of an environment variable
# from the operating system. We can substitute into this with the names of the desired environment variables later.
get_env_var_cmd = 'echo $%s'
# a sentinel value that we can use to emulate what locale environment variables might be set to on an international
# system.
lang_sentinel_value = "en_US.UTF-8"
# a temporary hash that contains sentinel values for each of the locale environment variables that we override in
# "execute"
locale_sentinel_env = {}
Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |var| locale_sentinel_env[var] = lang_sentinel_value }
it "should override the locale environment variables when :override_locale is not set (defaults to true)" do
# temporarily override the locale environment vars with a sentinel value, so that we can confirm that
# execute is actually setting them.
Puppet::Util.withenv(locale_sentinel_env) do
Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var|
# we expect that all of the POSIX vars will have been cleared except for LANG and LC_ALL
expected_value = (['LANG', 'LC_ALL'].include?(var)) ? "C" : ""
Puppet::Util::execute(get_env_var_cmd % var).strip.should == expected_value
end
end
end
it "should override the LANG environment variable when :override_locale is set to true" do
# temporarily override the locale environment vars with a sentinel value, so that we can confirm that
# execute is actually setting them.
Puppet::Util.withenv(locale_sentinel_env) do
Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var|
# we expect that all of the POSIX vars will have been cleared except for LANG and LC_ALL
expected_value = (['LANG', 'LC_ALL'].include?(var)) ? "C" : ""
Puppet::Util::execute(get_env_var_cmd % var, {:override_locale => true}).strip.should == expected_value
end
end
end
it "should *not* override the LANG environment variable when :override_locale is set to false" do
# temporarily override the locale environment vars with a sentinel value, so that we can confirm that
# execute is not setting them.
Puppet::Util.withenv(locale_sentinel_env) do
Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var|
Puppet::Util::execute(get_env_var_cmd % var, {:override_locale => false}).strip.should == lang_sentinel_value
end
end
end
it "should have restored the LANG and locale environment variables after execution" do
# we'll do this once without any sentinel values, to give us a little more test coverage
orig_env_vals = {}
Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var|
orig_env_vals[var] = ENV[var]
end
# now we can really execute any command--doesn't matter what it is...
Puppet::Util::execute(get_env_var_cmd % 'anything', {:override_locale => true})
# now we check and make sure the original environment was restored
Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var|
ENV[var].should == orig_env_vals[var]
end
# now, once more... but with our sentinel values
Puppet::Util.withenv(locale_sentinel_env) do
# now we can really execute any command--doesn't matter what it is...
Puppet::Util::execute(get_env_var_cmd % 'anything', {:override_locale => true})
# now we check and make sure the original environment was restored
Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var|
ENV[var].should == locale_sentinel_env[var]
end
end
end
end
describe "#execute (posix user env vars)", :unless => Puppet.features.microsoft_windows? do
# build up a printf-style string that contains a command to get the value of an environment variable
# from the operating system. We can substitute into this with the names of the desired environment variables later.
get_env_var_cmd = 'echo $%s'
# a sentinel value that we can use to emulate what locale environment variables might be set to on an international
# system.
user_sentinel_value = "Abracadabra"
# a temporary hash that contains sentinel values for each of the locale environment variables that we override in
# "execute"
user_sentinel_env = {}
Puppet::Util::POSIX::USER_ENV_VARS.each { |var| user_sentinel_env[var] = user_sentinel_value }
it "should unset user-related environment vars during execution" do
# first we set up a temporary execution environment with sentinel values for the user-related environment vars
# that we care about.
Puppet::Util.withenv(user_sentinel_env) do
# with this environment, we loop over the vars in question
Puppet::Util::POSIX::USER_ENV_VARS.each do |var|
# ensure that our temporary environment is set up as we expect
ENV[var].should == user_sentinel_env[var]
# run an "exec" via the provider and ensure that it unsets the vars
Puppet::Util::execute(get_env_var_cmd % var).strip.should == ""
# ensure that after the exec, our temporary env is still intact
ENV[var].should == user_sentinel_env[var]
end
end
end
it "should have restored the user-related environment variables after execution" do
# we'll do this once without any sentinel values, to give us a little more test coverage
orig_env_vals = {}
Puppet::Util::POSIX::USER_ENV_VARS.each do |var|
orig_env_vals[var] = ENV[var]
end
# now we can really execute any command--doesn't matter what it is...
Puppet::Util::execute(get_env_var_cmd % 'anything')
# now we check and make sure the original environment was restored
Puppet::Util::POSIX::USER_ENV_VARS.each do |var|
ENV[var].should == orig_env_vals[var]
end
# now, once more... but with our sentinel values
Puppet::Util.withenv(user_sentinel_env) do
# now we can really execute any command--doesn't matter what it is...
Puppet::Util::execute(get_env_var_cmd % 'anything')
# now we check and make sure the original environment was restored
Puppet::Util::POSIX::USER_ENV_VARS.each do |var|
ENV[var].should == user_sentinel_env[var]
end
end
end
end
describe "after execution" do
before :each do
stub_process_wait(0)
if Puppet.features.microsoft_windows?
Puppet::Util::Execution.stubs(:execute_windows).returns(proc_info_stub)
else
Puppet::Util::Execution.stubs(:execute_posix).returns(pid)
end
end
it "should wait for the child process to exit" do
Puppet::Util::Execution.stubs(:wait_for_output)
Puppet::Util::Execution.execute('test command')
end
it "should close the stdin/stdout/stderr files used by the child" do
stdin = mock 'file', :close
stdout = mock 'file', :close
stderr = mock 'file', :close
File.expects(:open).
times(3).
returns(stdin).
then.returns(stdout).
then.returns(stderr)
Puppet::Util::Execution.execute('test command', {:squelch => true, :combine => false})
end
it "should read and return the output if squelch is false" do
stdout = Tempfile.new('test')
Tempfile.stubs(:new).returns(stdout)
stdout.write("My expected command output")
Puppet::Util::Execution.execute('test command').should == "My expected command output"
end
it "should not read the output if squelch is true" do
stdout = Tempfile.new('test')
Tempfile.stubs(:new).returns(stdout)
stdout.write("My expected command output")
Puppet::Util::Execution.execute('test command', :squelch => true).should == ''
end
it "should delete the file used for output if squelch is false" do
stdout = Tempfile.new('test')
path = stdout.path
Tempfile.stubs(:new).returns(stdout)
Puppet::Util::Execution.execute('test command')
Puppet::FileSystem.exist?(path).should be_false
end
it "should not raise an error if the file is open" do
stdout = Tempfile.new('test')
Tempfile.stubs(:new).returns(stdout)
file = File.new(stdout.path, 'r')
Puppet::Util.execute('test command')
end
it "should raise an error if failonfail is true and the child failed" do
stub_process_wait(1)
expect {
subject.execute('fail command', :failonfail => true)
}.to raise_error(Puppet::ExecutionFailure, /Execution of 'fail command' returned 1/)
end
it "should not raise an error if failonfail is false and the child failed" do
stub_process_wait(1)
subject.execute('fail command', :failonfail => false)
end
it "should not raise an error if failonfail is true and the child succeeded" do
stub_process_wait(0)
subject.execute('fail command', :failonfail => true)
end
it "should not raise an error if failonfail is false and the child succeeded" do
stub_process_wait(0)
subject.execute('fail command', :failonfail => false)
end
it "should default failonfail to true when no options are specified" do
stub_process_wait(1)
expect {
subject.execute('fail command')
}.to raise_error(Puppet::ExecutionFailure, /Execution of 'fail command' returned 1/)
end
it "should default failonfail to false when options are specified, but failonfail is not" do
stub_process_wait(1)
subject.execute('fail command', { :combine => true })
end
it "should default failonfail to false when an empty hash of options is specified" do
stub_process_wait(1)
subject.execute('fail command', {})
end
it "should raise an error if a nil option is specified" do
expect {
Puppet::Util::Execution.execute('fail command', nil)
}.to raise_error(TypeError, /(can\'t convert|no implicit conversion of) nil into Hash/)
end
end
end
describe "#execpipe" do
it "should execute a string as a string" do
Puppet::Util::Execution.expects(:open).with('| echo hello 2>&1').returns('hello')
$CHILD_STATUS.expects(:==).with(0).returns(true)
Puppet::Util::Execution.execpipe('echo hello').should == 'hello'
end
it "should print meaningful debug message for string argument" do
Puppet::Util::Execution.expects(:debug).with("Executing 'echo hello'")
Puppet::Util::Execution.expects(:open).with('| echo hello 2>&1').returns('hello')
$CHILD_STATUS.expects(:==).with(0).returns(true)
Puppet::Util::Execution.execpipe('echo hello')
end
it "should print meaningful debug message for array argument" do
Puppet::Util::Execution.expects(:debug).with("Executing 'echo hello'")
Puppet::Util::Execution.expects(:open).with('| echo hello 2>&1').returns('hello')
$CHILD_STATUS.expects(:==).with(0).returns(true)
Puppet::Util::Execution.execpipe(['echo','hello'])
end
it "should execute an array by pasting together with spaces" do
Puppet::Util::Execution.expects(:open).with('| echo hello 2>&1').returns('hello')
$CHILD_STATUS.expects(:==).with(0).returns(true)
Puppet::Util::Execution.execpipe(['echo', 'hello']).should == 'hello'
end
it "should fail if asked to fail, and the child does" do
Puppet::Util::Execution.stubs(:open).returns('error message')
$CHILD_STATUS.expects(:==).with(0).returns(false)
expect { Puppet::Util::Execution.execpipe('echo hello') }.
to raise_error Puppet::ExecutionFailure, /error message/
end
it "should not fail if asked not to fail, and the child does" do
Puppet::Util::Execution.stubs(:open).returns('error message')
$CHILD_STATUS.stubs(:==).with(0).returns(false)
Puppet::Util::Execution.execpipe('echo hello', false).should == 'error message'
end
end
end
diff --git a/spec/unit/util/windows/api_types_spec.rb b/spec/unit/util/windows/api_types_spec.rb
index 75d591edd..a1e1c76c9 100644
--- a/spec/unit/util/windows/api_types_spec.rb
+++ b/spec/unit/util/windows/api_types_spec.rb
@@ -1,24 +1,28 @@
# encoding: UTF-8
#!/usr/bin/env ruby
require 'spec_helper'
describe "FFI::MemoryPointer", :if => Puppet.features.microsoft_windows? do
context "read_wide_string" do
let (:string) { "foo_bar" }
it "should properly roundtrip a given string" do
- ptr = FFI::MemoryPointer.from_string_to_wide_string(string)
- read_string = ptr.read_wide_string(string.length)
+ read_string = nil
+ FFI::MemoryPointer.from_string_to_wide_string(string) do |ptr|
+ read_string = ptr.read_wide_string(string.length)
+ end
read_string.should == string
end
it "should return a given string in the default encoding" do
- ptr = FFI::MemoryPointer.from_string_to_wide_string(string)
- read_string = ptr.read_wide_string(string.length)
+ read_string = nil
+ FFI::MemoryPointer.from_string_to_wide_string(string) do |ptr|
+ read_string = ptr.read_wide_string(string.length)
+ end
read_string.encoding.should == Encoding.default_external
end
end
end