diff --git a/lib/puppet/util/windows/api_types.rb b/lib/puppet/util/windows/api_types.rb index b1ac804e1..8f236060f 100644 --- a/lib/puppet/util/windows/api_types.rb +++ b/lib/puppet/util/windows/api_types.rb @@ -1,102 +1,121 @@ require 'ffi' require 'puppet/util/windows/string' module Puppet::Util::Windows::APITypes module ::FFI WIN32_FALSE = 0 end module ::FFI::Library # Wrapper method for attach_function + private def attach_function_private(*args) attach_function(*args) private args[0] end end class ::FFI::Pointer NULL_HANDLE = 0 + NULL_TERMINATOR_WCHAR = 0 def self.from_string_to_wide_string(str) str = Puppet::Util::Windows::String.wide_string(str) ptr = FFI::MemoryPointer.new(:byte, str.bytesize) # uchar here is synonymous with byte ptr.put_array_of_uchar(0, str.bytes.to_a) ptr end def read_win32_bool # BOOL is always a 32-bit integer in Win32 # some Win32 APIs return 1 for true, while others are non-0 read_int32 != FFI::WIN32_FALSE end alias_method :read_dword, :read_uint32 def read_handle type_size == 4 ? read_uint32 : read_uint64 end + alias_method :read_wchar, :read_uint16 + def read_wide_string(char_length) # char_length is number of wide chars (typically excluding NULLs), *not* bytes str = get_bytes(0, char_length * 2).force_encoding('UTF-16LE') str.encode(Encoding.default_external) end + def read_arbitrary_wide_string_up_to(max_char_length = 512) + # max_char_length is number of wide chars (typically excluding NULLs), *not* bytes + # use a pointer to read one UTF-16LE char (2 bytes) at a time + wchar_ptr = FFI::Pointer.new(:wchar, address) + + # now iterate 2 bytes at a time until an offset lower than max_char_length is found + 0.upto(max_char_length - 1) do |i| + if wchar_ptr[i].read_wchar == NULL_TERMINATOR_WCHAR + return read_wide_string(i) + end + end + + read_wide_string(max_char_length) + end + alias_method :write_dword, :write_uint32 end # FFI Types # https://github.com/ffi/ffi/wiki/Types # Windows - Common Data Types # http://msdn.microsoft.com/en-us/library/cc230309.aspx # Windows Data Types # http://msdn.microsoft.com/en-us/library/windows/desktop/aa383751(v=vs.85).aspx FFI.typedef :uint16, :word FFI.typedef :uint32, :dword # uintptr_t is defined in an FFI conf as platform specific, either # ulong_long on x64 or just ulong on x86 FFI.typedef :uintptr_t, :handle # buffer_inout is similar to pointer (platform specific), but optimized for buffers FFI.typedef :buffer_inout, :lpwstr # buffer_in is similar to pointer (platform specific), but optimized for CONST read only buffers FFI.typedef :buffer_in, :lpcwstr # string is also similar to pointer, but should be used for const char * # NOTE that this is not wide, useful only for A suffixed functions FFI.typedef :string, :lpcstr # pointer in FFI is platform specific # NOTE: for API calls with reserved lpvoid parameters, pass a FFI::Pointer::NULL FFI.typedef :pointer, :lpcvoid FFI.typedef :pointer, :lpvoid FFI.typedef :pointer, :lpword FFI.typedef :pointer, :lpdword FFI.typedef :pointer, :pdword FFI.typedef :pointer, :phandle FFI.typedef :pointer, :ulong_ptr FFI.typedef :pointer, :pbool # any time LONG / ULONG is in a win32 API definition DO NOT USE platform specific width # which is what FFI uses by default # instead create new aliases for these very special cases # NOTE: not a good idea to redefine FFI :ulong since other typedefs may rely on it FFI.typedef :uint32, :win32_ulong FFI.typedef :int32, :win32_long # FFI bool can be only 1 byte at times, # Win32 BOOL is a signed int, and is always 4 bytes, even on x64 # http://blogs.msdn.com/b/oldnewthing/archive/2011/03/28/10146459.aspx FFI.typedef :int32, :win32_bool # NOTE: FFI already defines (u)short as a 16-bit (un)signed like this: # FFI.typedef :uint16, :ushort # FFI.typedef :int16, :short # 8 bits per byte FFI.typedef :uchar, :byte + FFI.typedef :uint16, :wchar end diff --git a/lib/puppet/util/windows/security.rb b/lib/puppet/util/windows/security.rb index 60b594840..38cf018b0 100644 --- a/lib/puppet/util/windows/security.rb +++ b/lib/puppet/util/windows/security.rb @@ -1,652 +1,661 @@ # 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) + 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) + 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| - raise Puppet::Util::Windows::Error.new("Invalid SID") unless IsValidSid(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) + 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 = 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_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 = 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_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(owner_sid.unpack('L')[0]) - group = sid_ptr_to_string(group_sid.unpack('L')[0]) + 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(ppsd.unpack('L')[0]) + 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 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, + 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 90b48d933..c51d9bebe 100644 --- a/lib/puppet/util/windows/sid.rb +++ b/lib/puppet/util/windows/sid.rb @@ -1,118 +1,164 @@ require 'puppet/util/windows' module Puppet::Util::Windows module SID - require 'windows/security' - include ::Windows::Security - - require 'windows/memory' - include ::Windows::Memory - - require 'windows/msvcrt/string' - include ::Windows::MSVCRT::String + 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) - sid_buf = 0.chr * 256 - str_ptr = 0.chr * 4 - - raise Puppet::Util::Windows::Error.new("Invalid SID") unless IsValidSid(psid) + if ! psid.instance_of?(FFI::Pointer) || IsValidSid(psid) == FFI::WIN32_FALSE + raise Puppet::Util::Windows::Error.new("Invalid SID") + end - raise Puppet::Util::Windows::Error.new("Failed to convert binary SID") unless ConvertSidToStringSid(psid, str_ptr) + buffer_ptr = FFI::MemoryPointer.new(:pointer, 1) begin - strncpy(sid_buf, str_ptr.unpack('L')[0], sid_buf.size - 1) - sid_buf[sid_buf.size - 1] = 0.chr - return sid_buf.strip + if ConvertSidToStringSidW(psid, buffer_ptr) == FFI::WIN32_FALSE + raise Puppet::Util::Windows::Error.new("Failed to convert binary SID") + end + + wide_string_ptr = buffer_ptr.read_pointer + + if wide_string_ptr.null? + raise Puppet::Error.new("ConvertSidToStringSidW failed to allocate buffer for sid") + end + + return wide_string_ptr.read_arbitrary_wide_string_up_to(MAXIMUM_SID_STRING_LENGTH) ensure - LocalFree(str_ptr.unpack('L')[0]) + if ! wide_string_ptr.nil? && ! wide_string_ptr.null? + if LocalFree(wide_string_ptr.address) != FFI::Pointer::NULL_HANDLE + Puppet.debug "LocalFree memory leak" + end + end end 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, &block) - sid_buf = 0.chr * 80 - string_addr = [string].pack('p*').unpack('L')[0] + def string_to_sid_ptr(string_sid, &block) + lpcwstr = FFI::MemoryPointer.from_string_to_wide_string(string_sid) + sid_ptr_ptr = FFI::MemoryPointer.new(:pointer, 1) - raise Puppet::Util::Windows::Error.new("Failed to convert string SID: #{string}") unless ConvertStringSidToSid(string_addr, sid_buf) + 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 = sid_buf.unpack('L')[0] begin - yield sid_ptr + yield sid_ptr = sid_ptr_ptr.read_pointer ensure - LocalFree(sid_ptr) + if LocalFree(sid_ptr.address) != FFI::Pointer::NULL_HANDLE + Puppet.debug "LocalFree memory leak" + end end end # Return true if the string is a valid SID, e.g. "S-1-5-32-544", false otherwise. - def valid_sid?(string) - string_to_sid_ptr(string) { |ptr| true } + def valid_sid?(string_sid) + string_to_sid_ptr(string_sid) { |ptr| true } rescue Puppet::Util::Windows::Error => e if e.code == ERROR_INVALID_SID_STRUCTURE false else raise end end + + ffi_convention :stdcall + + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379151(v=vs.85).aspx + # BOOL WINAPI IsValidSid( + # _In_ PSID pSid + # ); + ffi_lib :advapi32 + attach_function_private :IsValidSid, + [:pointer], :win32_bool + + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa376399(v=vs.85).aspx + # BOOL ConvertSidToStringSid( + # _In_ PSID Sid, + # _Out_ LPTSTR *StringSid + # ); + ffi_lib :advapi32 + attach_function_private :ConvertSidToStringSidW, + [:pointer, :pointer], :win32_bool + + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa376402(v=vs.85).aspx + # BOOL WINAPI ConvertStringSidToSid( + # _In_ LPCTSTR StringSid, + # _Out_ PSID *Sid + # ); + ffi_lib :advapi32 + attach_function_private :ConvertStringSidToSidW, + [:lpcwstr, :pointer], :win32_bool + + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa366730(v=vs.85).aspx + # HLOCAL WINAPI LocalFree( + # _In_ HLOCAL hMem + # ); + ffi_lib :kernel32 + attach_function_private :LocalFree, [:handle], :handle end end