diff --git a/lib/puppet/util/colors.rb b/lib/puppet/util/colors.rb index d5ded05d2..016d070b4 100644 --- a/lib/puppet/util/colors.rb +++ b/lib/puppet/util/colors.rb @@ -1,213 +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) chars_written = 0 FFI::MemoryPointer.from_string_to_wide_string(str) do |msg_ptr| - WriteConsoleOutputCharacterW(@handle, msg_ptr, - str.length, writeCoord, numberOfCharsWritten_ptr) - chars_written = numberOfCharsWritten_ptr.read_dword + 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) result = false FFI::MemoryPointer.from_string_to_wide_string(str) do |msg_ptr| - result = WriteConsoleW(@handle, msg_ptr, - str.length, FFI::MemoryPointer.new(:dword, 1), - FFI::MemoryPointer::NULL) != FFI::WIN32_FALSE + 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/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/error.rb b/lib/puppet/util/windows/error.rb index 2a5634951..913258624 100644 --- a/lib/puppet/util/windows/error.rb +++ b/lib/puppet/util/windows/error.rb @@ -1,86 +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 error_string = '' # this pointer actually points to a :lpwstr (pointer) since we're letting Windows allocate for us - buffer_ptr = FFI::MemoryPointer.new(:pointer, 1) - length = FormatMessageW(flags, FFI::Pointer::NULL, code, dwLanguageId, - buffer_ptr, 0, FFI::Pointer::NULL) + 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 - 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}") + 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 - error_string = wide_string_ptr.read_wide_string(length) + # returns an FFI::Pointer with autorelease set to false, which is what we want + 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 end diff --git a/lib/puppet/util/windows/file.rb b/lib/puppet/util/windows/file.rb index b3a14addf..130bb555f 100644 --- a/lib/puppet/util/windows/file.rb +++ b/lib/puppet/util/windows/file.rb @@ -1,305 +1,310 @@ 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( 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 end end def readlink(link_name) open_symlink(link_name) do |handle| resolve_symlink(handle) end 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) path = nil get_reparse_point_data(handle) do |reparse_data| offset = reparse_data[:PrintNameOffset] length = reparse_data[:PrintNameLength] path = reparse_data[:PathBuffer].to_a[offset, length].pack('C*') path = path.force_encoding('UTF-16LE').encode(Encoding.default_external) end 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 a4aec43c7..1681d077e 100644 --- a/lib/puppet/util/windows/process.rb +++ b/lib/puppet/util/windows/process.rb @@ -1,317 +1,340 @@ 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 = -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 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) + CloseHandle(token_handle) if token_handle end 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})") + 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 + ) + + if result == FFI::WIN32_FALSE + raise Puppet::Util::Windows::Error.new( + "LookupPrivilegeValue(#{system_name}, #{name}, #{luid_ptr})") + end + + yield LUID.new(luid_ptr) + end + + # the underlying MemoryPointer for LUID is cleaned up by this point + nil end module_function :lookup_privilege_value - def get_token_information(token_handle, token_information) + def get_token_information(token_handle, token_information, &block) # 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 + 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 + 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) - # 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) + 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 - if result == FFI::WIN32_FALSE - raise Puppet::Util::Windows::Error.new( - "GetTokenInformation(#{token_handle}, #{token_information}, #{token_information_buf}, " + - "#{return_length}, #{return_length_ptr})") + 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) - privilege_symlink = 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? # default / pre-Vista elevated = false - 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 - elevated = token_elevation[:TokenIsElevated] != 0 - end + handle = nil - elevated - rescue Puppet::Util::Windows::Error => e - raise e if e.code != ERROR_NO_SUCH_PRIVILEGE - ensure - CloseHandle(handle) + 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 + CloseHandle(handle) if handle + end 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 28fa48f6d..c582f78ed 100644 --- a/lib/puppet/util/windows/sid.rb +++ b/lib/puppet/util/windows/sid.rb @@ -1,153 +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) - if ConvertSidToStringSidW(psid, buffer_ptr) == FFI::WIN32_FALSE - raise Puppet::Util::Windows::Error.new("Failed to convert binary SID") - end - sid_string = nil - 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") + 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 - sid_string = wide_string_ptr.read_arbitrary_wide_string_up_to(MAXIMUM_SID_STRING_LENGTH) + 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) FFI::MemoryPointer.from_string_to_wide_string(string_sid) do |lpcwstr| - sid_ptr_ptr = FFI::MemoryPointer.new(:pointer, 1) + 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 - sid_ptr_ptr.read_win32_local_pointer do |sid_ptr| - yield sid_ptr + 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) 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 end end diff --git a/lib/puppet/util/windows/user.rb b/lib/puppet/util/windows/user.rb index 0ffbfdc25..7ce424787 100644 --- a/lib/puppet/util/windows/user.rb +++ b/lib/puppet/util/windows/user.rb @@ -1,291 +1,299 @@ 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 + + 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 - 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") + # 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) 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) + 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| 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 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}") 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