diff --git a/lib/puppet/util/windows/adsi.rb b/lib/puppet/util/windows/adsi.rb index 041be7765..4da2faeb5 100644 --- a/lib/puppet/util/windows/adsi.rb +++ b/lib/puppet/util/windows/adsi.rb @@ -1,430 +1,410 @@ 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 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 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 SystemCallError 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::SID.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 extend FFI::Library 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::SID.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 # UNLEN from lmcons.h - http://stackoverflow.com/a/2155176 MAX_USERNAME_LENGTH = 256 def self.current_user_name user_name = '' max_length = MAX_USERNAME_LENGTH + 1 # NULL terminated 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 GetUserNameW(buffer, buffer_size) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Failed to get user name") end # buffer_size includes trailing NULL user_name = buffer.read_wide_string(buffer_size.read_dword - 1) end end user_name 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 ffi_convention :stdcall # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724432(v=vs.85).aspx # BOOL WINAPI GetUserName( # _Out_ LPTSTR lpBuffer, # _Inout_ LPDWORD lpnSize # ); ffi_lib :advapi32 attach_function_private :GetUserNameW, [:lpwstr, :lpdword], :win32_bool 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, :sid 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 sid @sid ||= Puppet::Util::Windows::SID.octet_string_to_sid_object(native_group.objectSID) 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::SID.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::SID.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/file.rb b/lib/puppet/util/windows/file.rb index 07ffc1cf4..b7c0cb4ff 100644 --- a/lib/puppet/util/windows/file.rb +++ b/lib/puppet/util/windows/file.rb @@ -1,401 +1,395 @@ require 'puppet/util/windows' module Puppet::Util::Windows::File require 'ffi' extend FFI::Library extend Puppet::Util::Windows::String FILE_ATTRIBUTE_READONLY = 0x00000001 SYNCHRONIZE = 0x100000 STANDARD_RIGHTS_REQUIRED = 0xf0000 STANDARD_RIGHTS_READ = 0x20000 STANDARD_RIGHTS_WRITE = 0x20000 STANDARD_RIGHTS_EXECUTE = 0x20000 STANDARD_RIGHTS_ALL = 0x1F0000 SPECIFIC_RIGHTS_ALL = 0xFFFF FILE_READ_DATA = 1 FILE_WRITE_DATA = 2 FILE_APPEND_DATA = 4 FILE_READ_EA = 8 FILE_WRITE_EA = 16 FILE_EXECUTE = 32 FILE_DELETE_CHILD = 64 FILE_READ_ATTRIBUTES = 128 FILE_WRITE_ATTRIBUTES = 256 FILE_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x1FF FILE_GENERIC_READ = STANDARD_RIGHTS_READ | FILE_READ_DATA | FILE_READ_ATTRIBUTES | FILE_READ_EA | SYNCHRONIZE FILE_GENERIC_WRITE = STANDARD_RIGHTS_WRITE | FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA | SYNCHRONIZE FILE_GENERIC_EXECUTE = STANDARD_RIGHTS_EXECUTE | FILE_READ_ATTRIBUTES | FILE_EXECUTE | SYNCHRONIZE 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 get_file_attributes(file_name) - Puppet.deprecation_warning('Puppet::Util::Windows::File.get_file_attributes is deprecated; please use Puppet::Util::Windows::File.get_attributes') - get_attributes(file_name) - end - module_function :get_file_attributes - def get_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 module_function :get_attributes def add_attributes(path, flags) oldattrs = get_attributes(path) if (oldattrs | flags) != oldattrs set_attributes(path, oldattrs | flags) end end module_function :add_attributes def remove_attributes(path, flags) oldattrs = get_attributes(path) if (oldattrs & ~flags) != oldattrs set_attributes(path, oldattrs & ~flags) end end module_function :remove_attributes def set_attributes(path, flags) success = SetFileAttributesW(wide_string(path), flags) != FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Failed to set file attributes") if !success success end module_function :set_attributes #define INVALID_HANDLE_VALUE ((HANDLE)(LONG_PTR)-1) INVALID_HANDLE_VALUE = FFI::Pointer.new(-1).address 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 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 out_buffer end FILE_ATTRIBUTE_REPARSE_POINT = 0x400 def symlink?(file_name) begin attributes = get_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 GENERIC_WRITE = 0x40000000 GENERIC_EXECUTE = 0x20000000 GENERIC_ALL = 0x10000000 FILE_SHARE_READ = 1 FILE_SHARE_WRITE = 2 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 FFI::WIN32.CloseHandle(handle) if handle end # handle has had CloseHandle called against it, so nothing to return nil end def readlink(link_name) link = nil open_symlink(link_name) do |handle| link = resolve_symlink(handle) end link end module_function :readlink def stat(file_name) file_name = file_name.to_s # accomodate PathName or String stat = File.stat(file_name) singleton_class = class << stat; self; end target_path = file_name if symlink?(file_name) target_path = readlink(file_name) link_ftype = File.stat(target_path).ftype # sigh, monkey patch instance method for instance, and close over link_ftype singleton_class.send(:define_method, :ftype) do link_ftype end end singleton_class.send(:define_method, :mode) do Puppet::Util::Windows::Security.get_mode(target_path) end stat end module_function :stat def lstat(file_name) file_name = file_name.to_s # accomodate PathName or String # monkey'ing around! stat = File.lstat(file_name) singleton_class = class << stat; self; end singleton_class.send(:define_method, :mode) do Puppet::Util::Windows::Security.get_mode(file_name) end if symlink?(file_name) def stat.ftype "link" end end stat end module_function :lstat private # http://msdn.microsoft.com/en-us/library/windows/desktop/aa364571(v=vs.85).aspx FSCTL_GET_REPARSE_POINT = 0x900a8 def self.resolve_symlink(handle) path = nil get_reparse_point_data(handle) do |reparse_data| offset = reparse_data[:PrintNameOffset] length = reparse_data[:PrintNameLength] ptr = reparse_data.pointer + reparse_data.offset_of(:PathBuffer) + offset path = ptr.read_wide_string(length / 2) # length is bytes, need UTF-16 wchars end 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 # http://msdn.microsoft.com/en-us/library/windows/desktop/aa364944(v=vs.85).aspx # DWORD WINAPI GetFileAttributes( # _In_ LPCTSTR lpFileName # ); ffi_lib :kernel32 attach_function_private :GetFileAttributesW, [:lpcwstr], :dword # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365535(v=vs.85).aspx # BOOL WINAPI SetFileAttributes( # _In_ LPCTSTR lpFileName, # _In_ DWORD dwFileAttributes # ); ffi_lib :kernel32 attach_function_private :SetFileAttributesW, [:lpcwstr, :dword], :win32_bool # 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 end diff --git a/lib/puppet/util/windows/security.rb b/lib/puppet/util/windows/security.rb index 8dc03a9a2..b7f6c3d85 100644 --- a/lib/puppet/util/windows/security.rb +++ b/lib/puppet/util/windows/security.rb @@ -1,920 +1,856 @@ # 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' module Puppet::Util::Windows::Security include Puppet::Util::Windows::String 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 FILE = Puppet::Util::Windows::File SE_BACKUP_NAME = 'SeBackupPrivilege' SE_RESTORE_NAME = 'SeRestorePrivilege' DELETE = 0x00010000 READ_CONTROL = 0x20000 WRITE_DAC = 0x40000 WRITE_OWNER = 0x80000 OWNER_SECURITY_INFORMATION = 1 GROUP_SECURITY_INFORMATION = 2 DACL_SECURITY_INFORMATION = 4 # 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 FILE_PERSISTENT_ACLS = 0x00000008 def supports_acl?(path) supported = false root = Pathname.new(path).enum_for(:ascend).to_a.last.to_s # 'A trailing backslash is required' root = "#{root}\\" unless root =~ /[\/\\]$/ FFI::MemoryPointer.new(:pointer, 1) do |flags_ptr| if GetVolumeInformationW(wide_string(root), FFI::Pointer::NULL, 0, FFI::Pointer::NULL, FFI::Pointer::NULL, flags_ptr, FFI::Pointer::NULL, 0) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Failed to get volume information") end supported = flags_ptr.read_dword & FILE_PERSISTENT_ACLS == FILE_PERSISTENT_ACLS end supported end - def get_attributes(path) - Puppet.deprecation_warning('Puppet::Util::Windows::Security.get_attributes is deprecated; please use Puppet::Util::Windows::File.get_attributes') - FILE.get_attributes(file_name) - end - - def add_attributes(path, flags) - Puppet.deprecation_warning('Puppet::Util::Windows::Security.add_attributes is deprecated; please use Puppet::Util::Windows::File.add_attributes') - FILE.add_attributes(path, flags) - end - - def remove_attributes(path, flags) - Puppet.deprecation_warning('Puppet::Util::Windows::Security.remove_attributes is deprecated; please use Puppet::Util::Windows::File.remove_attributes') - FILE.remove_attributes(path, flags) - end - - def set_attributes(path, flags) - Puppet.deprecation_warning('Puppet::Util::Windows::Security.set_attributes is deprecated; please use Puppet::Util::Windows::File.set_attributes') - FILE.set_attributes(path, flags) - end - MASK_TO_MODE = { FILE::FILE_GENERIC_READ => S_IROTH, FILE::FILE_GENERIC_WRITE => S_IWOTH, (FILE::FILE_GENERIC_EXECUTE & ~FILE::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::FILE_WRITE_DATA | FILE::FILE_EXECUTE | FILE::FILE_DELETE_CHILD)) == (FILE::FILE_WRITE_DATA | FILE::FILE_EXECUTE) mode |= S_ISVTX; end when well_known_nobody_sid if (ace.mask & FILE::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::FILE_GENERIC_READ, S_IWOTH => FILE::FILE_GENERIC_WRITE, S_IXOTH => (FILE::FILE_GENERIC_EXECUTE & ~FILE::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 = FILE::STANDARD_RIGHTS_ALL | FILE::FILE_READ_ATTRIBUTES | FILE::FILE_WRITE_ATTRIBUTES group_allow = FILE::STANDARD_RIGHTS_READ | FILE::FILE_READ_ATTRIBUTES | FILE::SYNCHRONIZE other_allow = FILE::STANDARD_RIGHTS_READ | FILE::FILE_READ_ATTRIBUTES | FILE::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::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::FILE_ALL_ACCESS end isdir = File.directory?(path) if isdir if (mode & (S_IWUSR | S_IXUSR)) == (S_IWUSR | S_IXUSR) owner_allow |= FILE::FILE_DELETE_CHILD end if (mode & (S_IWGRP | S_IXGRP)) == (S_IWGRP | S_IXGRP) && (mode & S_ISVTX) == 0 group_allow |= FILE::FILE_DELETE_CHILD end if (mode & (S_IWOTH | S_IXOTH)) == (S_IWOTH | S_IXOTH) && (mode & S_ISVTX) == 0 other_allow |= FILE::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::FILE_WRITE_DATA) == FILE::FILE_WRITE_DATA FILE.remove_attributes(path, FILE::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 inherit_only = Puppet::Util::Windows::AccessControlEntry::INHERIT_ONLY_ACE if isdir inherit = inherit_only | Puppet::Util::Windows::AccessControlEntry::CONTAINER_INHERIT_ACE dacl.allow(Win32::Security::SID::CreatorOwner, owner_allow, inherit) dacl.allow(Win32::Security::SID::CreatorGroup, group_allow, inherit) inherit = inherit_only | Puppet::Util::Windows::AccessControlEntry::OBJECT_INHERIT_ACE dacl.allow(Win32::Security::SID::CreatorOwner, owner_allow & ~FILE::FILE_EXECUTE, inherit) dacl.allow(Win32::Security::SID::CreatorGroup, group_allow & ~FILE::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 ACL_REVISION = 2 def add_access_allowed_ace(acl, mask, sid, inherit = nil) inherit ||= NO_INHERITANCE Puppet::Util::Windows::SID.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 if AddAccessAllowedAceEx(acl, ACL_REVISION, inherit, mask, sid_ptr) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Failed to add access control entry") end end # ensure this method is void if it doesn't raise nil end def add_access_denied_ace(acl, mask, sid, inherit = nil) inherit ||= NO_INHERITANCE Puppet::Util::Windows::SID.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 if AddAccessDeniedAceEx(acl, ACL_REVISION, inherit, mask, sid_ptr) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Failed to add access control entry") end end # ensure this method is void if it doesn't raise nil end def parse_dacl(dacl_ptr) # REMIND: need to handle NULL DACL if IsValidAcl(dacl_ptr) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Invalid DACL") end dacl_struct = ACL.new(dacl_ptr) ace_count = dacl_struct[:AceCount] dacl = Puppet::Util::Windows::AccessControlList.new # deny all return dacl if ace_count == 0 0.upto(ace_count - 1) do |i| FFI::MemoryPointer.new(:pointer, 1) do |ace_ptr| next if GetAce(dacl_ptr, i, ace_ptr) == FFI::WIN32_FALSE # ACE structures vary depending on the type. We are only concerned with # ACCESS_ALLOWED_ACE and ACCESS_DENIED_ACEs, which have the same layout ace = GENERIC_ACCESS_ACE.new(ace_ptr.get_pointer(0)) #deref LPVOID * ace_type = ace[:Header][:AceType] if ace_type != Puppet::Util::Windows::AccessControlEntry::ACCESS_ALLOWED_ACE_TYPE && ace_type != Puppet::Util::Windows::AccessControlEntry::ACCESS_DENIED_ACE_TYPE Puppet.warning "Unsupported access control entry type: 0x#{ace_type.to_s(16)}" next end # using pointer addition gives the FFI::Pointer a size, but that's OK here sid = Puppet::Util::Windows::SID.sid_ptr_to_string(ace.pointer + GENERIC_ACCESS_ACE.offset_of(:SidStart)) mask = ace[:Mask] ace_flags = ace[:Header][:AceFlags] case ace_type when Puppet::Util::Windows::AccessControlEntry::ACCESS_ALLOWED_ACE_TYPE dacl.allow(sid, mask, ace_flags) when Puppet::Util::Windows::AccessControlEntry::ACCESS_DENIED_ACE_TYPE dacl.deny(sid, mask, ace_flags) end 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, &block) handle = CreateFileW( wide_string(path), access, FILE::FILE_SHARE_READ | FILE::FILE_SHARE_WRITE, FFI::Pointer::NULL, # security_attributes FILE::OPEN_EXISTING, FILE::FILE_FLAG_OPEN_REPARSE_POINT | FILE::FILE_FLAG_BACKUP_SEMANTICS, FFI::Pointer::NULL_HANDLE) # template if handle == Puppet::Util::Windows::File::INVALID_HANDLE_VALUE raise Puppet::Util::Windows::Error.new("Failed to open '#{path}'") end begin yield handle ensure FFI::WIN32.CloseHandle(handle) if handle end # handle has already had CloseHandle called against it, nothing to return nil end # Execute a block with the specified privilege enabled def with_privilege(privilege, &block) set_privilege(privilege, true) yield ensure set_privilege(privilege, false) end SE_PRIVILEGE_ENABLED = 0x00000002 TOKEN_ADJUST_PRIVILEGES = 0x0020 # 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? Puppet::Util::Windows::Process.with_process_token(TOKEN_ADJUST_PRIVILEGES) do |token| Puppet::Util::Windows::Process.lookup_privilege_value(privilege) do |luid| FFI::MemoryPointer.new(Puppet::Util::Windows::Process::LUID_AND_ATTRIBUTES.size) do |luid_and_attributes_ptr| # allocate unmanaged memory for structs that we clean up afterwards luid_and_attributes = Puppet::Util::Windows::Process::LUID_AND_ATTRIBUTES.new(luid_and_attributes_ptr) luid_and_attributes[:Luid] = luid luid_and_attributes[:Attributes] = enable ? SE_PRIVILEGE_ENABLED : 0 FFI::MemoryPointer.new(Puppet::Util::Windows::Process::TOKEN_PRIVILEGES.size) do |token_privileges_ptr| token_privileges = Puppet::Util::Windows::Process::TOKEN_PRIVILEGES.new(token_privileges_ptr) token_privileges[:PrivilegeCount] = 1 token_privileges[:Privileges][0] = luid_and_attributes # size is correct given we only have 1 LUID, otherwise would be: # [:PrivilegeCount].size + [:PrivilegeCount] * LUID_AND_ATTRIBUTES.size if AdjustTokenPrivileges(token, FFI::WIN32_FALSE, token_privileges, token_privileges.size, FFI::MemoryPointer::NULL, FFI::MemoryPointer::NULL) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Failed to adjust process privileges") end end end end end # token / luid structs freed by this point, so return true as nothing raised true end - def with_process_token(access, &block) - Puppet.deprecation_warning('Puppet::Util::Windows::Security.with_process_token is deprecated; please use Puppet::Util::Windows::Process.with_process_token') - Puppet::Util::Windows::Process.with_process_token(access) do |token| - yield token - end - - nil - end - def get_security_descriptor(path) sd = nil with_privilege(SE_BACKUP_NAME) do open_file(path, READ_CONTROL) do |handle| FFI::MemoryPointer.new(:pointer, 1) do |owner_sid_ptr_ptr| FFI::MemoryPointer.new(:pointer, 1) do |group_sid_ptr_ptr| FFI::MemoryPointer.new(:pointer, 1) do |dacl_ptr_ptr| FFI::MemoryPointer.new(:pointer, 1) do |sd_ptr_ptr| rv = GetSecurityInfo( handle, :SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, owner_sid_ptr_ptr, group_sid_ptr_ptr, dacl_ptr_ptr, FFI::Pointer::NULL, #sacl sd_ptr_ptr) #sec desc raise Puppet::Util::Windows::Error.new("Failed to get security information") if rv != FFI::ERROR_SUCCESS # these 2 convenience params are not freed since they point inside sd_ptr owner = Puppet::Util::Windows::SID.sid_ptr_to_string(owner_sid_ptr_ptr.get_pointer(0)) group = Puppet::Util::Windows::SID.sid_ptr_to_string(group_sid_ptr_ptr.get_pointer(0)) FFI::MemoryPointer.new(:word, 1) do |control| FFI::MemoryPointer.new(:dword, 1) do |revision| sd_ptr_ptr.read_win32_local_pointer do |sd_ptr| if GetSecurityDescriptorControl(sd_ptr, control, revision) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Failed to get security descriptor control") end protect = (control.read_word & SE_DACL_PROTECTED) == SE_DACL_PROTECTED dacl = parse_dacl(dacl_ptr_ptr.get_pointer(0)) sd = Puppet::Util::Windows::SecurityDescriptor.new(owner, group, dacl, protect) end end end end end end end end end sd end def get_max_generic_acl_size(ace_count) # http://msdn.microsoft.com/en-us/library/windows/desktop/aa378853(v=vs.85).aspx # To calculate the initial size of an ACL, add the following together, and then align the result to the nearest DWORD: # * Size of the ACL structure. # * Size of each ACE structure that the ACL is to contain minus the SidStart member (DWORD) of the ACE. # * Length of the SID that each ACE is to contain. ACL.size + ace_count * MAXIMUM_GENERIC_ACE_SIZE 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) FFI::MemoryPointer.new(:byte, get_max_generic_acl_size(sd.dacl.count)) do |acl_ptr| if InitializeAcl(acl_ptr, acl_ptr.size, ACL_REVISION) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Failed to initialize ACL") end if IsValidAcl(acl_ptr) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Invalid DACL") end with_privilege(SE_BACKUP_NAME) do with_privilege(SE_RESTORE_NAME) do open_file(path, READ_CONTROL | WRITE_DAC | WRITE_OWNER) do |handle| Puppet::Util::Windows::SID.string_to_sid_ptr(sd.owner) do |owner_sid_ptr| Puppet::Util::Windows::SID.string_to_sid_ptr(sd.group) do |group_sid_ptr| sd.dacl.each do |ace| case ace.type when Puppet::Util::Windows::AccessControlEntry::ACCESS_ALLOWED_ACE_TYPE #puts "ace: allow, sid #{Puppet::Util::Windows::SID.sid_to_name(ace.sid)}, mask 0x#{ace.mask.to_s(16)}" add_access_allowed_ace(acl_ptr, ace.mask, ace.sid, ace.flags) when Puppet::Util::Windows::AccessControlEntry::ACCESS_DENIED_ACE_TYPE #puts "ace: deny, sid #{Puppet::Util::Windows::SID.sid_to_name(ace.sid)}, mask 0x#{ace.mask.to_s(16)}" add_access_denied_ace(acl_ptr, 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, owner_sid_ptr, group_sid_ptr, acl_ptr, FFI::MemoryPointer::NULL) if rv != FFI::ERROR_SUCCESS raise Puppet::Util::Windows::Error.new("Failed to set security information") end end end end end end end - - def name_to_sid(name) - Puppet.deprecation_warning('Puppet::Util::Windows::Security.name_to_sid is deprecated; please use Puppet::Util::Windows::SID.name_to_sid') - Puppet::Util::Windows::SID.name_to_sid(name) - end - - def name_to_sid_object(name) - Puppet.deprecation_warning('Puppet::Util::Windows::Security.name_to_sid_object is deprecated; please use Puppet::Util::Windows::SID.name_to_sid_object') - Puppet::Util::Windows::SID.name_to_sid_object(name) - end - - def octet_string_to_sid_object(bytes) - Puppet.deprecation_warning('Puppet::Util::Windows::Security.octet_string_to_sid_object is deprecated; please use Puppet::Util::Windows::SID.octet_string_to_sid_object') - Puppet::Util::Windows::SID.octet_string_to_sid_object(bytes) - end - - def sid_to_name(value) - Puppet.deprecation_warning('Puppet::Util::Windows::Security.sid_to_name is deprecated; please use Puppet::Util::Windows::SID.sid_to_name') - Puppet::Util::Windows::SID.sid_to_name(value) - end - - def sid_ptr_to_string(psid) - Puppet.deprecation_warning('Puppet::Util::Windows::Security.sid_ptr_to_string is deprecated; please use Puppet::Util::Windows::SID.sid_ptr_to_string') - Puppet::Util::Windows::SID.sid_ptr_to_string(psid) - end - - def string_to_sid_ptr(string_sid, &block) - Puppet.deprecation_warning('Puppet::Util::Windows::Security.string_to_sid_ptr is deprecated; please use Puppet::Util::Windows::SID.string_to_sid_ptr') - Puppet::Util::Windows::SID.string_to_sid_ptr(string_sid, &block) - end - - def valid_sid?(string_sid) - Puppet.deprecation_warning('Puppet::Util::Windows::Security.valid_sid? is deprecated; please use Puppet::Util::Windows::SID.valid_sid?') - Puppet::Util::Windows::SID.valid_sid?(string_sid) - end end ffi_convention :stdcall # http://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx # 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/aa364993(v=vs.85).aspx # BOOL WINAPI GetVolumeInformation( # _In_opt_ LPCTSTR lpRootPathName, # _Out_opt_ LPTSTR lpVolumeNameBuffer, # _In_ DWORD nVolumeNameSize, # _Out_opt_ LPDWORD lpVolumeSerialNumber, # _Out_opt_ LPDWORD lpMaximumComponentLength, # _Out_opt_ LPDWORD lpFileSystemFlags, # _Out_opt_ LPTSTR lpFileSystemNameBuffer, # _In_ DWORD nFileSystemNameSize # ); ffi_lib :kernel32 attach_function_private :GetVolumeInformationW, [:lpcwstr, :lpwstr, :dword, :lpdword, :lpdword, :lpdword, :lpwstr, :dword], :win32_bool # http://msdn.microsoft.com/en-us/library/windows/desktop/aa374951(v=vs.85).aspx # BOOL WINAPI AddAccessAllowedAceEx( # _Inout_ PACL pAcl, # _In_ DWORD dwAceRevision, # _In_ DWORD AceFlags, # _In_ DWORD AccessMask, # _In_ PSID pSid # ); ffi_lib :advapi32 attach_function_private :AddAccessAllowedAceEx, [:pointer, :dword, :dword, :dword, :pointer], :win32_bool # http://msdn.microsoft.com/en-us/library/windows/desktop/aa374964(v=vs.85).aspx # BOOL WINAPI AddAccessDeniedAceEx( # _Inout_ PACL pAcl, # _In_ DWORD dwAceRevision, # _In_ DWORD AceFlags, # _In_ DWORD AccessMask, # _In_ PSID pSid # ); ffi_lib :advapi32 attach_function_private :AddAccessDeniedAceEx, [:pointer, :dword, :dword, :dword, :pointer], :win32_bool # http://msdn.microsoft.com/en-us/library/windows/desktop/aa374931(v=vs.85).aspx # typedef struct _ACL { # BYTE AclRevision; # BYTE Sbz1; # WORD AclSize; # WORD AceCount; # WORD Sbz2; # } ACL, *PACL; class ACL < FFI::Struct layout :AclRevision, :byte, :Sbz1, :byte, :AclSize, :word, :AceCount, :word, :Sbz2, :word end # http://msdn.microsoft.com/en-us/library/windows/desktop/aa374912(v=vs.85).aspx # ACE types # http://msdn.microsoft.com/en-us/library/windows/desktop/aa374919(v=vs.85).aspx # typedef struct _ACE_HEADER { # BYTE AceType; # BYTE AceFlags; # WORD AceSize; # } ACE_HEADER, *PACE_HEADER; class ACE_HEADER < FFI::Struct layout :AceType, :byte, :AceFlags, :byte, :AceSize, :word end # http://msdn.microsoft.com/en-us/library/windows/desktop/aa374892(v=vs.85).aspx # ACCESS_MASK # http://msdn.microsoft.com/en-us/library/windows/desktop/aa374847(v=vs.85).aspx # typedef struct _ACCESS_ALLOWED_ACE { # ACE_HEADER Header; # ACCESS_MASK Mask; # DWORD SidStart; # } ACCESS_ALLOWED_ACE, *PACCESS_ALLOWED_ACE; # # http://msdn.microsoft.com/en-us/library/windows/desktop/aa374879(v=vs.85).aspx # typedef struct _ACCESS_DENIED_ACE { # ACE_HEADER Header; # ACCESS_MASK Mask; # DWORD SidStart; # } ACCESS_DENIED_ACE, *PACCESS_DENIED_ACE; class GENERIC_ACCESS_ACE < FFI::Struct # ACE structures must be aligned on DWORD boundaries. All Windows # memory-management functions return DWORD-aligned handles to memory pack 4 layout :Header, ACE_HEADER, :Mask, :dword, :SidStart, :dword end # http://stackoverflow.com/a/1792930 MAXIMUM_SID_BYTES_LENGTH = 68 MAXIMUM_GENERIC_ACE_SIZE = GENERIC_ACCESS_ACE.offset_of(:SidStart) + MAXIMUM_SID_BYTES_LENGTH # http://msdn.microsoft.com/en-us/library/windows/desktop/aa446634(v=vs.85).aspx # BOOL WINAPI GetAce( # _In_ PACL pAcl, # _In_ DWORD dwAceIndex, # _Out_ LPVOID *pAce # ); ffi_lib :advapi32 attach_function_private :GetAce, [:pointer, :dword, :pointer], :win32_bool # http://msdn.microsoft.com/en-us/library/windows/desktop/aa375202(v=vs.85).aspx # BOOL WINAPI AdjustTokenPrivileges( # _In_ HANDLE TokenHandle, # _In_ BOOL DisableAllPrivileges, # _In_opt_ PTOKEN_PRIVILEGES NewState, # _In_ DWORD BufferLength, # _Out_opt_ PTOKEN_PRIVILEGES PreviousState, # _Out_opt_ PDWORD ReturnLength # ); ffi_lib :advapi32 attach_function_private :AdjustTokenPrivileges, [:handle, :win32_bool, :pointer, :dword, :pointer, :pdword], :win32_bool # 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 # http://msdn.microsoft.com/en-us/library/windows/desktop/aa378853(v=vs.85).aspx # BOOL WINAPI InitializeAcl( # _Out_ PACL pAcl, # _In_ DWORD nAclLength, # _In_ DWORD dwAclRevision # ); ffi_lib :advapi32 attach_function_private :InitializeAcl, [:pointer, :dword, :dword], :win32_bool # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379142(v=vs.85).aspx # BOOL WINAPI IsValidAcl( # _In_ PACL pAcl # ); ffi_lib :advapi32 attach_function_private :IsValidAcl, [:pointer], :win32_bool # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379593(v=vs.85).aspx SE_OBJECT_TYPE = enum( :SE_UNKNOWN_OBJECT_TYPE, 0, :SE_FILE_OBJECT, :SE_SERVICE, :SE_PRINTER, :SE_REGISTRY_KEY, :SE_LMSHARE, :SE_KERNEL_OBJECT, :SE_WINDOW_OBJECT, :SE_DS_OBJECT, :SE_DS_OBJECT_ALL, :SE_PROVIDER_DEFINED_OBJECT, :SE_WMIGUID_OBJECT, :SE_REGISTRY_WOW64_32KEY ) # http://msdn.microsoft.com/en-us/library/windows/desktop/aa446654(v=vs.85).aspx # DWORD WINAPI GetSecurityInfo( # _In_ HANDLE handle, # _In_ SE_OBJECT_TYPE ObjectType, # _In_ SECURITY_INFORMATION SecurityInfo, # _Out_opt_ PSID *ppsidOwner, # _Out_opt_ PSID *ppsidGroup, # _Out_opt_ PACL *ppDacl, # _Out_opt_ PACL *ppSacl, # _Out_opt_ PSECURITY_DESCRIPTOR *ppSecurityDescriptor # ); ffi_lib :advapi32 attach_function_private :GetSecurityInfo, [:handle, SE_OBJECT_TYPE, :dword, :pointer, :pointer, :pointer, :pointer, :pointer], :dword # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379588(v=vs.85).aspx # DWORD WINAPI SetSecurityInfo( # _In_ HANDLE handle, # _In_ SE_OBJECT_TYPE ObjectType, # _In_ SECURITY_INFORMATION SecurityInfo, # _In_opt_ PSID psidOwner, # _In_opt_ PSID psidGroup, # _In_opt_ PACL pDacl, # _In_opt_ PACL pSacl # ); ffi_lib :advapi32 # TODO: SECURITY_INFORMATION is actually a bitmask the size of a DWORD attach_function_private :SetSecurityInfo, [:handle, SE_OBJECT_TYPE, :dword, :pointer, :pointer, :pointer, :pointer], :dword end diff --git a/spec/unit/util/windows/adsi_spec.rb b/spec/unit/util/windows/adsi_spec.rb index f569d91e9..593c73256 100755 --- a/spec/unit/util/windows/adsi_spec.rb +++ b/spec/unit/util/windows/adsi_spec.rb @@ -1,440 +1,395 @@ #!/usr/bin/env ruby require 'spec_helper' require 'puppet/util/windows' describe Puppet::Util::Windows::ADSI, :if => Puppet.features.microsoft_windows? do let(:connection) { stub 'connection' } before(:each) do Puppet::Util::Windows::ADSI.instance_variable_set(:@computer_name, 'testcomputername') Puppet::Util::Windows::ADSI.stubs(:connect).returns connection end after(:each) do Puppet::Util::Windows::ADSI.instance_variable_set(:@computer_name, nil) end it "should generate the correct URI for a resource" do Puppet::Util::Windows::ADSI.uri('test', 'user').should == "WinNT://./test,user" end it "should be able to get the name of the computer" do Puppet::Util::Windows::ADSI.computer_name.should == 'testcomputername' end it "should be able to provide the correct WinNT base URI for the computer" do Puppet::Util::Windows::ADSI.computer_uri.should == "WinNT://." end it "should generate a fully qualified WinNT URI" do Puppet::Util::Windows::ADSI.computer_uri('testcomputername').should == "WinNT://testcomputername" end - describe ".sid_for_account" do - it "should return nil if the account does not exist" do - Puppet::Util::Windows::SID.expects(:name_to_sid).with('foobar').returns nil - - Puppet::Util::Windows::ADSI.sid_for_account('foobar').should be_nil - end - - it "should return a SID for a passed user or group name" do - Puppet::Util::Windows::SID.expects(:name_to_sid).with('testers').returns 'S-1-5-32-547' - - Puppet::Util::Windows::ADSI.sid_for_account('testers').should == 'S-1-5-32-547' - end - - it "should return a SID for a passed fully-qualified user or group name" do - Puppet::Util::Windows::SID.expects(:name_to_sid).with('MACHINE\testers').returns 'S-1-5-32-547' - - Puppet::Util::Windows::ADSI.sid_for_account('MACHINE\testers').should == 'S-1-5-32-547' - end - end - describe ".computer_name" do it "should return a non-empty ComputerName string" do Puppet::Util::Windows::ADSI.instance_variable_set(:@computer_name, nil) Puppet::Util::Windows::ADSI.computer_name.should_not be_empty end end describe ".sid_uri" do it "should raise an error when the input is not a SID object" do [Object.new, {}, 1, :symbol, '', nil].each do |input| expect { Puppet::Util::Windows::ADSI.sid_uri(input) }.to raise_error(Puppet::Error, /Must use a valid SID object/) end end it "should return a SID uri for a well-known SID (SYSTEM)" do sid = Win32::Security::SID.new('SYSTEM') Puppet::Util::Windows::ADSI.sid_uri(sid).should == 'WinNT://S-1-5-18' end end describe Puppet::Util::Windows::ADSI::User do let(:username) { 'testuser' } let(:domain) { 'DOMAIN' } let(:domain_username) { "#{domain}\\#{username}"} it "should generate the correct URI" do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) Puppet::Util::Windows::ADSI::User.uri(username).should == "WinNT://./#{username},user" end it "should generate the correct URI for a user with a domain" do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) Puppet::Util::Windows::ADSI::User.uri(username, domain).should == "WinNT://#{domain}/#{username},user" end it "should be able to parse a username without a domain" do Puppet::Util::Windows::ADSI::User.parse_name(username).should == [username, '.'] end it "should be able to parse a username with a domain" do Puppet::Util::Windows::ADSI::User.parse_name(domain_username).should == [username, domain] end it "should raise an error with a username that contains a /" do expect { Puppet::Util::Windows::ADSI::User.parse_name("#{domain}/#{username}") }.to raise_error(Puppet::Error, /Value must be in DOMAIN\\user style syntax/) end it "should be able to create a user" do adsi_user = stub('adsi') connection.expects(:Create).with('user', username).returns(adsi_user) Puppet::Util::Windows::ADSI::Group.expects(:exists?).with(username).returns(false) user = Puppet::Util::Windows::ADSI::User.create(username) user.should be_a(Puppet::Util::Windows::ADSI::User) user.native_user.should == adsi_user end it "should be able to check the existence of a user" do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) Puppet::Util::Windows::ADSI.expects(:connect).with("WinNT://./#{username},user").returns connection Puppet::Util::Windows::ADSI::User.exists?(username).should be_true end it "should be able to check the existence of a domain user" do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) Puppet::Util::Windows::ADSI.expects(:connect).with("WinNT://#{domain}/#{username},user").returns connection Puppet::Util::Windows::ADSI::User.exists?(domain_username).should be_true end it "should be able to confirm the existence of a user with a well-known SID" do system_user = Win32::Security::SID::LocalSystem # ensure that the underlying OS is queried here Puppet::Util::Windows::ADSI.unstub(:connect) Puppet::Util::Windows::ADSI::User.exists?(system_user).should be_true end it "should return nil with an unknown SID" do bogus_sid = 'S-1-2-3-4' # ensure that the underlying OS is queried here Puppet::Util::Windows::ADSI.unstub(:connect) Puppet::Util::Windows::ADSI::User.exists?(bogus_sid).should be_false end it "should be able to delete a user" do connection.expects(:Delete).with('user', username) Puppet::Util::Windows::ADSI::User.delete(username) end it "should return an enumeration of IADsUser wrapped objects" do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) name = 'Administrator' wmi_users = [stub('WMI', :name => name)] Puppet::Util::Windows::ADSI.expects(:execquery).with('select name from win32_useraccount where localaccount = "TRUE"').returns(wmi_users) native_user = stub('IADsUser') homedir = "C:\\Users\\#{name}" native_user.expects(:Get).with('HomeDirectory').returns(homedir) Puppet::Util::Windows::ADSI.expects(:connect).with("WinNT://./#{name},user").returns(native_user) users = Puppet::Util::Windows::ADSI::User.to_a users.length.should == 1 users[0].name.should == name users[0]['HomeDirectory'].should == homedir end describe "an instance" do let(:adsi_user) { stub('user', :objectSID => []) } let(:sid) { stub(:account => username, :domain => 'testcomputername') } let(:user) { Puppet::Util::Windows::ADSI::User.new(username, adsi_user) } it "should provide its groups as a list of names" do names = ["group1", "group2"] groups = names.map { |name| mock('group', :Name => name) } adsi_user.expects(:Groups).returns(groups) user.groups.should =~ names end it "should be able to test whether a given password is correct" do Puppet::Util::Windows::ADSI::User.expects(:logon).with(username, 'pwdwrong').returns(false) Puppet::Util::Windows::ADSI::User.expects(:logon).with(username, 'pwdright').returns(true) user.password_is?('pwdwrong').should be_false user.password_is?('pwdright').should be_true end it "should be able to set a password" do adsi_user.expects(:SetPassword).with('pwd') adsi_user.expects(:SetInfo).at_least_once flagname = "UserFlags" fADS_UF_DONT_EXPIRE_PASSWD = 0x10000 adsi_user.expects(:Get).with(flagname).returns(0) adsi_user.expects(:Put).with(flagname, fADS_UF_DONT_EXPIRE_PASSWD) user.password = 'pwd' end it "should generate the correct URI" do Puppet::Util::Windows::SID.stubs(:octet_string_to_sid_object).returns(sid) user.uri.should == "WinNT://testcomputername/#{username},user" end describe "when given a set of groups to which to add the user" do let(:groups_to_set) { 'group1,group2' } before(:each) do Puppet::Util::Windows::SID.stubs(:octet_string_to_sid_object).returns(sid) user.expects(:groups).returns ['group2', 'group3'] end describe "if membership is specified as inclusive" do it "should add the user to those groups, and remove it from groups not in the list" do group1 = stub 'group1' group1.expects(:Add).with("WinNT://testcomputername/#{username},user") group3 = stub 'group1' group3.expects(:Remove).with("WinNT://testcomputername/#{username},user") Puppet::Util::Windows::ADSI.expects(:sid_uri).with(sid).returns("WinNT://testcomputername/#{username},user").twice Puppet::Util::Windows::ADSI.expects(:connect).with('WinNT://./group1,group').returns group1 Puppet::Util::Windows::ADSI.expects(:connect).with('WinNT://./group3,group').returns group3 user.set_groups(groups_to_set, false) end end describe "if membership is specified as minimum" do it "should add the user to the specified groups without affecting its other memberships" do group1 = stub 'group1' group1.expects(:Add).with("WinNT://testcomputername/#{username},user") Puppet::Util::Windows::ADSI.expects(:sid_uri).with(sid).returns("WinNT://testcomputername/#{username},user") Puppet::Util::Windows::ADSI.expects(:connect).with('WinNT://./group1,group').returns group1 user.set_groups(groups_to_set, true) end end end end end describe Puppet::Util::Windows::ADSI::Group do let(:groupname) { 'testgroup' } describe "an instance" do let(:adsi_group) { stub 'group' } let(:group) { Puppet::Util::Windows::ADSI::Group.new(groupname, adsi_group) } let(:someone_sid){ stub(:account => 'someone', :domain => 'testcomputername')} - it "should be able to add a member (deprecated)" do - Puppet.expects(:deprecation_warning).with('Puppet::Util::Windows::ADSI::Group#add_members is deprecated; please use Puppet::Util::Windows::ADSI::Group#add_member_sids') - - Puppet::Util::Windows::SID.expects(:name_to_sid_object).with('someone').returns(someone_sid) - Puppet::Util::Windows::ADSI.expects(:sid_uri).with(someone_sid).returns("WinNT://testcomputername/someone,user") - - adsi_group.expects(:Add).with("WinNT://testcomputername/someone,user") - - group.add_member('someone') - end - - it "should raise when adding a member that can't resolve to a SID (deprecated)" do - expect { - group.add_member('foobar') - }.to raise_error(Puppet::Error, /Could not resolve username: foobar/) - end - - it "should be able to remove a member (deprecated)" do - Puppet.expects(:deprecation_warning).with('Puppet::Util::Windows::ADSI::Group#remove_members is deprecated; please use Puppet::Util::Windows::ADSI::Group#remove_member_sids') - - Puppet::Util::Windows::SID.expects(:name_to_sid_object).with('someone').returns(someone_sid) - Puppet::Util::Windows::ADSI.expects(:sid_uri).with(someone_sid).returns("WinNT://testcomputername/someone,user") - - adsi_group.expects(:Remove).with("WinNT://testcomputername/someone,user") - - group.remove_member('someone') - end - - it "should raise when removing a member that can't resolve to a SID (deprecated)" do - expect { - group.remove_member('foobar') - }.to raise_error(Puppet::Error, /Could not resolve username: foobar/) - end - describe "should be able to use SID objects" do let(:system) { Puppet::Util::Windows::SID.name_to_sid_object('SYSTEM') } + let(:invalid) { Puppet::Util::Windows::SID.name_to_sid_object('foobar') } it "to add a member" do adsi_group.expects(:Add).with("WinNT://S-1-5-18") group.add_member_sids(system) end + it "and raise when passed a non-SID object to add" do + expect{ group.add_member_sids(invalid)}.to raise_error(Puppet::Error, /Must use a valid SID object/) + end + it "to remove a member" do adsi_group.expects(:Remove).with("WinNT://S-1-5-18") group.remove_member_sids(system) end + + it "and raise when passed a non-SID object to remove" do + expect{ group.remove_member_sids(invalid)}.to raise_error(Puppet::Error, /Must use a valid SID object/) + end end it "should provide its groups as a list of names" do names = ['user1', 'user2'] users = names.map { |name| mock('user', :Name => name) } adsi_group.expects(:Members).returns(users) group.members.should =~ names end it "should be able to add a list of users to a group" do names = ['DOMAIN\user1', 'user2'] sids = [ stub(:account => 'user1', :domain => 'DOMAIN'), stub(:account => 'user2', :domain => 'testcomputername'), stub(:account => 'user3', :domain => 'DOMAIN2'), ] # use stubbed objectSid on member to return stubbed SID Puppet::Util::Windows::SID.expects(:octet_string_to_sid_object).with([0]).returns(sids[0]) Puppet::Util::Windows::SID.expects(:octet_string_to_sid_object).with([1]).returns(sids[1]) Puppet::Util::Windows::SID.expects(:name_to_sid_object).with('user2').returns(sids[1]) Puppet::Util::Windows::SID.expects(:name_to_sid_object).with('DOMAIN2\user3').returns(sids[2]) Puppet::Util::Windows::ADSI.expects(:sid_uri).with(sids[0]).returns("WinNT://DOMAIN/user1,user") Puppet::Util::Windows::ADSI.expects(:sid_uri).with(sids[2]).returns("WinNT://DOMAIN2/user3,user") members = names.each_with_index.map{|n,i| stub(:Name => n, :objectSID => [i])} adsi_group.expects(:Members).returns members adsi_group.expects(:Remove).with('WinNT://DOMAIN/user1,user') adsi_group.expects(:Add).with('WinNT://DOMAIN2/user3,user') group.set_members(['user2', 'DOMAIN2\user3']) end it "should raise an error when a username does not resolve to a SID" do expect { adsi_group.expects(:Members).returns [] group.set_members(['foobar']) }.to raise_error(Puppet::Error, /Could not resolve username: foobar/) end it "should generate the correct URI" do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) group.uri.should == "WinNT://./#{groupname},group" end end it "should generate the correct URI" do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) Puppet::Util::Windows::ADSI::Group.uri("people").should == "WinNT://./people,group" end it "should be able to create a group" do adsi_group = stub("adsi") connection.expects(:Create).with('group', groupname).returns(adsi_group) Puppet::Util::Windows::ADSI::User.expects(:exists?).with(groupname).returns(false) group = Puppet::Util::Windows::ADSI::Group.create(groupname) group.should be_a(Puppet::Util::Windows::ADSI::Group) group.native_group.should == adsi_group end it "should be able to confirm the existence of a group" do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) Puppet::Util::Windows::ADSI.expects(:connect).with("WinNT://./#{groupname},group").returns connection Puppet::Util::Windows::ADSI::Group.exists?(groupname).should be_true end it "should be able to confirm the existence of a group with a well-known SID" do service_group = Win32::Security::SID::Service # ensure that the underlying OS is queried here Puppet::Util::Windows::ADSI.unstub(:connect) Puppet::Util::Windows::ADSI::Group.exists?(service_group).should be_true end it "should return nil with an unknown SID" do bogus_sid = 'S-1-2-3-4' # ensure that the underlying OS is queried here Puppet::Util::Windows::ADSI.unstub(:connect) Puppet::Util::Windows::ADSI::Group.exists?(bogus_sid).should be_false end it "should be able to delete a group" do connection.expects(:Delete).with('group', groupname) Puppet::Util::Windows::ADSI::Group.delete(groupname) end it "should return an enumeration of IADsGroup wrapped objects" do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) name = 'Administrators' wmi_groups = [stub('WMI', :name => name)] Puppet::Util::Windows::ADSI.expects(:execquery).with('select name from win32_group where localaccount = "TRUE"').returns(wmi_groups) native_group = stub('IADsGroup') native_group.expects(:Members).returns([stub(:Name => 'Administrator')]) Puppet::Util::Windows::ADSI.expects(:connect).with("WinNT://./#{name},group").returns(native_group) groups = Puppet::Util::Windows::ADSI::Group.to_a groups.length.should == 1 groups[0].name.should == name groups[0].members.should == ['Administrator'] end end describe Puppet::Util::Windows::ADSI::UserProfile do it "should be able to delete a user profile" do connection.expects(:Delete).with("Win32_UserProfile.SID='S-A-B-C'") Puppet::Util::Windows::ADSI::UserProfile.delete('S-A-B-C') end it "should warn on 2003" do connection.expects(:Delete).raises(RuntimeError, "Delete (WIN32OLERuntimeError) OLE error code:80041010 in SWbemServicesEx Invalid class HRESULT error code:0x80020009 Exception occurred.") Puppet.expects(:warning).with("Cannot delete user profile for 'S-A-B-C' prior to Vista SP1") Puppet::Util::Windows::ADSI::UserProfile.delete('S-A-B-C') end end end diff --git a/spec/unit/util/windows/sid_spec.rb b/spec/unit/util/windows/sid_spec.rb index 2748f13c6..1e9f44070 100755 --- a/spec/unit/util/windows/sid_spec.rb +++ b/spec/unit/util/windows/sid_spec.rb @@ -1,166 +1,176 @@ #!/usr/bin/env ruby require 'spec_helper' describe "Puppet::Util::Windows::SID", :if => Puppet.features.microsoft_windows? do if Puppet.features.microsoft_windows? require 'puppet/util/windows' end let(:subject) { Puppet::Util::Windows::SID } let(:sid) { Win32::Security::SID::LocalSystem } let(:invalid_sid) { 'bogus' } let(:unknown_sid) { 'S-0-0-0' } let(:unknown_name) { 'chewbacca' } context "#octet_string_to_sid_object" do it "should properly convert an array of bytes for the local Administrator SID" do host = '.' username = 'Administrator' admin = WIN32OLE.connect("WinNT://#{host}/#{username},user") converted = subject.octet_string_to_sid_object(admin.objectSID) converted.should == Win32::Security::SID.new(username, host) converted.should be_an_instance_of Win32::Security::SID end it "should properly convert an array of bytes for a well-known SID" do bytes = [1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0] converted = subject.octet_string_to_sid_object(bytes) converted.should == Win32::Security::SID.new('SYSTEM') converted.should be_an_instance_of Win32::Security::SID end it "should raise an error for non-array input" do expect { subject.octet_string_to_sid_object(invalid_sid) }.to raise_error(Puppet::Error, /Octet string must be an array of bytes/) end it "should raise an error for an empty byte array" do expect { subject.octet_string_to_sid_object([]) }.to raise_error(Puppet::Error, /Octet string must be an array of bytes/) end it "should raise an error for a malformed byte array" do expect { invalid_octet = [1] subject.octet_string_to_sid_object(invalid_octet) }.to raise_error(SystemCallError, /No mapping between account names and security IDs was done./) end end context "#name_to_sid" do it "should return nil if the account does not exist" do subject.name_to_sid(unknown_name).should be_nil end it "should accept unqualified account name" do subject.name_to_sid('SYSTEM').should == sid end + it "should return a SID for a passed user or group name" do + subject.expects(:name_to_sid_object).with('testers').returns 'S-1-5-32-547' + subject.name_to_sid('testers').should == 'S-1-5-32-547' + end + + it "should return a SID for a passed fully-qualified user or group name" do + subject.expects(:name_to_sid_object).with('MACHINE\testers').returns 'S-1-5-32-547' + subject.name_to_sid('MACHINE\testers').should == 'S-1-5-32-547' + end + it "should be case-insensitive" do subject.name_to_sid('SYSTEM').should == subject.name_to_sid('system') end it "should be leading and trailing whitespace-insensitive" do subject.name_to_sid('SYSTEM').should == subject.name_to_sid(' SYSTEM ') end it "should accept domain qualified account names" do subject.name_to_sid('NT AUTHORITY\SYSTEM').should == sid end it "should be the identity function for any sid" do subject.name_to_sid(sid).should == sid end end context "#name_to_sid_object" do it "should return nil if the account does not exist" do subject.name_to_sid_object(unknown_name).should be_nil end it "should return a Win32::Security::SID instance for any valid sid" do subject.name_to_sid_object(sid).should be_an_instance_of(Win32::Security::SID) end it "should accept unqualified account name" do subject.name_to_sid_object('SYSTEM').to_s.should == sid end it "should be case-insensitive" do subject.name_to_sid_object('SYSTEM').should == subject.name_to_sid_object('system') end it "should be leading and trailing whitespace-insensitive" do subject.name_to_sid_object('SYSTEM').should == subject.name_to_sid_object(' SYSTEM ') end it "should accept domain qualified account names" do subject.name_to_sid_object('NT AUTHORITY\SYSTEM').to_s.should == sid end end context "#sid_to_name" do it "should return nil if given a sid for an account that doesn't exist" do subject.sid_to_name(unknown_sid).should be_nil end it "should accept a sid" do subject.sid_to_name(sid).should == "NT AUTHORITY\\SYSTEM" end end context "#sid_ptr_to_string" do it "should raise if given an invalid sid" do expect { subject.sid_ptr_to_string(nil) }.to raise_error(Puppet::Error, /Invalid SID/) end it "should yield a valid sid pointer" do string = nil subject.string_to_sid_ptr(sid) do |ptr| string = subject.sid_ptr_to_string(ptr) end string.should == sid end end context "#string_to_sid_ptr" do it "should yield sid_ptr" do ptr = nil subject.string_to_sid_ptr(sid) do |p| ptr = p end ptr.should_not be_nil end it "should raise on an invalid sid" do expect { subject.string_to_sid_ptr(invalid_sid) }.to raise_error(Puppet::Error, /Failed to convert string SID/) end end context "#valid_sid?" do it "should return true for a valid SID" do subject.valid_sid?(sid).should be_true end it "should return false for an invalid SID" do subject.valid_sid?(invalid_sid).should be_false end it "should raise if the conversion fails" do subject.expects(:string_to_sid_ptr).with(sid). raises(Puppet::Util::Windows::Error.new("Failed to convert string SID: #{sid}", Puppet::Util::Windows::Error::ERROR_ACCESS_DENIED)) expect { subject.string_to_sid_ptr(sid) {|ptr| } }.to raise_error(Puppet::Util::Windows::Error, /Failed to convert string SID: #{sid}/) end end end