diff --git a/lib/puppet/feature/base.rb b/lib/puppet/feature/base.rb index e30aed313..6538ef379 100644 --- a/lib/puppet/feature/base.rb +++ b/lib/puppet/feature/base.rb @@ -1,88 +1,98 @@ require 'puppet/util/feature' # Add the simple features, all in one file. # Order is important as some features depend on others # We have a syslog implementation Puppet.features.add(:syslog, :libs => ["syslog"]) # We can use POSIX user functions Puppet.features.add(:posix) do require 'etc' !Etc.getpwuid(0).nil? && Puppet.features.syslog? end # We can use Microsoft Windows functions Puppet.features.add(:microsoft_windows) do begin # ruby require 'Win32API' # case matters in this require! require 'win32ole' require 'win32/registry' # gems require 'sys/admin' require 'win32/process' require 'win32/dir' require 'win32/service' require 'win32/api' require 'win32/taskscheduler' true rescue LoadError => err warn "Cannot run on Microsoft Windows without the sys-admin, win32-process, win32-dir, win32-service and win32-taskscheduler gems: #{err}" unless Puppet.features.posix? end end raise Puppet::Error,"Cannot determine basic system flavour" unless Puppet.features.posix? or Puppet.features.microsoft_windows? # We've got LDAP available. Puppet.features.add(:ldap, :libs => ["ldap"]) # We have the Rdoc::Usage library. Puppet.features.add(:usage, :libs => %w{rdoc/ri/ri_paths rdoc/usage}) # We have libshadow, useful for managing passwords. Puppet.features.add(:libshadow, :libs => ["shadow"]) # We're running as root. Puppet.features.add(:root) { require 'puppet/util/suidmanager'; Puppet::Util::SUIDManager.root? } # We have lcs diff Puppet.features.add :diff, :libs => %w{diff/lcs diff/lcs/hunk} # We have augeas Puppet.features.add(:augeas, :libs => ["augeas"]) # We have RRD available Puppet.features.add(:rrd_legacy, :libs => ["RRDtool"]) Puppet.features.add(:rrd, :libs => ["RRD"]) # We have OpenSSL Puppet.features.add(:openssl, :libs => ["openssl"]) # We have CouchDB Puppet.features.add(:couchdb, :libs => ["couchrest"]) # We have sqlite Puppet.features.add(:sqlite, :libs => ["sqlite3"]) # We have Hiera Puppet.features.add(:hiera, :libs => ["hiera"]) Puppet.features.add(:minitar, :libs => ["archive/tar/minitar"]) # We can manage symlinks Puppet.features.add(:manages_symlinks) do if ! Puppet::Util::Platform.windows? true else - begin - require 'Win32API' - Win32API.new('kernel32', 'CreateSymbolicLink', 'SSL', 'B') - true - rescue LoadError => err - Puppet.debug("CreateSymbolicLink is not available") - false + module WindowsSymlink + require 'ffi' + extend FFI::Library + + def self.is_implemented + begin + ffi_lib :kernel32 + attach_function :CreateSymbolicLinkW, [:lpwstr, :lpwstr, :dword], :bool + + true + rescue LoadError => err + Puppet.debug("CreateSymbolicLink is not available") + false + end + end end + + WindowsSymlink.is_implemented end end diff --git a/lib/puppet/util/windows.rb b/lib/puppet/util/windows.rb index af8a36995..5e0851cd8 100644 --- a/lib/puppet/util/windows.rb +++ b/lib/puppet/util/windows.rb @@ -1,17 +1,18 @@ module Puppet::Util::Windows if Puppet::Util::Platform.windows? # these reference platform specific gems + require 'puppet/util/windows/api_types' + require 'puppet/util/windows/string' require 'puppet/util/windows/error' require 'puppet/util/windows/sid' require 'puppet/util/windows/security' require 'puppet/util/windows/user' require 'puppet/util/windows/process' - require 'puppet/util/windows/string' require 'puppet/util/windows/file' require 'puppet/util/windows/root_certs' require 'puppet/util/windows/access_control_entry' require 'puppet/util/windows/access_control_list' require 'puppet/util/windows/security_descriptor' end require 'puppet/util/windows/registry' end diff --git a/lib/puppet/util/windows/api_types.rb b/lib/puppet/util/windows/api_types.rb new file mode 100644 index 000000000..efb9a4caf --- /dev/null +++ b/lib/puppet/util/windows/api_types.rb @@ -0,0 +1,57 @@ +require 'ffi' + +module Puppet::Util::Windows::APITypes + module ::FFI::Library + # Wrapper method for attach_function + private + def attach_function_private(*args) + attach_function(*args) + private args[0] + end + 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, :lpvoid + FFI.typedef :pointer, :lpword + FFI.typedef :pointer, :lpdword + FFI.typedef :pointer, :pdword + FFI.typedef :pointer, :phandle + FFI.typedef :pointer, :ulong_ptr + + # 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 + + # NOTE: FFI already defines ushort as a 16-bit unsigned like this: + # FFI.typedef :uint16, :ushort + + # 8 bits per byte + FFI.typedef :uchar, :byte +end diff --git a/lib/puppet/util/windows/file.rb b/lib/puppet/util/windows/file.rb index 15a6ef469..1403a50f3 100644 --- a/lib/puppet/util/windows/file.rb +++ b/lib/puppet/util/windows/file.rb @@ -1,279 +1,284 @@ require 'puppet/util/windows' module Puppet::Util::Windows::File require 'ffi' require 'windows/api' + extend FFI::Library def replace_file(target, source) target_encoded = Puppet::Util::Windows::String.wide_string(target.to_s) source_encoded = Puppet::Util::Windows::String.wide_string(source.to_s) flags = 0x1 backup_file = nil - result = API.replace_file( + result = ReplaceFileW( target_encoded, source_encoded, backup_file, flags, - 0, - 0 + FFI::Pointer::NULL, + FFI::Pointer::NULL ) return true if result raise Puppet::Util::Windows::Error.new("ReplaceFile(#{target}, #{source})") end module_function :replace_file MoveFileEx = Windows::API.new('MoveFileExW', 'PPL', 'B') def move_file_ex(source, target, flags = 0) result = MoveFileEx.call(Puppet::Util::Windows::String.wide_string(source.to_s), Puppet::Util::Windows::String.wide_string(target.to_s), flags) return true unless result == 0 raise Puppet::Util::Windows::Error. new("MoveFileEx(#{source}, #{target}, #{flags.to_s(8)})") end module_function :move_file_ex - module API - extend FFI::Library + 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], :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' - 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 - # ); - attach_function :replace_file, :ReplaceFileW, - [:buffer_in, :buffer_in, :buffer_in, :uint, :uint, :uint], :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 - attach_function :create_symbolic_link, :CreateSymbolicLinkW, - [:buffer_in, :buffer_in, :uint], :bool - rescue LoadError - end - - # DWORD WINAPI GetFileAttributes( - # _In_ LPCTSTR lpFileName - # ); - attach_function :get_file_attributes, :GetFileAttributesW, - [:buffer_in], :uint - - # 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 - # ); - attach_function :create_file, :CreateFileW, - [:buffer_in, :uint, :uint, :pointer, :uint, :uint, :uint], :uint - - # 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 - # ); - attach_function :device_io_control, :DeviceIoControl, - [:uint, :uint, :pointer, :uint, :pointer, :uint, :pointer, :pointer], :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 ReparseDataBuffer < FFI::Struct - layout :reparse_tag, :uint, - :reparse_data_length, :ushort, - :reserved, :ushort, - :substitute_name_offset, :ushort, - :substitute_name_length, :ushort, - :print_name_offset, :ushort, - :print_name_length, :ushort, - :flags, :uint, - # max less above fields dword / uint 4 bytes, ushort 2 bytes - :path_buffer, [:uchar, MAXIMUM_REPARSE_DATA_BUFFER_SIZE - 20] - end + attach_function_private :CreateSymbolicLinkW, + [:lpwstr, :lpwstr, :dword], :bool + rescue LoadError + end - # BOOL WINAPI CloseHandle( - # _In_ HANDLE hObject - # ); - attach_function :close_handle, :CloseHandle, [:uint], :bool + # 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], :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], :bool + def symlink(target, symlink) flags = File.directory?(target) ? 0x1 : 0x0 - result = API.create_symbolic_link(Puppet::Util::Windows::String.wide_string(symlink.to_s), + result = CreateSymbolicLinkW(Puppet::Util::Windows::String.wide_string(symlink.to_s), Puppet::Util::Windows::String.wide_string(target.to_s), flags) return true if result 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 = API.get_file_attributes(Puppet::Util::Windows::String.wide_string(file_name.to_s)) + result = GetFileAttributesW(Puppet::Util::Windows::String.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 = API.create_file(Puppet::Util::Windows::String.wide_string(file_name.to_s), + result = CreateFileW(Puppet::Util::Windows::String.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.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 = API.device_io_control( + result = DeviceIoControl( handle, io_control_code, in_buffer, in_buffer.nil? ? 0 : in_buffer.size, out_buffer, out_buffer.size, - FFI::MemoryPointer.new(:uint, 1), + FFI::MemoryPointer.new(:dword, 1), nil ) return out_buffer if result 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 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( Puppet::Util::Windows::String.wide_string(link_name.to_s), GENERIC_READ, FILE_SHARE_READ, nil, # security_attributes OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, 0) # template_file ensure - API.close_handle(handle) if handle + 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) # must be multiple of 1024, min 10240 - out_buffer = FFI::MemoryPointer.new(API::ReparseDataBuffer.size) + out_buffer = FFI::MemoryPointer.new(REPARSE_DATA_BUFFER.size) device_io_control(handle, FSCTL_GET_REPARSE_POINT, nil, out_buffer) - reparse_data = API::ReparseDataBuffer.new(out_buffer) - offset = reparse_data[:print_name_offset] - length = reparse_data[:print_name_length] + reparse_data = REPARSE_DATA_BUFFER.new(out_buffer) + offset = reparse_data[:PrintNameOffset] + length = reparse_data[:PrintNameLength] - result = reparse_data[:path_buffer].to_a[offset, length].pack('C*') + result = reparse_data[:PathBuffer].to_a[offset, length].pack('C*') result.force_encoding('UTF-16LE').encode(Encoding.default_external) end end diff --git a/lib/puppet/util/windows/process.rb b/lib/puppet/util/windows/process.rb index c1f0bedd4..1128dd673 100644 --- a/lib/puppet/util/windows/process.rb +++ b/lib/puppet/util/windows/process.rb @@ -1,238 +1,251 @@ require 'puppet/util/windows' require 'windows/process' require 'windows/handle' require 'windows/synchronize' +require 'ffi' module Puppet::Util::Windows::Process extend ::Windows::Process extend ::Windows::Handle extend ::Windows::Synchronize - module API - require 'ffi' - extend FFI::Library - ffi_convention :stdcall - - ffi_lib 'kernel32' - - # http://msdn.microsoft.com/en-us/library/windows/desktop/ms683179(v=vs.85).aspx - # HANDLE WINAPI GetCurrentProcess(void); - attach_function :get_current_process, :GetCurrentProcess, [], :uint - - # BOOL WINAPI CloseHandle( - # _In_ HANDLE hObject - # ); - attach_function :close_handle, :CloseHandle, [:uint], :bool - - ffi_lib 'advapi32' - - # 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 - # ); - attach_function :open_process_token, :OpenProcessToken, - [:uint, :uint, :pointer], :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 :low_part, :uint, - :high_part, :int - 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 - # ); - attach_function :lookup_privilege_value, :LookupPrivilegeValueA, - [:string, :string, :pointer], :bool - - Token_Information = enum( - :token_user, 1, - :token_groups, - :token_privileges, - :token_owner, - :token_primary_group, - :token_default_dacl, - :token_source, - :token_type, - :token_impersonation_level, - :token_statistics, - :token_restricted_sids, - :token_session_id, - :token_groups_and_privileges, - :token_session_reference, - :token_sandbox_inert, - :token_audit_policy, - :token_origin, - :token_elevation_type, - :token_linked_token, - :token_elevation, - :token_has_restrictions, - :token_access_information, - :token_virtualization_allowed, - :token_virtualization_enabled, - :token_integrity_level, - :token_ui_access, - :token_mandatory_policy, - :token_logon_sid, - :max_token_info_class - ) - - # 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, :uint - end + extend FFI::Library + ffi_convention :stdcall + + # 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], :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], :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/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 :privilege_count, :uint, - :privileges, [LUID_And_Attributes, 1] # placeholder for offset - 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 :LookupPrivilegeValueA, + [:lpcstr, :lpcstr, :pointer], :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/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 - # ); - attach_function :get_token_information, :GetTokenInformation, - [:uint, Token_Information, :pointer, :uint, :pointer ], :bool + # 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/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 ], :bool + 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) == Windows::Synchronize::WAIT_TIMEOUT sleep(1) end exit_status = [0].pack('L') unless GetExitCodeProcess(handle, exit_status) raise Puppet::Util::Windows::Error.new("Failed to get child process exit code") end exit_status = exit_status.unpack('L').first # $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 - API.get_current_process + GetCurrentProcess() end module_function :get_current_process def open_process_token(handle, desired_access) - token_handle_ptr = FFI::MemoryPointer.new(:uint, 1) - result = API.open_process_token(handle, desired_access, token_handle_ptr) + token_handle_ptr = FFI::MemoryPointer.new(:handle, 1) + result = OpenProcessToken(handle, desired_access, token_handle_ptr) if !result raise Puppet::Util::Windows::Error.new( "OpenProcessToken(#{handle}, #{desired_access.to_s(8)}, #{token_handle_ptr})") end begin yield token_handle = token_handle_ptr.read_uint ensure - API.close_handle(token_handle) + CloseHandle(token_handle) end end module_function :open_process_token def lookup_privilege_value(name, system_name = '') - luid = FFI::MemoryPointer.new(API::LUID.size) - result = API.lookup_privilege_value( + luid = FFI::MemoryPointer.new(LUID.size) + result = LookupPrivilegeValueA( system_name, name.to_s, luid ) - return API::LUID.new(luid) if result + return LUID.new(luid) if result raise Puppet::Util::Windows::Error.new( "LookupPrivilegeValue(#{system_name}, #{name}, #{luid})") end module_function :lookup_privilege_value def get_token_information(token_handle, token_information) # to determine buffer size - return_length_ptr = FFI::MemoryPointer.new(:uint, 1) - result = API.get_token_information(token_handle, token_information, nil, 0, return_length_ptr) + 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_uint 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 token_information_buf = FFI::MemoryPointer.new(return_length) - result = API.get_token_information(token_handle, token_information, + result = GetTokenInformation(token_handle, token_information, token_information_buf, return_length, return_length_ptr) if !result raise Puppet::Util::Windows::Error.new( "GetTokenInformation(#{token_handle}, #{token_information}, #{token_information_buf}, " + "#{return_length}, #{return_length_ptr})") end - raw_privileges = API::Token_Privileges.new(token_information_buf) - privileges = { :count => raw_privileges[:privilege_count], :privileges => [] } + raw_privileges = TOKEN_PRIVILEGES.new(token_information_buf) + privileges = { :count => raw_privileges[:PrivilegeCount], :privileges => [] } - offset = token_information_buf + API::Token_Privileges.offset_of(:privileges) - privilege_ptr = FFI::Pointer.new(API::LUID_And_Attributes, offset) + 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 + # extract each instance of LUID_AND_ATTRIBUTES 0.upto(privileges[:count] - 1) do |i| - privileges[:privileges] << API::LUID_And_Attributes.new(privilege_ptr[i]) + privileges[:privileges] << LUID_AND_ATTRIBUTES.new(privilege_ptr[i]) end privileges end module_function :get_token_information TOKEN_ALL_ACCESS = 0xF01FF ERROR_NO_SUCH_PRIVILEGE = 1313 def process_privilege_symlink? 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, :token_privileges) - token_info[:privileges].any? { |p| p[:luid].values == luid.values } + token_info = get_token_information(token_handle, :TokenPrivileges) + token_info[:privileges].any? { |p| p[:Luid].values == luid.values } end 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? end diff --git a/lib/puppet/util/windows/root_certs.rb b/lib/puppet/util/windows/root_certs.rb index 7988ab832..07f9daa1e 100644 --- a/lib/puppet/util/windows/root_certs.rb +++ b/lib/puppet/util/windows/root_certs.rb @@ -1,101 +1,108 @@ require 'puppet/util/windows' require 'openssl' require 'ffi' # Represents a collection of trusted root certificates. # # @api public class Puppet::Util::Windows::RootCerts include Enumerable extend FFI::Library - typedef :ulong, :dword - typedef :uintptr_t, :handle - def initialize(roots) @roots = roots end # Enumerates each root certificate. # @yieldparam cert [OpenSSL::X509::Certificate] each root certificate # @api public def each @roots.each {|cert| yield cert} end # Returns a new instance. # @return [Puppet::Util::Windows::RootCerts] object constructed from current root certificates def self.instance new(self.load_certs) end # Returns an array of root certificates. # # @return [Array<[OpenSSL::X509::Certificate]>] an array of root certificates # @api private def self.load_certs certs = [] # This is based on a patch submitted to openssl: # http://www.mail-archive.com/openssl-dev@openssl.org/msg26958.html ptr = FFI::Pointer::NULL store = CertOpenSystemStoreA(nil, "ROOT") begin while (ptr = CertEnumCertificatesInStore(store, ptr)) and not ptr.null? context = CERT_CONTEXT.new(ptr) cert_buf = context[:pbCertEncoded].read_bytes(context[:cbCertEncoded]) begin certs << OpenSSL::X509::Certificate.new(cert_buf) rescue => detail Puppet.warning("Failed to import root certificate: #{detail.inspect}") end end ensure CertCloseStore(store, 0) end certs end - private - - # typedef ULONG_PTR HCRYPTPROV_LEGACY; + ffi_convention :stdcall # typedef void *HCERTSTORE; + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa377189(v=vs.85).aspx + # typedef struct _CERT_CONTEXT { + # DWORD dwCertEncodingType; + # BYTE *pbCertEncoded; + # DWORD cbCertEncoded; + # PCERT_INFO pCertInfo; + # HCERTSTORE hCertStore; + # } CERT_CONTEXT, *PCERT_CONTEXT;typedef const CERT_CONTEXT *PCCERT_CONTEXT; class CERT_CONTEXT < FFI::Struct layout( :dwCertEncodingType, :dword, :pbCertEncoded, :pointer, :cbCertEncoded, :dword, :pCertInfo, :pointer, :hCertStore, :handle ) end + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa376560(v=vs.85).aspx # HCERTSTORE # WINAPI # CertOpenSystemStoreA( # __in_opt HCRYPTPROV_LEGACY hProv, # __in LPCSTR szSubsystemProtocol # ); + # typedef ULONG_PTR HCRYPTPROV_LEGACY; ffi_lib :crypt32 - attach_function :CertOpenSystemStoreA, [:pointer, :string], :handle + attach_function_private :CertOpenSystemStoreA, [:ulong_ptr, :lpcstr], :handle + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa376050(v=vs.85).aspx # PCCERT_CONTEXT # WINAPI # CertEnumCertificatesInStore( # __in HCERTSTORE hCertStore, # __in_opt PCCERT_CONTEXT pPrevCertContext # ); ffi_lib :crypt32 - attach_function :CertEnumCertificatesInStore, [:handle, :pointer], :pointer + attach_function_private :CertEnumCertificatesInStore, [:handle, :pointer], :pointer + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa376026(v=vs.85).aspx # BOOL # WINAPI # CertCloseStore( # __in_opt HCERTSTORE hCertStore, # __in DWORD dwFlags # ); ffi_lib :crypt32 - attach_function :CertCloseStore, [:handle, :dword], :bool + attach_function_private :CertCloseStore, [:handle, :dword], :bool end diff --git a/lib/puppet/util/windows/security.rb b/lib/puppet/util/windows/security.rb index d1c1bcd34..f54ca2632 100644 --- a/lib/puppet/util/windows/security.rb +++ b/lib/puppet/util/windows/security.rb @@ -1,651 +1,652 @@ # 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| raise Puppet::Util::Windows::Error.new("Invalid SID") unless IsValidSid(sid_ptr) unless AddAccessAllowedAceEx(acl, ACL_REVISION, inherit, mask, sid_ptr) 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| raise Puppet::Util::Windows::Error.new("Invalid SID") unless IsValidSid(sid_ptr) unless AddAccessDeniedAceEx(acl, ACL_REVISION, inherit, mask, sid_ptr) 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 = ace_ptr.unpack('L')[0] + 8 # address of ace_ptr->SidStart raise Puppet::Util::Windows::Error.new("Failed to read DACL, invalid SID") unless IsValidSid(sid_ptr) sid = sid_ptr_to_string(sid_ptr) dacl.allow(sid, mask, ace_flags) when ACCESS_DENIED_ACE_TYPE sid_ptr = ace_ptr.unpack('L')[0] + 8 # address of ace_ptr->SidStart raise Puppet::Util::Windows::Error.new("Failed to read DACL, invalid SID") unless IsValidSid(sid_ptr) 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(owner_sid.unpack('L')[0]) group = sid_ptr_to_string(group_sid.unpack('L')[0]) - control = FFI::MemoryPointer.new(:uint16, 1) - revision = FFI::MemoryPointer.new(:uint32, 1) + control = FFI::MemoryPointer.new(:word, 1) + revision = FFI::MemoryPointer.new(:dword, 1) ffsd = FFI::Pointer.new(ppsd.unpack('L')[0]) - if ! API.get_security_descriptor_control(ffsd, control, revision) + if ! GetSecurityDescriptorControl(ffsd, control, revision) raise Puppet::Util::Windows::Error.new("Failed to get security descriptor control") end 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) 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, groupsid, acl, nil) raise Puppet::Util::Windows::Error.new("Failed to set security information") unless rv == ERROR_SUCCESS end end end end end end - module API - extend FFI::Library - ffi_lib 'kernel32' - ffi_convention :stdcall - - # 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 :get_security_descriptor_control, :GetSecurityDescriptorControl, [:pointer, :pointer, :pointer], :bool - 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], :bool end