diff --git a/lib/puppet/util/windows.rb b/lib/puppet/util/windows.rb index 612f72b78..af440552f 100644 --- a/lib/puppet/util/windows.rb +++ b/lib/puppet/util/windows.rb @@ -1,13 +1,16 @@ module Puppet::Util::Windows if Puppet::Util::Platform.windows? # these reference platform specific gems require 'puppet/util/windows/error' require 'puppet/util/windows/sid' require 'puppet/util/windows/security' require 'puppet/util/windows/user' require 'puppet/util/windows/process' require 'puppet/util/windows/file' require 'puppet/util/windows/root_certs' + require 'puppet/util/windows/access_control_entry' + require 'puppet/util/windows/access_control_list' + require 'puppet/util/windows/security_descriptor' end require 'puppet/util/windows/registry' end diff --git a/lib/puppet/util/windows/access_control_entry.rb b/lib/puppet/util/windows/access_control_entry.rb new file mode 100644 index 000000000..5cd052dae --- /dev/null +++ b/lib/puppet/util/windows/access_control_entry.rb @@ -0,0 +1,84 @@ +# Windows Access Control Entry +# +# Represents an access control entry, which grants or denies a subject, +# identified by a SID, rights to a securable object. +# +# @see http://msdn.microsoft.com/en-us/library/windows/desktop/aa374868(v=vs.85).aspx +# @api private +class Puppet::Util::Windows::AccessControlEntry + require 'puppet/util/windows/security' + include Puppet::Util::Windows::SID + + attr_accessor :sid + attr_reader :mask, :flags, :type + + OBJECT_INHERIT_ACE = 0x1 + CONTAINER_INHERIT_ACE = 0x2 + NO_PROPAGATE_INHERIT_ACE = 0x4 + INHERIT_ONLY_ACE = 0x8 + INHERITED_ACE = 0x10 + + ACCESS_ALLOWED_ACE_TYPE = 0x0 + ACCESS_DENIED_ACE_TYPE = 0x1 + + def initialize(sid, mask, flags = 0, type = ACCESS_ALLOWED_ACE_TYPE) + @sid = sid + @mask = mask + @flags = flags + @type = type + end + + # Returns true if this ACE is inherited from a parent. If false, + # then the ACE is set directly on the object to which it refers. + # + # @return [Boolean] true if the ACE is inherited + def inherited? + (@flags & INHERITED_ACE) == INHERITED_ACE + end + + # Returns true if this ACE only applies to children of the object. + # If false, it applies to the object. + # + # @return [Boolean] true if the ACE only applies to children and + # not the object itself. + def inherit_only? + (@flags & INHERIT_ONLY_ACE) == INHERIT_ONLY_ACE + end + + # Returns true if this ACE applies to child directories. + # + # @return [Boolean] true if the ACE applies to child direcories + def container_inherit? + (@flags & CONTAINER_INHERIT_ACE) == CONTAINER_INHERIT_ACE + end + + # Returns true if this ACE applies to child files. + # + # @return [Boolean] true if the ACE applies to child files. + def object_inherit? + (@flags & OBJECT_INHERIT_ACE) == OBJECT_INHERIT_ACE + end + + def inspect + inheritance = "" + inheritance << '(I)' if inherited? + inheritance << '(OI)' if object_inherit? + inheritance << '(CI)' if container_inherit? + inheritance << '(IO)' if inherit_only? + + left = "#{sid_to_name(sid)}:#{inheritance}" + left = left.ljust(45) + "#{left} 0x#{mask.to_s(16)}" + end + + # Returns true if this ACE is equal to +other+ + def ==(other) + self.class == other.class && + sid == other.sid && + mask == other.mask && + flags == other.flags && + type == other.type + end + + alias eql? == +end diff --git a/lib/puppet/util/windows/access_control_list.rb b/lib/puppet/util/windows/access_control_list.rb new file mode 100644 index 000000000..14a924cd9 --- /dev/null +++ b/lib/puppet/util/windows/access_control_list.rb @@ -0,0 +1,106 @@ +# Windows Access Control List +# +# Represents a list of access control entries (ACEs). +# +# @see http://msdn.microsoft.com/en-us/library/windows/desktop/aa374872(v=vs.85).aspx +# @api private +class Puppet::Util::Windows::AccessControlList + include Enumerable + + ACCESS_ALLOWED_ACE_TYPE = 0x0 + ACCESS_DENIED_ACE_TYPE = 0x1 + + # Construct an ACL. + # + # @param acl [Enumerable] A list of aces to copy from. + def initialize(acl = nil) + if acl + @aces = acl.map(&:dup) + else + @aces = [] + end + end + + # Enumerate each ACE in the list. + # + # @yieldparam ace [Hash] the ace + def each + @aces.each {|ace| yield ace} + end + + # Allow the +sid+ to access a resource with the specified access +mask+. + # + # @param sid [String] The SID that the ACE is granting access to + # @param mask [int] The access mask granted to the SID + # @param flags [int] The flags assigned to the ACE, e.g. +INHERIT_ONLY_ACE+ + def allow(sid, mask, flags = 0) + @aces << Puppet::Util::Windows::AccessControlEntry.new(sid, mask, flags, ACCESS_ALLOWED_ACE_TYPE) + end + + # Deny the +sid+ access to a resource with the specified access +mask+. + # + # @param sid [String] The SID that the ACE is denying access to + # @param mask [int] The access mask denied to the SID + # @param flags [int] The flags assigned to the ACE, e.g. +INHERIT_ONLY_ACE+ + def deny(sid, mask, flags = 0) + @aces << Puppet::Util::Windows::AccessControlEntry.new(sid, mask, flags, ACCESS_DENIED_ACE_TYPE) + end + + # Reassign all ACEs currently assigned to +old_sid+ to +new_sid+ instead. + # If an ACE is inherited or is not assigned to +old_sid+, then it will + # be copied as-is to the new ACL, preserving its order within the ACL. + # + # @param old_sid [String] The old SID, e.g. 'S-1-5-18' + # @param new_sid [String] The new SID + # @return [AccessControlList] The copied ACL. + def reassign!(old_sid, new_sid) + new_aces = [] + prepend_needed = false + aces_to_prepend = [] + + @aces.each do |ace| + new_ace = ace.dup + + if ace.sid == old_sid + if ace.inherited? + # create an explicit ACE granting or denying the + # new_sid the rights that the inherited ACE + # granted or denied the old_sid. We mask off all + # flags except those affecting inheritance of the + # ACE we're creating. + inherit_mask = Windows::Security::CONTAINER_INHERIT_ACE | + Windows::Security::OBJECT_INHERIT_ACE | + Windows::Security::INHERIT_ONLY_ACE + explicit_ace = Puppet::Util::Windows::AccessControlEntry.new(new_sid, ace.mask, ace.flags & inherit_mask, ace.type) + aces_to_prepend << explicit_ace + else + new_ace.sid = new_sid + + prepend_needed = old_sid == Win32::Security::SID::LocalSystem + end + end + new_aces << new_ace + end + + @aces = [] + + if prepend_needed + mask = Windows::Security::STANDARD_RIGHTS_ALL | Windows::Security::SPECIFIC_RIGHTS_ALL + ace = Puppet::Util::Windows::AccessControlEntry.new( + Win32::Security::SID::LocalSystem, + mask) + @aces << ace + end + + @aces.concat(aces_to_prepend) + @aces.concat(new_aces) + end + + def inspect + str = "" + @aces.each do |ace| + str << " #{ace.inspect}\n" + end + str + end +end diff --git a/lib/puppet/util/windows/security.rb b/lib/puppet/util/windows/security.rb index 9b70dedd6..de40a6a1e 100644 --- a/lib/puppet/util/windows/security.rb +++ b/lib/puppet/util/windows/security.rb @@ -1,656 +1,648 @@ # 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 '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 # 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) - old_sid = get_owner(path) + sd = get_security_descriptor(path) - change_sid(old_sid, owner_sid, OWNER_SECURITY_INFORMATION, 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_sid(OWNER_SECURITY_INFORMATION, 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) - old_sid = get_group(path) + sd = get_security_descriptor(path) - change_sid(old_sid, group_sid, GROUP_SECURITY_INFORMATION, 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_sid(GROUP_SECURITY_INFORMATION, 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 change_sid(old_sid, new_sid, info, path) - if old_sid != new_sid - mode = get_mode(path) - - string_to_sid_ptr(new_sid) do |psid| - with_privilege(SE_RESTORE_NAME) do - open_file(path, WRITE_OWNER) do |handle| - set_security_info(handle, info, psid) - end - end - end - - # rebuild dacl now that sid has changed - set_mode(mode, path) - end - end - - def get_sid(info, path) - with_privilege(SE_BACKUP_NAME) do - open_file(path, READ_CONTROL) do |handle| - get_security_info(handle, info) - end - end - 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_dacl_for_path(path) - with_privilege(SE_BACKUP_NAME) do - open_file(path, READ_CONTROL) do |handle| - get_dacl(handle) - end - end - end - def get_aces_for_path_by_sid(path, sid) - get_dacl_for_path(path).select { |ace| ace[:sid] == 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) - owner_sid = get_owner(path) - group_sid = get_group(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 - with_privilege(SE_BACKUP_NAME) do - open_file(path, READ_CONTROL) do |handle| - mode = S_ISYSTEM_MISSING - - get_dacl(handle).each do |ace| - case ace[:sid] - when owner_sid - MASK_TO_MODE.each_pair do |k,v| - if (ace[:mask] & k) == k - mode |= (v << 6) - end - end - when group_sid - 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) and (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 + mode = S_ISYSTEM_MISSING - if ace[:sid] == well_known_system_sid - mode &= ~S_ISYSTEM_MISSING - end + sd = get_security_descriptor(path) + sd.dacl.each do |ace| + next if ace.inherit_only? - # if owner and group the same, then user and group modes are the OR of both - if owner_sid == group_sid - mode |= ((mode & S_IRWXG) << 3) | ((mode & S_IRWXU) >> 3) - #puts "owner: #{group_sid}, 0x#{ace[:mask].to_s(16)}, #{mode.to_s(8)}" + 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 - #puts "get_mode: #{mode.to_s(8)}" - mode + # 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) - owner_sid = get_owner(path) - group_sid = get_group(path) + 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 ! [group_sid, owner_sid].include? well_known_system_sid + 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) and (mode & S_ISVTX) == 0 + 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) and (mode & S_ISVTX) == 0 + 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 = owner_sid == group_sid + 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 - set_acl(path, protected) do |acl| - #puts "ace: owner #{owner_sid}, mask 0x#{owner_allow.to_s(16)}" - add_access_allowed_ace(acl, owner_allow, owner_sid) - - unless isownergroup - #puts "ace: group #{group_sid}, mask 0x#{group_allow.to_s(16)}" - add_access_allowed_ace(acl, group_allow, group_sid) - end - - #puts "ace: other #{well_known_world_sid}, mask 0x#{other_allow.to_s(16)}" - add_access_allowed_ace(acl, other_allow, well_known_world_sid) - - #puts "ace: nobody #{well_known_nobody_sid}, mask 0x#{nobody_allow.to_s(16)}" - add_access_allowed_ace(acl, nobody_allow, well_known_nobody_sid) + 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) - # puts "ace: system #{well_known_system_sid}, mask 0x#{system_allow.to_s(16)}" - add_access_allowed_ace(acl, system_allow, well_known_system_sid) + # 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 - add_access_allowed_ace(acl, owner_allow, Win32::Security::SID::CreatorOwner, inherit) - add_access_allowed_ace(acl, group_allow, Win32::Security::SID::CreatorGroup, inherit) + # 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 - add_access_allowed_ace(acl, owner_allow & ~FILE_EXECUTE, Win32::Security::SID::CreatorOwner, inherit) - add_access_allowed_ace(acl, group_allow & ~FILE_EXECUTE, Win32::Security::SID::CreatorGroup, inherit) - end + 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 - # setting DACL requires both READ_CONTROL and WRITE_DACL access rights, - # and their respective privileges, SE_BACKUP_NAME and SE_RESTORE_NAME. - def set_acl(path, protected = true) - with_privilege(SE_BACKUP_NAME) do - with_privilege(SE_RESTORE_NAME) do - open_file(path, READ_CONTROL | WRITE_DAC) do |handle| - acl = 0.chr * 1024 # This can be increased later as needed + def add_access_allowed_ace(acl, mask, sid, inherit = nil) + inherit ||= NO_INHERITANCE - 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) - - yield acl - - # protected means the object does not inherit aces from its parent - info = DACL_SECURITY_INFORMATION - info |= protected ? PROTECTED_DACL_SECURITY_INFORMATION : UNPROTECTED_DACL_SECURITY_INFORMATION - - # set the DACL - set_security_info(handle, info, acl) - end - end - end - end - - def add_access_allowed_ace(acl, mask, sid, inherit = NO_INHERITANCE) string_to_sid_ptr(sid) do |sid_ptr| raise Puppet::Util::Windows::Error.new("Invalid SID") unless IsValidSid(sid_ptr) unless AddAccessAllowedAceEx(acl, ACL_REVISION, inherit, mask, sid_ptr) raise Puppet::Util::Windows::Error.new("Failed to add access control entry") end end end def add_access_denied_ace(acl, mask, sid) string_to_sid_ptr(sid) do |sid_ptr| raise Puppet::Util::Windows::Error.new("Invalid SID") unless IsValidSid(sid_ptr) unless AddAccessDeniedAce(acl, ACL_REVISION, mask, sid_ptr) raise Puppet::Util::Windows::Error.new("Failed to add access control entry") end end end - def get_dacl(handle) - get_dacl_ptr(handle) do |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. + 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 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 = [] - - # 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') - - # skip aces that only serve to propagate inheritance - next if (ace_flags & INHERIT_ONLY_ACE).nonzero? - - case ace_type - when ACCESS_ALLOWED_ACE_TYPE - sid_ptr = ace_ptr.unpack('L')[0] + 8 # address of ace_ptr->SidStart - raise Puppet::Util::Windows::Error.new("Failed to read DACL, invalid SID") unless IsValidSid(sid_ptr) - sid = sid_ptr_to_string(sid_ptr) - dacl << {:sid => sid, :type => ace_type, :mask => mask, :flags => ace_flags} - else - Puppet.warning "Unsupported access control entry type: 0x#{ace_type.to_s(16)}" - end + # BYTE C AceType + # BYTE C AceFlags + # WORD S AceSize + # DWORD L ACCESS_MASK + # DWORD L Sid + # .. ... + # DWORD L Sid + + ace_buf = 0.chr * 8 + memcpy(ace_buf, ace_ptr.unpack('L')[0], ace_buf.size) + + ace_type, ace_flags, size, mask = ace_buf.unpack('CCSL') + + case ace_type + when ACCESS_ALLOWED_ACE_TYPE + sid_ptr = ace_ptr.unpack('L')[0] + 8 # address of ace_ptr->SidStart + raise Puppet::Util::Windows::Error.new("Failed to read DACL, invalid SID") unless IsValidSid(sid_ptr) + sid = sid_ptr_to_string(sid_ptr) + dacl.allow(sid, mask, ace_flags) + when ACCESS_DENIED_ACE_TYPE + sid_ptr = ace_ptr.unpack('L')[0] + 8 # address of ace_ptr->SidStart + raise Puppet::Util::Windows::Error.new("Failed to read DACL, invalid SID") unless IsValidSid(sid_ptr) + sid = sid_ptr_to_string(sid_ptr) + dacl.deny(sid, mask, ace_flags) + else + Puppet.warning "Unsupported access control entry type: 0x#{ace_type.to_s(16)}" end - - dacl - end - end - - def get_dacl_ptr(handle) - dacl = [0].pack('L') - sd = [0].pack('L') - - rv = GetSecurityInfo( - handle, - SE_FILE_OBJECT, - DACL_SECURITY_INFORMATION, - nil, - nil, - dacl, #dacl - nil, #sacl - sd) #sec desc - raise Puppet::Util::Windows::Error.new("Failed to get DACL") unless rv == ERROR_SUCCESS - begin - yield dacl.unpack('L')[0] - ensure - LocalFree(sd.unpack('L')[0]) end - end - - # Set the security info on the specified handle. - def set_security_info(handle, info, ptr) - rv = SetSecurityInfo( - handle, - SE_FILE_OBJECT, - info, - (info & OWNER_SECURITY_INFORMATION) == OWNER_SECURITY_INFORMATION ? ptr : nil, - (info & GROUP_SECURITY_INFORMATION) == GROUP_SECURITY_INFORMATION ? ptr : nil, - (info & DACL_SECURITY_INFORMATION) == DACL_SECURITY_INFORMATION ? ptr : nil, - nil) - raise Puppet::Util::Windows::Error.new("Failed to set security information") unless rv == ERROR_SUCCESS - end - # Get the SID string, e.g. "S-1-5-32-544", for the specified handle - # and type of information (owner, group). - def get_security_info(handle, info) - sid = [0].pack('L') - sd = [0].pack('L') - - rv = GetSecurityInfo( - handle, - SE_FILE_OBJECT, - info, # security info - info == OWNER_SECURITY_INFORMATION ? sid : nil, - info == GROUP_SECURITY_INFORMATION ? sid : nil, - nil, #dacl - nil, #sacl - sd) #sec desc - raise Puppet::Util::Windows::Error.new("Failed to get security information") unless rv == ERROR_SUCCESS - - begin - return sid_ptr_to_string(sid.unpack('L')[0]) - ensure - LocalFree(sd.unpack('L')[0]) - end + dacl end # Open an existing file with the specified access mode, and execute a # block with the opened file HANDLE. def open_file(path, access) handle = CreateFile( path, access, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, # security_attributes OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, 0) # template raise Puppet::Util::Windows::Error.new("Failed to open '#{path}'") if handle == INVALID_HANDLE_VALUE begin yield handle ensure CloseHandle(handle) end end # Execute a block with the specified privilege enabled def with_privilege(privilege) set_privilege(privilege, true) yield ensure set_privilege(privilege, false) end # Enable or disable a privilege. Note this doesn't add any privileges the # user doesn't already has, it just enables privileges that are disabled. def set_privilege(privilege, enable) return unless Puppet.features.root? with_process_token(TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY) do |token| tmpLuid = 0.chr * 8 # Get the LUID for specified privilege. unless LookupPrivilegeValue("", privilege, tmpLuid) raise Puppet::Util::Windows::Error.new("Failed to lookup privilege") end # DWORD + [LUID + DWORD] tkp = [1].pack('L') + tmpLuid + [enable ? SE_PRIVILEGE_ENABLED : 0].pack('L') unless AdjustTokenPrivileges(token, 0, tkp, tkp.length , nil, nil) raise Puppet::Util::Windows::Error.new("Failed to adjust process privileges") end end end # Execute a block with the current process token def with_process_token(access) token = 0.chr * 4 unless OpenProcessToken(GetCurrentProcess(), access, token) raise Puppet::Util::Windows::Error.new("Failed to open process token") end begin token = token.unpack('L')[0] yield token ensure CloseHandle(token) end end + + def get_security_descriptor(path) + sd = nil + + with_privilege(SE_BACKUP_NAME) do + open_file(path, READ_CONTROL) do |handle| + owner_sid = [0].pack('L') + group_sid = [0].pack('L') + dacl = [0].pack('L') + ppsd = [0].pack('L') + + rv = GetSecurityInfo( + handle, + SE_FILE_OBJECT, + OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, + owner_sid, + group_sid, + dacl, + nil, #sacl + ppsd) #sec desc + raise Puppet::Util::Windows::Error.new("Failed to get security information") unless rv == ERROR_SUCCESS + + begin + owner = sid_ptr_to_string(owner_sid.unpack('L')[0]) + group = sid_ptr_to_string(group_sid.unpack('L')[0]) + + control = FFI::MemoryPointer.new(:uint16, 1) + revision = FFI::MemoryPointer.new(:uint32, 1) + ffsd = FFI::Pointer.new(ppsd.unpack('L')[0]) + + if ! API.get_security_descriptor_control(ffsd, control, revision) + raise Puppet::Util::Windows::Error.new("Failed to get security descriptor control") + end + + protect = (control.read_uint16 & SE_DACL_PROTECTED) == SE_DACL_PROTECTED + + dacl = parse_dacl(dacl.unpack('L')[0]) + sd = Puppet::Util::Windows::SecurityDescriptor.new(owner, group, dacl, protect) + ensure + LocalFree(ppsd.unpack('L')[0]) + end + end + end + + sd + end + + # setting DACL requires both READ_CONTROL and WRITE_DACL access rights, + # and their respective privileges, SE_BACKUP_NAME and SE_RESTORE_NAME. + def set_security_descriptor(path, sd) + # REMIND: FFI + acl = 0.chr * 1024 # This can be increased later as neede + unless InitializeAcl(acl, acl.size, ACL_REVISION) + raise Puppet::Util::Windows::Error.new("Failed to initialize ACL") + end + + raise Puppet::Util::Windows::Error.new("Invalid DACL") unless IsValidAcl(acl) + + with_privilege(SE_BACKUP_NAME) do + with_privilege(SE_RESTORE_NAME) do + open_file(path, READ_CONTROL | WRITE_DAC | WRITE_OWNER) do |handle| + string_to_sid_ptr(sd.owner) do |ownersid| + string_to_sid_ptr(sd.group) do |groupsid| + sd.dacl.each do |ace| + case ace.type + when ACCESS_ALLOWED_ACE_TYPE + #puts "ace: allow, sid #{sid_to_name(ace.sid)}, mask 0x#{ace.mask.to_s(16)}" + add_access_allowed_ace(acl, ace.mask, ace.sid, ace.flags) + when ACCESS_DENIED_ACE_TYPE + #puts "ace: deny, sid #{sid_to_name(ace.sid)}, mask 0x#{ace.mask.to_s(16)}" + add_access_denied_ace(acl, ace.mask, ace.sid) + else + raise "We should never get here" + # TODO: this should have been a warning in an earlier commit + end + end + + # protected means the object does not inherit aces from its parent + flags = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION + flags |= sd.protect ? PROTECTED_DACL_SECURITY_INFORMATION : UNPROTECTED_DACL_SECURITY_INFORMATION + + rv = SetSecurityInfo(handle, + SE_FILE_OBJECT, + flags, + ownersid, + groupsid, + acl, + nil) + raise Puppet::Util::Windows::Error.new("Failed to set security information") unless rv == ERROR_SUCCESS + end + end + end + end + end + end + + module API + extend FFI::Library + ffi_lib 'kernel32' + ffi_convention :stdcall + + # typedef WORD SECURITY_DESCRIPTOR_CONTROL, *PSECURITY_DESCRIPTOR_CONTROL; + # BOOL WINAPI GetSecurityDescriptorControl( + # _In_ PSECURITY_DESCRIPTOR pSecurityDescriptor, + # _Out_ PSECURITY_DESCRIPTOR_CONTROL pControl, + # _Out_ LPDWORD lpdwRevision + # ); + ffi_lib :advapi32 + attach_function :get_security_descriptor_control, :GetSecurityDescriptorControl, [:pointer, :pointer, :pointer], :bool + end end diff --git a/lib/puppet/util/windows/security_descriptor.rb b/lib/puppet/util/windows/security_descriptor.rb new file mode 100644 index 000000000..9c95cee6d --- /dev/null +++ b/lib/puppet/util/windows/security_descriptor.rb @@ -0,0 +1,62 @@ +# Windows Security Descriptor +# +# Represents a security descriptor that can be applied to any Windows securable +# object, e.g. file, registry key, service, etc. It consists of an owner, group, +# flags, DACL, and SACL. The SACL is not currently supported, though it has the +# same layout as a DACL. +# +# @see http://msdn.microsoft.com/en-us/library/windows/desktop/aa379563(v=vs.85).aspx +# @api private +class Puppet::Util::Windows::SecurityDescriptor + require 'puppet/util/windows/security' + include Puppet::Util::Windows::SID + + attr_reader :owner, :group, :dacl + attr_accessor :protect + + # Construct a security descriptor + # + # @param owner [String] The SID of the owner, e.g. 'S-1-5-18' + # @param group [String] The SID of the group + # @param dacl [AccessControlList] The ACL specifying the rights granted to + # each user for accessing the object that the security descriptor refers to. + # @param protect [Boolean] If true, then inheritable access control + # entries will be blocked, and not applied to the object. + def initialize(owner, group, dacl, protect = false) + @owner = owner + @group = group + @dacl = dacl + @protect = protect + end + + # Set the owner. Non-inherited access control entries assigned to the + # current owner will be assigned to the new owner. + # + # @param new_owner [String] The SID of the new owner, e.g. 'S-1-5-18' + def owner=(new_owner) + if @owner != new_owner + @dacl.reassign!(@owner, new_owner) + @owner = new_owner + end + end + + # Set the group. Non-inherited access control entries assigned to the + # current group will be assigned to the new group. + # + # @param new_group [String] The SID of the new group, e.g. 'S-1-0-0' + def group=(new_group) + if @group != new_group + @dacl.reassign!(@group, new_group) + @group = new_group + end + end + + def inspect + str = sid_to_name(owner) + str << "\n" + str << sid_to_name(group) + str << "\n" + str << @dacl.inspect + str + end +end diff --git a/spec/integration/type/file_spec.rb b/spec/integration/type/file_spec.rb index 9f7a4453a..7cd39aace 100755 --- a/spec/integration/type/file_spec.rb +++ b/spec/integration/type/file_spec.rb @@ -1,1263 +1,1316 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet_spec/files' if Puppet.features.microsoft_windows? require 'puppet/util/windows' class WindowsSecurity extend Puppet::Util::Windows::Security end end describe Puppet::Type.type(:file) do include PuppetSpec::Files let(:catalog) { Puppet::Resource::Catalog.new } let(:path) do # we create a directory first so backups of :path that are stored in # the same directory will also be removed after the tests parent = tmpdir('file_spec') File.join(parent, 'file_testing') end let(:dir) do # we create a directory first so backups of :path that are stored in # the same directory will also be removed after the tests parent = tmpdir('file_spec') File.join(parent, 'dir_testing') end if Puppet.features.posix? def set_mode(mode, file) File.chmod(mode, file) end def get_mode(file) Puppet::FileSystem::File.new(file).lstat.mode end def get_owner(file) Puppet::FileSystem::File.new(file).lstat.uid end def get_group(file) Puppet::FileSystem::File.new(file).lstat.gid end else class SecurityHelper extend Puppet::Util::Windows::Security end def set_mode(mode, file) SecurityHelper.set_mode(mode, file) end def get_mode(file) SecurityHelper.get_mode(file) end def get_owner(file) SecurityHelper.get_owner(file) end def get_group(file) SecurityHelper.get_group(file) end def get_aces_for_path_by_sid(path, sid) SecurityHelper.get_aces_for_path_by_sid(path, sid) end end before do # stub this to not try to create state.yaml Puppet::Util::Storage.stubs(:store) end it "should not attempt to manage files that do not exist if no means of creating the file is specified" do source = tmpfile('source') catalog.add_resource described_class.new :path => source, :mode => 0755 status = catalog.apply.report.resource_statuses["File[#{source}]"] status.should_not be_failed status.should_not be_changed Puppet::FileSystem::File.exist?(source).should be_false end describe "when ensure is absent" do it "should remove the file if present" do FileUtils.touch(path) catalog.add_resource(described_class.new(:path => path, :ensure => :absent, :backup => :false)) report = catalog.apply.report report.resource_statuses["File[#{path}]"].should_not be_failed Puppet::FileSystem::File.exist?(path).should be_false end it "should do nothing if file is not present" do catalog.add_resource(described_class.new(:path => path, :ensure => :absent, :backup => :false)) report = catalog.apply.report report.resource_statuses["File[#{path}]"].should_not be_failed Puppet::FileSystem::File.exist?(path).should be_false end # issue #14599 it "should not fail if parts of path aren't directories" do FileUtils.touch(path) catalog.add_resource(described_class.new(:path => File.join(path,'no_such_file'), :ensure => :absent, :backup => :false)) report = catalog.apply.report report.resource_statuses["File[#{File.join(path,'no_such_file')}]"].should_not be_failed end end describe "when setting permissions" do it "should set the owner" do target = tmpfile_with_contents('target', '') owner = get_owner(target) catalog.add_resource described_class.new( :name => target, :owner => owner ) catalog.apply get_owner(target).should == owner end it "should set the group" do target = tmpfile_with_contents('target', '') group = get_group(target) catalog.add_resource described_class.new( :name => target, :group => group ) catalog.apply get_group(target).should == group end describe "when setting mode" do describe "for directories" do let(:target) { tmpdir('dir_mode') } it "should set executable bits for newly created directories" do catalog.add_resource described_class.new(:path => target, :ensure => :directory, :mode => 0600) catalog.apply (get_mode(target) & 07777).should == 0700 end it "should set executable bits for existing readable directories" do set_mode(0600, target) catalog.add_resource described_class.new(:path => target, :ensure => :directory, :mode => 0644) catalog.apply (get_mode(target) & 07777).should == 0755 end it "should not set executable bits for unreadable directories" do begin catalog.add_resource described_class.new(:path => target, :ensure => :directory, :mode => 0300) catalog.apply (get_mode(target) & 07777).should == 0300 ensure # so we can cleanup set_mode(0700, target) end end it "should set user, group, and other executable bits" do catalog.add_resource described_class.new(:path => target, :ensure => :directory, :mode => 0664) catalog.apply (get_mode(target) & 07777).should == 0775 end it "should set executable bits when overwriting a non-executable file" do target_path = tmpfile_with_contents('executable', '') set_mode(0444, target_path) catalog.add_resource described_class.new(:path => target_path, :ensure => :directory, :mode => 0666, :backup => false) catalog.apply (get_mode(target_path) & 07777).should == 0777 File.should be_directory(target_path) end end describe "for files" do it "should not set executable bits" do catalog.add_resource described_class.new(:path => path, :ensure => :file, :mode => 0666) catalog.apply (get_mode(path) & 07777).should == 0666 end it "should not set executable bits when replacing an executable directory (#10365)" do pending("bug #10365") FileUtils.mkdir(path) set_mode(0777, path) catalog.add_resource described_class.new(:path => path, :ensure => :file, :mode => 0666, :backup => false, :force => true) catalog.apply (get_mode(path) & 07777).should == 0666 end end describe "for links", :if => described_class.defaultprovider.feature?(:manages_symlinks) do let(:link) { tmpfile('link_mode') } describe "when managing links" do let(:link_target) { tmpfile('target') } before :each do FileUtils.touch(link_target) File.chmod(0444, link_target) Puppet::FileSystem::File.new(link_target).symlink(link) end it "should not set the executable bit on the link nor the target" do catalog.add_resource described_class.new(:path => link, :ensure => :link, :mode => 0666, :target => link_target, :links => :manage) catalog.apply (Puppet::FileSystem::File.new(link).stat.mode & 07777) == 0666 (Puppet::FileSystem::File.new(link_target).lstat.mode & 07777) == 0444 end it "should ignore dangling symlinks (#6856)" do File.delete(link_target) catalog.add_resource described_class.new(:path => link, :ensure => :link, :mode => 0666, :target => link_target, :links => :manage) catalog.apply Puppet::FileSystem::File.exist?(link).should be_false end it "should create a link to the target if ensure is omitted" do FileUtils.touch(link_target) catalog.add_resource described_class.new(:path => link, :target => link_target) catalog.apply Puppet::FileSystem::File.exist?(link).should be_true Puppet::FileSystem::File.new(link).lstat.ftype.should == 'link' Puppet::FileSystem::File.new(link).readlink().should == link_target end end describe "when following links" do it "should ignore dangling symlinks (#6856)" do target = tmpfile('dangling') FileUtils.touch(target) Puppet::FileSystem::File.new(target).symlink(link) File.delete(target) catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow) catalog.apply end describe "to a directory" do let(:link_target) { tmpdir('dir_target') } before :each do File.chmod(0600, link_target) Puppet::FileSystem::File.new(link_target).symlink(link) end after :each do File.chmod(0750, link_target) end describe "that is readable" do it "should set the executable bits when creating the destination (#10315)" do catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0666, :links => :follow) catalog.apply File.should be_directory(path) (get_mode(path) & 07777).should == 0777 end it "should set the executable bits when overwriting the destination (#10315)" do FileUtils.touch(path) catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0666, :links => :follow, :backup => false) catalog.apply File.should be_directory(path) (get_mode(path) & 07777).should == 0777 end end describe "that is not readable" do before :each do set_mode(0300, link_target) end # so we can cleanup after :each do set_mode(0700, link_target) end it "should set executable bits when creating the destination (#10315)" do catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0666, :links => :follow) catalog.apply File.should be_directory(path) (get_mode(path) & 07777).should == 0777 end it "should set executable bits when overwriting the destination" do FileUtils.touch(path) catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0666, :links => :follow, :backup => false) catalog.apply File.should be_directory(path) (get_mode(path) & 07777).should == 0777 end end end describe "to a file" do let(:link_target) { tmpfile('file_target') } before :each do FileUtils.touch(link_target) Puppet::FileSystem::File.new(link_target).symlink(link) end it "should create the file, not a symlink (#2817, #10315)" do catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow) catalog.apply File.should be_file(path) (get_mode(path) & 07777).should == 0600 end it "should overwrite the file" do FileUtils.touch(path) catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow) catalog.apply File.should be_file(path) (get_mode(path) & 07777).should == 0600 end end describe "to a link to a directory" do let(:real_target) { tmpdir('real_target') } let(:target) { tmpfile('target') } before :each do File.chmod(0666, real_target) # link -> target -> real_target Puppet::FileSystem::File.new(real_target).symlink(target) Puppet::FileSystem::File.new(target).symlink(link) end after :each do File.chmod(0750, real_target) end describe "when following all links" do it "should create the destination and apply executable bits (#10315)" do catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow) catalog.apply File.should be_directory(path) (get_mode(path) & 07777).should == 0700 end it "should overwrite the destination and apply executable bits" do FileUtils.mkdir(path) catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow) catalog.apply File.should be_directory(path) (get_mode(path) & 0111).should == 0100 end end end end end end end describe "when writing files" do it "should backup files to a filebucket when one is configured" do filebucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket" file = described_class.new :path => path, :backup => "mybucket", :content => "foo" catalog.add_resource file catalog.add_resource filebucket File.open(file[:path], "w") { |f| f.write("bar") } md5 = Digest::MD5.hexdigest("bar") catalog.apply filebucket.bucket.getfile(md5).should == "bar" end it "should backup files in the local directory when a backup string is provided" do file = described_class.new :path => path, :backup => ".bak", :content => "foo" catalog.add_resource file File.open(file[:path], "w") { |f| f.puts "bar" } catalog.apply backup = file[:path] + ".bak" Puppet::FileSystem::File.exist?(backup).should be_true File.read(backup).should == "bar\n" end it "should fail if no backup can be performed" do dir = tmpdir("backups") file = described_class.new :path => File.join(dir, "testfile"), :backup => ".bak", :content => "foo" catalog.add_resource file File.open(file[:path], 'w') { |f| f.puts "bar" } # Create a directory where the backup should be so that writing to it fails Dir.mkdir(File.join(dir, "testfile.bak")) Puppet::Util::Log.stubs(:newmessage) catalog.apply File.read(file[:path]).should == "bar\n" end it "should not backup symlinks", :if => described_class.defaultprovider.feature?(:manages_symlinks) do link = tmpfile("link") dest1 = tmpfile("dest1") dest2 = tmpfile("dest2") bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket" file = described_class.new :path => link, :target => dest2, :ensure => :link, :backup => "mybucket" catalog.add_resource file catalog.add_resource bucket File.open(dest1, "w") { |f| f.puts "whatever" } Puppet::FileSystem::File.new(dest1).symlink(link) md5 = Digest::MD5.hexdigest(File.read(file[:path])) catalog.apply Puppet::FileSystem::File.new(link).readlink().should == dest2 Puppet::FileSystem::File.exist?(bucket[:path]).should be_false end it "should backup directories to the local filesystem by copying the whole directory" do file = described_class.new :path => path, :backup => ".bak", :content => "foo", :force => true catalog.add_resource file Dir.mkdir(path) otherfile = File.join(path, "foo") File.open(otherfile, "w") { |f| f.print "yay" } catalog.apply backup = "#{path}.bak" FileTest.should be_directory(backup) File.read(File.join(backup, "foo")).should == "yay" end it "should backup directories to filebuckets by backing up each file separately" do bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket" file = described_class.new :path => tmpfile("bucket_backs"), :backup => "mybucket", :content => "foo", :force => true catalog.add_resource file catalog.add_resource bucket Dir.mkdir(file[:path]) foofile = File.join(file[:path], "foo") barfile = File.join(file[:path], "bar") File.open(foofile, "w") { |f| f.print "fooyay" } File.open(barfile, "w") { |f| f.print "baryay" } foomd5 = Digest::MD5.hexdigest(File.read(foofile)) barmd5 = Digest::MD5.hexdigest(File.read(barfile)) catalog.apply bucket.bucket.getfile(foomd5).should == "fooyay" bucket.bucket.getfile(barmd5).should == "baryay" end it "should propagate failures encountered when renaming the temporary file" do file = described_class.new :path => path, :content => "foo" file.stubs(:perform_backup).returns(true) catalog.add_resource file File.open(path, "w") { |f| f.print "bar" } File.expects(:rename).raises ArgumentError expect { file.write(:content) }.to raise_error(Puppet::Error, /Could not rename temporary file/) File.read(path).should == "bar" end end describe "when recursing" do def build_path(dir) Dir.mkdir(dir) File.chmod(0750, dir) @dirs = [dir] @files = [] %w{one two}.each do |subdir| fdir = File.join(dir, subdir) Dir.mkdir(fdir) File.chmod(0750, fdir) @dirs << fdir %w{three}.each do |file| ffile = File.join(fdir, file) @files << ffile File.open(ffile, "w") { |f| f.puts "test #{file}" } File.chmod(0640, ffile) end end end it "should be able to recurse over a nonexistent file" do @file = described_class.new( :name => path, :mode => 0644, :recurse => true, :backup => false ) catalog.add_resource @file lambda { @file.eval_generate }.should_not raise_error end it "should be able to recursively set properties on existing files" do path = tmpfile("file_integration_tests") build_path(path) file = described_class.new( :name => path, :mode => 0644, :recurse => true, :backup => false ) catalog.add_resource file catalog.apply @dirs.should_not be_empty @dirs.each do |path| (get_mode(path) & 007777).should == 0755 end @files.should_not be_empty @files.each do |path| (get_mode(path) & 007777).should == 0644 end end it "should be able to recursively make links to other files", :if => described_class.defaultprovider.feature?(:manages_symlinks) do source = tmpfile("file_link_integration_source") build_path(source) dest = tmpfile("file_link_integration_dest") @file = described_class.new(:name => dest, :target => source, :recurse => true, :ensure => :link, :backup => false) catalog.add_resource @file catalog.apply @dirs.each do |path| link_path = path.sub(source, dest) Puppet::FileSystem::File.new(link_path).lstat.should be_directory end @files.each do |path| link_path = path.sub(source, dest) Puppet::FileSystem::File.new(link_path).lstat.ftype.should == "link" end end it "should be able to recursively copy files" do source = tmpfile("file_source_integration_source") build_path(source) dest = tmpfile("file_source_integration_dest") @file = described_class.new(:name => dest, :source => source, :recurse => true, :backup => false) catalog.add_resource @file catalog.apply @dirs.each do |path| newpath = path.sub(source, dest) Puppet::FileSystem::File.new(newpath).lstat.should be_directory end @files.each do |path| newpath = path.sub(source, dest) Puppet::FileSystem::File.new(newpath).lstat.ftype.should == "file" end end it "should not recursively manage files managed by a more specific explicit file" do dir = tmpfile("recursion_vs_explicit_1") subdir = File.join(dir, "subdir") file = File.join(subdir, "file") FileUtils.mkdir_p(subdir) File.open(file, "w") { |f| f.puts "" } base = described_class.new(:name => dir, :recurse => true, :backup => false, :mode => "755") sub = described_class.new(:name => subdir, :recurse => true, :backup => false, :mode => "644") catalog.add_resource base catalog.add_resource sub catalog.apply (get_mode(file) & 007777).should == 0644 end it "should recursively manage files even if there is an explicit file whose name is a prefix of the managed file" do managed = File.join(path, "file") generated = File.join(path, "file_with_a_name_starting_with_the_word_file") managed_mode = 0700 FileUtils.mkdir_p(path) FileUtils.touch(managed) FileUtils.touch(generated) catalog.add_resource described_class.new(:name => path, :recurse => true, :backup => false, :mode => managed_mode) catalog.add_resource described_class.new(:name => managed, :recurse => true, :backup => false, :mode => "644") catalog.apply (get_mode(generated) & 007777).should == managed_mode end describe "when recursing remote directories" do describe "when sourceselect first" do describe "for a directory" do it "should recursively copy the first directory that exists" do one = File.expand_path('thisdoesnotexist') two = tmpdir('two') FileUtils.mkdir_p(File.join(two, 'three')) FileUtils.touch(File.join(two, 'three', 'four')) catalog.add_resource Puppet::Type.newfile( :path => path, :ensure => :directory, :backup => false, :recurse => true, :sourceselect => :first, :source => [one, two] ) catalog.apply File.should be_directory(path) Puppet::FileSystem::File.exist?(File.join(path, 'one')).should be_false Puppet::FileSystem::File.exist?(File.join(path, 'three', 'four')).should be_true end it "should recursively copy an empty directory" do one = File.expand_path('thisdoesnotexist') two = tmpdir('two') three = tmpdir('three') file_in_dir_with_contents(three, 'a', '') catalog.add_resource Puppet::Type.newfile( :path => path, :ensure => :directory, :backup => false, :recurse => true, :sourceselect => :first, :source => [one, two, three] ) catalog.apply File.should be_directory(path) Puppet::FileSystem::File.exist?(File.join(path, 'a')).should be_false end it "should only recurse one level" do one = tmpdir('one') FileUtils.mkdir_p(File.join(one, 'a', 'b')) FileUtils.touch(File.join(one, 'a', 'b', 'c')) two = tmpdir('two') FileUtils.mkdir_p(File.join(two, 'z')) FileUtils.touch(File.join(two, 'z', 'y')) catalog.add_resource Puppet::Type.newfile( :path => path, :ensure => :directory, :backup => false, :recurse => true, :recurselimit => 1, :sourceselect => :first, :source => [one, two] ) catalog.apply Puppet::FileSystem::File.exist?(File.join(path, 'a')).should be_true Puppet::FileSystem::File.exist?(File.join(path, 'a', 'b')).should be_false Puppet::FileSystem::File.exist?(File.join(path, 'z')).should be_false end end describe "for a file" do it "should copy the first file that exists" do one = File.expand_path('thisdoesnotexist') two = tmpfile_with_contents('two', 'yay') three = tmpfile_with_contents('three', 'no') catalog.add_resource Puppet::Type.newfile( :path => path, :ensure => :file, :backup => false, :sourceselect => :first, :source => [one, two, three] ) catalog.apply File.read(path).should == 'yay' end it "should copy an empty file" do one = File.expand_path('thisdoesnotexist') two = tmpfile_with_contents('two', '') three = tmpfile_with_contents('three', 'no') catalog.add_resource Puppet::Type.newfile( :path => path, :ensure => :file, :backup => false, :sourceselect => :first, :source => [one, two, three] ) catalog.apply File.read(path).should == '' end end end describe "when sourceselect all" do describe "for a directory" do it "should recursively copy all sources from the first valid source" do dest = tmpdir('dest') one = tmpdir('one') two = tmpdir('two') three = tmpdir('three') four = tmpdir('four') file_in_dir_with_contents(one, 'a', one) file_in_dir_with_contents(two, 'a', two) file_in_dir_with_contents(two, 'b', two) file_in_dir_with_contents(three, 'a', three) file_in_dir_with_contents(three, 'c', three) obj = Puppet::Type.newfile( :path => dest, :ensure => :directory, :backup => false, :recurse => true, :sourceselect => :all, :source => [one, two, three, four] ) catalog.add_resource obj catalog.apply File.read(File.join(dest, 'a')).should == one File.read(File.join(dest, 'b')).should == two File.read(File.join(dest, 'c')).should == three end it "should only recurse one level from each valid source" do one = tmpdir('one') FileUtils.mkdir_p(File.join(one, 'a', 'b')) FileUtils.touch(File.join(one, 'a', 'b', 'c')) two = tmpdir('two') FileUtils.mkdir_p(File.join(two, 'z')) FileUtils.touch(File.join(two, 'z', 'y')) obj = Puppet::Type.newfile( :path => path, :ensure => :directory, :backup => false, :recurse => true, :recurselimit => 1, :sourceselect => :all, :source => [one, two] ) catalog.add_resource obj catalog.apply Puppet::FileSystem::File.exist?(File.join(path, 'a')).should be_true Puppet::FileSystem::File.exist?(File.join(path, 'a', 'b')).should be_false Puppet::FileSystem::File.exist?(File.join(path, 'z')).should be_true Puppet::FileSystem::File.exist?(File.join(path, 'z', 'y')).should be_false end end end end end describe "when generating resources" do before do source = tmpdir("generating_in_catalog_source") s1 = file_in_dir_with_contents(source, "one", "uno") s2 = file_in_dir_with_contents(source, "two", "dos") @file = described_class.new( :name => path, :source => source, :recurse => true, :backup => false ) catalog.add_resource @file end it "should add each generated resource to the catalog" do catalog.apply do |trans| catalog.resource(:file, File.join(path, "one")).must be_a(described_class) catalog.resource(:file, File.join(path, "two")).must be_a(described_class) end end it "should have an edge to each resource in the relationship graph" do catalog.apply do |trans| one = catalog.resource(:file, File.join(path, "one")) catalog.relationship_graph.should be_edge(@file, one) two = catalog.resource(:file, File.join(path, "two")) catalog.relationship_graph.should be_edge(@file, two) end end end describe "when copying files" do it "should be able to copy files with pound signs in their names (#285)" do source = tmpfile_with_contents("filewith#signs", "foo") dest = tmpfile("destwith#signs") catalog.add_resource described_class.new(:name => dest, :source => source) catalog.apply File.read(dest).should == "foo" end it "should be able to copy files with spaces in their names" do dest = tmpfile("destwith spaces") source = tmpfile_with_contents("filewith spaces", "foo") File.chmod(0755, source) catalog.add_resource described_class.new(:path => dest, :source => source) catalog.apply expected_mode = Puppet.features.microsoft_windows? ? 0644 : 0755 File.read(dest).should == "foo" (Puppet::FileSystem::File.new(dest).stat.mode & 007777).should == expected_mode end it "should be able to copy individual files even if recurse has been specified" do source = tmpfile_with_contents("source", "foo") dest = tmpfile("dest") catalog.add_resource described_class.new(:name => dest, :source => source, :recurse => true) catalog.apply File.read(dest).should == "foo" end end it "should create a file with content if ensure is omitted" do catalog.add_resource described_class.new( :path => path, :content => "this is some content, yo" ) catalog.apply File.read(path).should == "this is some content, yo" end it "should create files with content if both content and ensure are set" do file = described_class.new( :path => path, :ensure => "file", :content => "this is some content, yo" ) catalog.add_resource file catalog.apply File.read(path).should == "this is some content, yo" end it "should delete files with sources but that are set for deletion" do source = tmpfile_with_contents("source_source_with_ensure", "yay") dest = tmpfile_with_contents("source_source_with_ensure", "boo") file = described_class.new( :path => dest, :ensure => :absent, :source => source, :backup => false ) catalog.add_resource file catalog.apply Puppet::FileSystem::File.exist?(dest).should be_false end describe "when sourcing" do let(:source) { tmpfile_with_contents("source_default_values", "yay") } it "should apply the source metadata values" do set_mode(0770, source) file = described_class.new( :path => path, :ensure => :file, :source => source, :backup => false ) catalog.add_resource file catalog.apply get_owner(path).should == get_owner(source) get_group(path).should == get_group(source) (get_mode(path) & 07777).should == 0770 end it "should override the default metadata values" do set_mode(0770, source) file = described_class.new( :path => path, :ensure => :file, :source => source, :backup => false, :mode => 0440 ) catalog.add_resource file catalog.apply (get_mode(path) & 07777).should == 0440 end describe "on Windows systems", :if => Puppet.features.microsoft_windows? do def expects_sid_granted_full_access_explicitly(path, sid) inherited_ace = Windows::Security::INHERITED_ACE aces = get_aces_for_path_by_sid(path, sid) aces.should_not be_empty aces.each do |ace| - ace[:mask].should == Windows::File::FILE_ALL_ACCESS - (ace[:flags] & inherited_ace).should_not == inherited_ace + ace.mask.should == Windows::File::FILE_ALL_ACCESS + (ace.flags & inherited_ace).should_not == inherited_ace end end def expects_system_granted_full_access_explicitly(path) expects_sid_granted_full_access_explicitly(path, @sids[:system]) end def expects_at_least_one_inherited_ace_grants_full_access(path, sid) inherited_ace = Windows::Security::INHERITED_ACE aces = get_aces_for_path_by_sid(path, sid) aces.should_not be_empty aces.any? do |ace| - ace[:mask] == Windows::File::FILE_ALL_ACCESS && - (ace[:flags] & inherited_ace) == inherited_ace + ace.mask == Windows::File::FILE_ALL_ACCESS && + (ace.flags & inherited_ace) == inherited_ace end.should be_true end def expects_at_least_one_inherited_system_ace_grants_full_access(path) expects_at_least_one_inherited_ace_grants_full_access(path, @sids[:system]) end it "should provide valid default values when ACLs are not supported" do Puppet::Util::Windows::Security.stubs(:supports_acl?).with(source).returns false file = described_class.new( :path => path, :ensure => :file, :source => source, :backup => false ) catalog.add_resource file catalog.apply get_owner(path).should =~ /^S\-1\-5\-.*$/ get_group(path).should =~ /^S\-1\-0\-0.*$/ get_mode(path).should == 0644 end describe "when processing SYSTEM ACEs" do before do @sids = { :current_user => Puppet::Util::Windows::Security.name_to_sid(Sys::Admin.get_login), :system => Win32::Security::SID::LocalSystem, :admin => Puppet::Util::Windows::Security.name_to_sid("Administrator"), :guest => Puppet::Util::Windows::Security.name_to_sid("Guest"), :users => Win32::Security::SID::BuiltinUsers, :power_users => Win32::Security::SID::PowerUsers, + :none => Win32::Security::SID::Nobody } end describe "on files" do before :each do @file = described_class.new( :path => path, :ensure => :file, :source => source, :backup => false ) catalog.add_resource @file end + describe "when source permissions are ignored" do + before :each do + @file[:source_permissions] = :ignore + end + + it "preserves the inherited SYSTEM ACE" do + catalog.apply + + expects_at_least_one_inherited_system_ace_grants_full_access(path) + end + end + describe "when permissions are insync?" do - it "preserves inherited SYSTEM ACEs (needs access to SecurityDescriptor)" + it "preserves the explicit SYSTEM ACE" do + FileUtils.touch(path) + + sd = Puppet::Util::Windows::Security.get_security_descriptor(path) + sd.protect = true + sd.owner = @sids[:none] + sd.group = @sids[:none] + Puppet::Util::Windows::Security.set_security_descriptor(source, sd) + Puppet::Util::Windows::Security.set_security_descriptor(path, sd) + + catalog.apply + + expects_system_granted_full_access_explicitly(path) + end end describe "when permissions are not insync?" do before :each do @file[:owner] = 'None' @file[:group] = 'None' end it "replaces inherited SYSTEM ACEs with an uninherited one for an existing file" do FileUtils.touch(path) expects_at_least_one_inherited_system_ace_grants_full_access(path) catalog.apply expects_system_granted_full_access_explicitly(path) end it "replaces inherited SYSTEM ACEs for a new file with an uninherited one" do catalog.apply expects_system_granted_full_access_explicitly(path) end end describe "created with SYSTEM as the group" do before :each do @file[:owner] = @sids[:users] @file[:group] = @sids[:system] @file[:mode] = 0644 catalog.apply end it "should allow the user to explicitly set the mode to 4" do system_aces = get_aces_for_path_by_sid(path, @sids[:system]) system_aces.should_not be_empty system_aces.each do |ace| - ace[:mask].should == Windows::File::FILE_GENERIC_READ + ace.mask.should == Windows::File::FILE_GENERIC_READ end end - it "should restore SYSTEM permission to FULL access when the group is later changed" do + it "prepends SYSTEM ace when changing group from system to power users" do @file[:group] = @sids[:power_users] catalog.apply system_aces = get_aces_for_path_by_sid(path, @sids[:system]) - system_aces.should_not be_empty - system_aces.each { |ace| ace[:mask].should == Windows::File::FILE_ALL_ACCESS } + system_aces.size.should == 1 end end end describe "on directories" do before :each do @directory = described_class.new( :path => dir, :ensure => :directory ) catalog.add_resource @directory end + describe "when source permissions are ignored" do + before :each do + @directory[:source_permissions] = :ignore + end + + it "preserves the inherited SYSTEM ACE" do + catalog.apply + + expects_at_least_one_inherited_system_ace_grants_full_access(dir) + end + end + describe "when permissions are insync?" do - it "preserves inherited SYSTEM ACEs (needs access to SecurityDescriptor)" + it "preserves the explicit SYSTEM ACE" do + Dir.mkdir(dir) + + source_dir = tmpdir('source_dir') + @directory[:source] = source_dir + + sd = Puppet::Util::Windows::Security.get_security_descriptor(source_dir) + sd.protect = true + sd.owner = @sids[:none] + sd.group = @sids[:none] + Puppet::Util::Windows::Security.set_security_descriptor(source_dir, sd) + Puppet::Util::Windows::Security.set_security_descriptor(dir, sd) + + catalog.apply + + expects_system_granted_full_access_explicitly(dir) + end end describe "when permissions are not insync?" do before :each do @directory[:owner] = 'None' @directory[:group] = 'None' + @directory[:mode] = 0444 end it "replaces inherited SYSTEM ACEs with an uninherited one for an existing directory" do FileUtils.mkdir(dir) expects_at_least_one_inherited_system_ace_grants_full_access(dir) catalog.apply expects_system_granted_full_access_explicitly(dir) end it "replaces inherited SYSTEM ACEs with an uninherited one for an existing directory" do catalog.apply expects_system_granted_full_access_explicitly(dir) end describe "created with SYSTEM as the group" do before :each do - @directory[:group] = @sids[:system] @directory[:owner] = @sids[:users] + @directory[:group] = @sids[:system] @directory[:mode] = 0644 catalog.apply end it "should allow the user to explicitly set the mode to 4" do system_aces = get_aces_for_path_by_sid(dir, @sids[:system]) system_aces.should_not be_empty system_aces.each do |ace| # unlike files, Puppet sets execute bit on directories that are readable - ace[:mask].should == Windows::File::FILE_GENERIC_READ | Windows::File::FILE_GENERIC_EXECUTE + ace.mask.should == Windows::File::FILE_GENERIC_READ | Windows::File::FILE_GENERIC_EXECUTE end end - it "should restore SYSTEM permission to FULL access when the group is later changed" do + it "prepends SYSTEM ace when changing group from system to power users" do @directory[:group] = @sids[:power_users] catalog.apply system_aces = get_aces_for_path_by_sid(dir, @sids[:system]) - system_aces.should_not be_empty - system_aces.each { |ace| ace[:mask].should == Windows::File::FILE_ALL_ACCESS } + system_aces.size.should == 1 end end end end end end end describe "when purging files" do before do sourcedir = tmpdir("purge_source") destdir = tmpdir("purge_dest") sourcefile = File.join(sourcedir, "sourcefile") @copiedfile = File.join(destdir, "sourcefile") @localfile = File.join(destdir, "localfile") @purgee = File.join(destdir, "to_be_purged") File.open(@localfile, "w") { |f| f.print "oldtest" } File.open(sourcefile, "w") { |f| f.print "funtest" } # this file should get removed File.open(@purgee, "w") { |f| f.print "footest" } lfobj = Puppet::Type.newfile( :title => "localfile", :path => @localfile, :content => "rahtest", :ensure => :file, :backup => false ) destobj = Puppet::Type.newfile( :title => "destdir", :path => destdir, :source => sourcedir, :backup => false, :purge => true, :recurse => true ) catalog.add_resource lfobj, destobj catalog.apply end it "should still copy remote files" do File.read(@copiedfile).should == 'funtest' end it "should not purge managed, local files" do File.read(@localfile).should == 'rahtest' end it "should purge files that are neither remote nor otherwise managed" do Puppet::FileSystem::File.exist?(@purgee).should be_false end end def tmpfile_with_contents(name, contents) file = tmpfile(name) File.open(file, "w") { |f| f.write contents } file end def file_in_dir_with_contents(dir, name, contents) full_name = File.join(dir, name) File.open(full_name, "w") { |f| f.write contents } full_name end end diff --git a/spec/integration/util/windows/security_spec.rb b/spec/integration/util/windows/security_spec.rb index e60d91760..99d866f7f 100755 --- a/spec/integration/util/windows/security_spec.rb +++ b/spec/integration/util/windows/security_spec.rb @@ -1,698 +1,821 @@ #!/usr/bin/env ruby require 'spec_helper' require 'puppet/util/adsi' if Puppet.features.microsoft_windows? class WindowsSecurityTester require 'puppet/util/windows/security' include Puppet::Util::Windows::Security end end describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_windows? do include PuppetSpec::Files before :all do @sids = { :current_user => Puppet::Util::Windows::Security.name_to_sid(Sys::Admin.get_login), :system => Win32::Security::SID::LocalSystem, :admin => Puppet::Util::Windows::Security.name_to_sid("Administrator"), :administrators => Win32::Security::SID::BuiltinAdministrators, :guest => Puppet::Util::Windows::Security.name_to_sid("Guest"), :users => Win32::Security::SID::BuiltinUsers, :power_users => Win32::Security::SID::PowerUsers, :none => Win32::Security::SID::Nobody, + :everyone => Win32::Security::SID::Everyone } end let (:sids) { @sids } let (:winsec) { WindowsSecurityTester.new } def set_group_depending_on_current_user(path) if sids[:current_user] == sids[:system] # if the current user is SYSTEM, by setting the group to # guest, SYSTEM is automagically given full control, so instead # override that behavior with SYSTEM as group and a specific mode winsec.set_group(sids[:system], path) mode = winsec.get_mode(path) winsec.set_mode(mode & ~WindowsSecurityTester::S_IRWXG, path) else winsec.set_group(sids[:guest], path) end end shared_examples_for "only child owner" do it "should allow child owner" do - check_child_owner + winsec.set_owner(sids[:guest], parent) + winsec.set_group(sids[:current_user], parent) + winsec.set_mode(0700, parent) + + check_delete(path) end it "should deny parent owner" do - pending("when running as SYSTEM the absence of a SYSTEM group/owner causes full access to be added for SYSTEM", - :if => sids[:current_user] == sids[:system]) do - lambda { check_parent_owner }.should raise_error(Errno::EACCES) - end + winsec.set_owner(sids[:guest], path) + winsec.set_group(sids[:current_user], path) + winsec.set_mode(0700, path) + + lambda { check_delete(path) }.should raise_error(Errno::EACCES) end it "should deny group" do - lambda { check_group }.should raise_error(Errno::EACCES) + winsec.set_owner(sids[:guest], path) + winsec.set_group(sids[:current_user], path) + winsec.set_mode(0700, path) + + lambda { check_delete(path) }.should raise_error(Errno::EACCES) end it "should deny other" do - pending("when running as SYSTEM the absence of a SYSTEM group/owner causes full access to be added for SYSTEM", - :if => sids[:current_user] == sids[:system]) do - lambda { check_other }.should raise_error(Errno::EACCES) - end + winsec.set_owner(sids[:guest], path) + winsec.set_group(sids[:current_user], path) + winsec.set_mode(0700, path) + + lambda { check_delete(path) }.should raise_error(Errno::EACCES) end end shared_examples_for "a securable object" do describe "on a volume that doesn't support ACLs" do [:owner, :group, :mode].each do |p| it "should return nil #{p}" do winsec.stubs(:supports_acl?).returns false winsec.send("get_#{p}", path).should be_nil end end end describe "on a volume that supports ACLs" do describe "for a normal user" do before :each do Puppet.features.stubs(:root?).returns(false) end after :each do winsec.set_mode(WindowsSecurityTester::S_IRWXU, parent) winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) if Puppet::FileSystem::File.exist?(path) end describe "#supports_acl?" do %w[c:/ c:\\ c:/windows/system32 \\\\localhost\\C$ \\\\127.0.0.1\\C$\\foo].each do |path| it "should accept #{path}" do winsec.should be_supports_acl(path) end end it "should raise an exception if it cannot get volume information" do expect { winsec.supports_acl?('foobar') }.to raise_error(Puppet::Error, /Failed to get volume information/) end end describe "#owner=" do it "should allow setting to the current user" do winsec.set_owner(sids[:current_user], path) end it "should raise an exception when setting to a different user" do lambda { winsec.set_owner(sids[:guest], path) }.should raise_error(Puppet::Error, /This security ID may not be assigned as the owner of this object./) end end describe "#owner" do it "it should not be empty" do winsec.get_owner(path).should_not be_empty end it "should raise an exception if an invalid path is provided" do lambda { winsec.get_owner("c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) end end describe "#group=" do it "should allow setting to a group the current owner is a member of" do winsec.set_group(sids[:users], path) end # Unlike unix, if the user has permission to WRITE_OWNER, which the file owner has by default, # then they can set the primary group to a group that the user does not belong to. it "should allow setting to a group the current owner is not a member of" do winsec.set_group(sids[:power_users], path) end end describe "#group" do it "should not be empty" do winsec.get_group(path).should_not be_empty end it "should raise an exception if an invalid path is provided" do lambda { winsec.get_group("c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) end end - it "should preserve full control for SYSTEM when setting owner and group" do + it "should preserve inherited full control for SYSTEM when setting owner and group" do # new file has SYSTEM system_aces = winsec.get_aces_for_path_by_sid(path, sids[:system]) system_aces.should_not be_empty # when running under SYSTEM account, multiple ACEs come back # so we only care that we have at least one of these system_aces.any? do |ace| - ace[:mask] == Windows::File::FILE_ALL_ACCESS + ace.mask == Windows::File::FILE_ALL_ACCESS end.should be_true + # changing the owner/group will no longer make the SD protected winsec.set_group(sids[:power_users], path) winsec.set_owner(sids[:administrators], path) - # and should still have a noninherited SYSTEM ACE granting full control - system_aces = winsec.get_aces_for_path_by_sid(path, sids[:system]) - - inherited = Windows::Security::INHERITED_ACE - system_aces.each do |ace| - ace[:mask].should == Windows::File::FILE_ALL_ACCESS - (ace[:flags] & inherited).should_not == inherited - end + system_aces.find do |ace| + ace.mask == Windows::File::FILE_ALL_ACCESS && ace.inherited? + end.should_not be_nil end describe "#mode=" do (0000..0700).step(0100) do |mode| it "should enforce mode #{mode.to_s(8)}" do winsec.set_mode(mode, path) check_access(mode, path) end end it "should round-trip all 128 modes that do not require deny ACEs" do 0.upto(1).each do |s| 0.upto(7).each do |u| 0.upto(u).each do |g| 0.upto(g).each do |o| # if user is superset of group, and group superset of other, then # no deny ace is required, and mode can be converted to win32 # access mask, and back to mode without loss of information # (provided the owner and group are not the same) next if ((u & g) != g) or ((g & o) != o) mode = (s << 9 | u << 6 | g << 3 | o << 0) winsec.set_mode(mode, path) winsec.get_mode(path).to_s(8).should == mode.to_s(8) end end end end end it "should preserve full control for SYSTEM when setting mode" do # new file has SYSTEM system_aces = winsec.get_aces_for_path_by_sid(path, sids[:system]) system_aces.should_not be_empty + # when running under SYSTEM account, multiple ACEs come back # so we only care that we have at least one of these system_aces.any? do |ace| - ace[:mask] == WindowsSecurityTester::FILE_ALL_ACCESS + ace.mask == WindowsSecurityTester::FILE_ALL_ACCESS end.should be_true + # changing the mode will make the SD protected winsec.set_group(sids[:none], path) winsec.set_mode(0600, path) - # and should still have the same SYSTEM ACE(s) - inherited = Windows::Security::INHERITED_ACE + # and should have a non-inherited SYSTEM ACE(s) system_aces = winsec.get_aces_for_path_by_sid(path, sids[:system]) system_aces.each do |ace| - ace[:mask].should == Windows::File::FILE_ALL_ACCESS - (ace[:flags] & inherited).should_not == inherited + ace.mask.should == Windows::File::FILE_ALL_ACCESS && ! ace.inherited? end end describe "for modes that require deny aces" do it "should map everyone to group and owner" do winsec.set_mode(0426, path) winsec.get_mode(path).to_s(8).should == "666" end it "should combine user and group modes when owner and group sids are equal" do winsec.set_group(winsec.get_owner(path), path) winsec.set_mode(0410, path) winsec.get_mode(path).to_s(8).should == "550" end end describe "for read-only objects" do before :each do winsec.set_group(sids[:none], path) winsec.set_mode(0600, path) winsec.add_attributes(path, WindowsSecurityTester::FILE_ATTRIBUTE_READONLY) (winsec.get_attributes(path) & WindowsSecurityTester::FILE_ATTRIBUTE_READONLY).should be_nonzero end it "should make them writable if any sid has write permission" do winsec.set_mode(WindowsSecurityTester::S_IWUSR, path) (winsec.get_attributes(path) & WindowsSecurityTester::FILE_ATTRIBUTE_READONLY).should == 0 end it "should leave them read-only if no sid has write permission and should allow full access for SYSTEM" do winsec.set_mode(WindowsSecurityTester::S_IRUSR | WindowsSecurityTester::S_IXGRP, path) (winsec.get_attributes(path) & WindowsSecurityTester::FILE_ATTRIBUTE_READONLY).should be_nonzero system_aces = winsec.get_aces_for_path_by_sid(path, sids[:system]) # when running under SYSTEM account, and set_group / set_owner hasn't been called # SYSTEM full access will be restored system_aces.any? do |ace| - ace[:mask] == Windows::File::FILE_ALL_ACCESS + ace.mask == Windows::File::FILE_ALL_ACCESS end.should be_true end end it "should raise an exception if an invalid path is provided" do lambda { winsec.set_mode(sids[:guest], "c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) end end describe "#mode" do it "should report when extra aces are encounted" do - winsec.set_acl(path, true) do |acl| - (544..547).each do |rid| - winsec.add_access_allowed_ace(acl, WindowsSecurityTester::STANDARD_RIGHTS_ALL, "S-1-5-32-#{rid}") - end + sd = winsec.get_security_descriptor(path) + (544..547).each do |rid| + sd.dacl.allow("S-1-5-32-#{rid}", WindowsSecurityTester::STANDARD_RIGHTS_ALL) end + winsec.set_security_descriptor(path, sd) + mode = winsec.get_mode(path) (mode & WindowsSecurityTester::S_IEXTRA).should == WindowsSecurityTester::S_IEXTRA end - it "should warn if a deny ace is encountered" do - winsec.set_acl(path) do |acl| - winsec.add_access_denied_ace(acl, WindowsSecurityTester::FILE_GENERIC_WRITE, sids[:guest]) - winsec.add_access_allowed_ace(acl, WindowsSecurityTester::STANDARD_RIGHTS_ALL | WindowsSecurityTester::SPECIFIC_RIGHTS_ALL, sids[:current_user]) - end - - Puppet.expects(:warning).with("Unsupported access control entry type: 0x1").at_least_once + it "should return deny aces" do + sd = winsec.get_security_descriptor(path) + sd.dacl.deny(sids[:guest], WindowsSecurityTester::FILE_GENERIC_WRITE) + winsec.set_security_descriptor(path, sd) - winsec.get_mode(path) + guest_aces = winsec.get_aces_for_path_by_sid(path, sids[:guest]) + guest_aces.find do |ace| + ace.type == WindowsSecurityTester::ACCESS_DENIED_ACE_TYPE + end.should_not be_nil end it "should skip inherit-only ace" do - winsec.set_acl(path) do |acl| - winsec.add_access_allowed_ace(acl, WindowsSecurityTester::STANDARD_RIGHTS_ALL | WindowsSecurityTester::SPECIFIC_RIGHTS_ALL, sids[:current_user]) - winsec.add_access_allowed_ace(acl, WindowsSecurityTester::FILE_GENERIC_READ, Win32::Security::SID::Everyone, WindowsSecurityTester::INHERIT_ONLY_ACE | WindowsSecurityTester::OBJECT_INHERIT_ACE) - end + sd = winsec.get_security_descriptor(path) + dacl = Puppet::Util::Windows::AccessControlList.new + dacl.allow( + sids[:current_user], WindowsSecurityTester::STANDARD_RIGHTS_ALL | WindowsSecurityTester::SPECIFIC_RIGHTS_ALL + ) + dacl.allow( + sids[:everyone], + WindowsSecurityTester::FILE_GENERIC_READ, + WindowsSecurityTester::INHERIT_ONLY_ACE | WindowsSecurityTester::OBJECT_INHERIT_ACE + ) + winsec.set_security_descriptor(path, sd) (winsec.get_mode(path) & WindowsSecurityTester::S_IRWXO).should == 0 end it "should raise an exception if an invalid path is provided" do lambda { winsec.get_mode("c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) end end describe "inherited access control entries" do it "should be absent when the access control list is protected, and should not remove SYSTEM" do winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) mode = winsec.get_mode(path) [ WindowsSecurityTester::S_IEXTRA, WindowsSecurityTester::S_ISYSTEM_MISSING ].each do |flag| (mode & flag).should_not == flag end end it "should be present when the access control list is unprotected" do # add a bunch of aces to the parent with permission to add children allow = WindowsSecurityTester::STANDARD_RIGHTS_ALL | WindowsSecurityTester::SPECIFIC_RIGHTS_ALL inherit = WindowsSecurityTester::OBJECT_INHERIT_ACE | WindowsSecurityTester::CONTAINER_INHERIT_ACE - winsec.set_acl(parent, true) do |acl| - winsec.add_access_allowed_ace(acl, allow, "S-1-1-0", inherit) # everyone - - (544..547).each do |rid| - winsec.add_access_allowed_ace(acl, WindowsSecurityTester::STANDARD_RIGHTS_ALL, "S-1-5-32-#{rid}", inherit) - end + sd = winsec.get_security_descriptor(parent) + sd.dacl.allow( + "S-1-1-0", #everyone + allow, + inherit + ) + (544..547).each do |rid| + sd.dacl.allow( + "S-1-5-32-#{rid}", + WindowsSecurityTester::STANDARD_RIGHTS_ALL, + inherit + ) end + winsec.set_security_descriptor(parent, sd) # unprotect child, it should inherit from parent winsec.set_mode(WindowsSecurityTester::S_IRWXU, path, false) (winsec.get_mode(path) & WindowsSecurityTester::S_IEXTRA).should == WindowsSecurityTester::S_IEXTRA end end end describe "for an administrator", :if => Puppet.features.root? do before :each do winsec.set_mode(WindowsSecurityTester::S_IRWXU | WindowsSecurityTester::S_IRWXG, path) set_group_depending_on_current_user(path) winsec.set_owner(sids[:guest], path) lambda { File.open(path, 'r') }.should raise_error(Errno::EACCES) end after :each do if Puppet::FileSystem::File.exist?(path) winsec.set_owner(sids[:current_user], path) winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) end end describe "#owner=" do it "should accept a user sid" do winsec.set_owner(sids[:admin], path) winsec.get_owner(path).should == sids[:admin] end it "should accept a group sid" do winsec.set_owner(sids[:power_users], path) winsec.get_owner(path).should == sids[:power_users] end it "should raise an exception if an invalid sid is provided" do lambda { winsec.set_owner("foobar", path) }.should raise_error(Puppet::Error, /Failed to convert string SID/) end it "should raise an exception if an invalid path is provided" do lambda { winsec.set_owner(sids[:guest], "c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) end end describe "#group=" do it "should accept a group sid" do winsec.set_group(sids[:power_users], path) winsec.get_group(path).should == sids[:power_users] end it "should accept a user sid" do winsec.set_group(sids[:admin], path) winsec.get_group(path).should == sids[:admin] end - it "should allow owner and group to be the same sid" do - winsec.set_mode(0610, path) + it "should combine owner and group rights when they are the same sid" do winsec.set_owner(sids[:power_users], path) winsec.set_group(sids[:power_users], path) + winsec.set_mode(0610, path) winsec.get_owner(path).should == sids[:power_users] winsec.get_group(path).should == sids[:power_users] # note group execute permission added to user ace, and then group rwx value # reflected to match - winsec.get_mode(path).to_s(8).should == "770" + + # Exclude missing system ace, since that's not relevant + (winsec.get_mode(path) & 0777).to_s(8).should == "770" end it "should raise an exception if an invalid sid is provided" do lambda { winsec.set_group("foobar", path) }.should raise_error(Puppet::Error, /Failed to convert string SID/) end it "should raise an exception if an invalid path is provided" do lambda { winsec.set_group(sids[:guest], "c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) end end describe "when the sid is NULL" do it "should retrieve an empty owner sid" it "should retrieve an empty group sid" end describe "when the sid refers to a deleted trustee" do it "should retrieve the user sid" do sid = nil user = Puppet::Util::ADSI::User.create("delete_me_user") user.commit begin sid = Sys::Admin::get_user(user.name).sid winsec.set_owner(sid, path) winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) ensure Puppet::Util::ADSI::User.delete(user.name) end winsec.get_owner(path).should == sid winsec.get_mode(path).should == WindowsSecurityTester::S_IRWXU end it "should retrieve the group sid" do sid = nil group = Puppet::Util::ADSI::Group.create("delete_me_group") group.commit begin sid = Sys::Admin::get_group(group.name).sid winsec.set_group(sid, path) winsec.set_mode(WindowsSecurityTester::S_IRWXG, path) ensure Puppet::Util::ADSI::Group.delete(group.name) end winsec.get_group(path).should == sid winsec.get_mode(path).should == WindowsSecurityTester::S_IRWXG end end describe "#mode" do it "should deny all access when the DACL is empty, including SYSTEM" do - winsec.set_acl(path, true) { |acl| } + sd = winsec.get_security_descriptor(path) + # don't allow inherited aces to affect the test + protect = true + new_sd = Puppet::Util::Windows::SecurityDescriptor.new(sd.owner, sd.group, [], protect) + winsec.set_security_descriptor(path, new_sd) winsec.get_mode(path).should == WindowsSecurityTester::S_ISYSTEM_MISSING end # REMIND: ruby crashes when trying to set a NULL DACL # it "should allow all when it is nil" do # winsec.set_owner(sids[:current_user], path) # winsec.open_file(path, WindowsSecurityTester::READ_CONTROL | WindowsSecurityTester::WRITE_DAC) do |handle| # winsec.set_security_info(handle, WindowsSecurityTester::DACL_SECURITY_INFORMATION | WindowsSecurityTester::PROTECTED_DACL_SECURITY_INFORMATION, nil) # end # winsec.get_mode(path).to_s(8).should == "777" # end end describe "when the parent directory" do before :each do winsec.set_owner(sids[:current_user], parent) winsec.set_owner(sids[:current_user], path) winsec.set_mode(0777, path, false) end - def check_child_owner - winsec.set_group(sids[:guest], parent) - winsec.set_owner(sids[:guest], parent) + describe "is writable and executable" do + describe "and sticky bit is set" do + it "should allow child owner" do + winsec.set_owner(sids[:guest], parent) + winsec.set_group(sids[:current_user], parent) + winsec.set_mode(01700, parent) - check_delete(path) - end + check_delete(path) + end - def check_parent_owner - winsec.set_group(sids[:guest], path) - winsec.set_owner(sids[:guest], path) + it "should allow parent owner" do + winsec.set_owner(sids[:current_user], parent) + winsec.set_group(sids[:guest], parent) + winsec.set_mode(01700, parent) - check_delete(path) - end + winsec.set_owner(sids[:current_user], path) + winsec.set_group(sids[:guest], path) + winsec.set_mode(0700, path) - def check_group - winsec.set_group(sids[:current_user], path) - winsec.set_owner(sids[:guest], path) + check_delete(path) + end - winsec.set_owner(sids[:guest], parent) + it "should deny group" do + winsec.set_owner(sids[:guest], parent) + winsec.set_group(sids[:current_user], parent) + winsec.set_mode(01770, parent) - check_delete(path) - end + winsec.set_owner(sids[:guest], path) + winsec.set_group(sids[:current_user], path) + winsec.set_mode(0700, path) - def check_other - winsec.set_group(sids[:guest], path) - winsec.set_owner(sids[:guest], path) + lambda { check_delete(path) }.should raise_error(Errno::EACCES) + end - winsec.set_owner(sids[:guest], parent) + it "should deny other" do + winsec.set_owner(sids[:guest], parent) + winsec.set_group(sids[:current_user], parent) + winsec.set_mode(01777, parent) - check_delete(path) - end + winsec.set_owner(sids[:guest], path) + winsec.set_group(sids[:current_user], path) + winsec.set_mode(0700, path) - describe "is writable and executable" do - describe "and sticky bit is set" do - before :each do - winsec.set_mode(01777, parent) + lambda { check_delete(path) }.should raise_error(Errno::EACCES) + end end - it "should allow child owner" do - check_child_owner - end + describe "and sticky bit is not set" do + it "should allow child owner" do + winsec.set_owner(sids[:guest], parent) + winsec.set_group(sids[:current_user], parent) + winsec.set_mode(0700, parent) - it "should allow parent owner" do - check_parent_owner - end + check_delete(path) + end - it "should deny group" do - lambda { check_group }.should raise_error(Errno::EACCES) - end + it "should allow parent owner" do + winsec.set_owner(sids[:current_user], parent) + winsec.set_group(sids[:guest], parent) + winsec.set_mode(0700, parent) + + winsec.set_owner(sids[:current_user], path) + winsec.set_group(sids[:guest], path) + winsec.set_mode(0700, path) - it "should deny other" do - pending("when running as SYSTEM the absence of a SYSTEM group/owner causes full access to be added for SYSTEM", - :if => sids[:current_user] == sids[:system]) do - lambda { check_other }.should raise_error(Errno::EACCES) + check_delete(path) end - end - end - describe "and sticky bit is not set" do - before :each do - winsec.set_mode(0777, parent) - end + it "should allow group" do + winsec.set_owner(sids[:guest], parent) + winsec.set_group(sids[:current_user], parent) + winsec.set_mode(0770, parent) - it "should allow child owner" do - check_child_owner - end + winsec.set_owner(sids[:guest], path) + winsec.set_group(sids[:current_user], path) + winsec.set_mode(0700, path) - it "should allow parent owner" do - check_parent_owner - end + check_delete(path) + end - it "should allow group" do - check_group - end + it "should allow other" do + winsec.set_owner(sids[:guest], parent) + winsec.set_group(sids[:current_user], parent) + winsec.set_mode(0777, parent) - it "should allow other" do - check_other + winsec.set_owner(sids[:guest], path) + winsec.set_group(sids[:current_user], path) + winsec.set_mode(0700, path) + + check_delete(path) + end end end - end - describe "is not writable" do - before :each do - winsec.set_mode(0555, parent) + describe "is not writable" do + before :each do + winsec.set_group(sids[:current_user], parent) + winsec.set_mode(0555, parent) + end + + it_behaves_like "only child owner" end - it_behaves_like "only child owner" - end + describe "is not executable" do + before :each do + winsec.set_group(sids[:current_user], parent) + winsec.set_mode(0666, parent) + end - describe "is not executable" do - before :each do - winsec.set_mode(0666, parent) + it_behaves_like "only child owner" end - - it_behaves_like "only child owner" end end end end - end describe "file" do let (:parent) do tmpdir('win_sec_test_file') end let (:path) do path = File.join(parent, 'childfile') File.new(path, 'w').close path end it_behaves_like "a securable object" do def check_access(mode, path) if (mode & WindowsSecurityTester::S_IRUSR).nonzero? check_read(path) else lambda { check_read(path) }.should raise_error(Errno::EACCES) end if (mode & WindowsSecurityTester::S_IWUSR).nonzero? check_write(path) else lambda { check_write(path) }.should raise_error(Errno::EACCES) end if (mode & WindowsSecurityTester::S_IXUSR).nonzero? lambda { check_execute(path) }.should raise_error(Errno::ENOEXEC) else lambda { check_execute(path) }.should raise_error(Errno::EACCES) end end def check_read(path) File.open(path, 'r').close end def check_write(path) File.open(path, 'w').close end def check_execute(path) Kernel.exec(path) end def check_delete(path) File.delete(path) end end describe "locked files" do let (:explorer) { File.join(Dir::WINDOWS, "explorer.exe") } it "should get the owner" do winsec.get_owner(explorer).should match /^S-1-5-/ end it "should get the group" do winsec.get_group(explorer).should match /^S-1-5-/ end it "should get the mode" do winsec.get_mode(explorer).should == (WindowsSecurityTester::S_IRWXU | WindowsSecurityTester::S_IRWXG | WindowsSecurityTester::S_IEXTRA) end end end describe "directory" do let (:parent) do tmpdir('win_sec_test_dir') end let (:path) do path = File.join(parent, 'childdir') Dir.mkdir(path) path end it_behaves_like "a securable object" do def check_access(mode, path) if (mode & WindowsSecurityTester::S_IRUSR).nonzero? check_read(path) else lambda { check_read(path) }.should raise_error(Errno::EACCES) end if (mode & WindowsSecurityTester::S_IWUSR).nonzero? check_write(path) else lambda { check_write(path) }.should raise_error(Errno::EACCES) end if (mode & WindowsSecurityTester::S_IXUSR).nonzero? check_execute(path) else lambda { check_execute(path) }.should raise_error(Errno::EACCES) end end def check_read(path) Dir.entries(path) end def check_write(path) Dir.mkdir(File.join(path, "subdir")) end def check_execute(path) Dir.chdir(path) {} end def check_delete(path) Dir.rmdir(path) end end describe "inheritable aces" do it "should be applied to child objects" do mode640 = WindowsSecurityTester::S_IRUSR | WindowsSecurityTester::S_IWUSR | WindowsSecurityTester::S_IRGRP winsec.set_mode(mode640, path) newfile = File.join(path, "newfile.txt") File.new(newfile, "w").close newdir = File.join(path, "newdir") Dir.mkdir(newdir) [newfile, newdir].each do |p| mode = winsec.get_mode(p) (mode & 07777).to_s(8).should == mode640.to_s(8) end end end end + + context "security descriptor" do + let(:path) { tmpfile('sec_descriptor') } + let(:read_execute) { 0x201FF } + let(:synchronize) { 0x100000 } + + before :each do + FileUtils.touch(path) + end + + it "preserves aces for other users" do + dacl = Puppet::Util::Windows::AccessControlList.new + sids_in_dacl = [sids[:current_user], sids[:users]] + sids_in_dacl.each do |sid| + dacl.allow(sid, read_execute) + end + sd = Puppet::Util::Windows::SecurityDescriptor.new(sids[:guest], sids[:guest], dacl, true) + winsec.set_security_descriptor(path, sd) + + aces = winsec.get_security_descriptor(path).dacl.to_a + aces.map(&:sid).should == sids_in_dacl + aces.map(&:mask).all? { |mask| mask == read_execute }.should be_true + end + + it "changes the sid for all aces that were assigned to the old owner" do + sd = winsec.get_security_descriptor(path) + sd.owner.should_not == sids[:guest] + + sd.dacl.allow(sd.owner, read_execute) + sd.dacl.allow(sd.owner, synchronize) + + sd.owner = sids[:guest] + winsec.set_security_descriptor(path, sd) + + dacl = winsec.get_security_descriptor(path).dacl + aces = dacl.find_all { |ace| ace.sid == sids[:guest] } + # only non-inherited aces will be reassigned to guest, so + # make sure we find at least the two we added + aces.size.should >= 2 + end + + it "preserves INHERIT_ONLY_ACEs" do + # inherit only aces can only be set on directories + dir = tmpdir('inheritonlyace') + + inherit_flags = Puppet::Util::Windows::AccessControlEntry::INHERIT_ONLY_ACE | + Puppet::Util::Windows::AccessControlEntry::OBJECT_INHERIT_ACE | + Puppet::Util::Windows::AccessControlEntry::CONTAINER_INHERIT_ACE + + sd = winsec.get_security_descriptor(dir) + sd.dacl.allow(sd.owner, Windows::File::FILE_ALL_ACCESS, inherit_flags) + winsec.set_security_descriptor(dir, sd) + + sd = winsec.get_security_descriptor(dir) + + winsec.set_owner(sids[:guest], dir) + + sd = winsec.get_security_descriptor(dir) + sd.dacl.find do |ace| + ace.sid == sids[:guest] && ace.inherit_only? + end.should_not be_nil + end + + context "when managing mode" do + it "removes aces for sids that are neither the owner nor group" do + # add a guest ace, it's never owner or group + sd = winsec.get_security_descriptor(path) + sd.dacl.allow(sids[:guest], read_execute) + winsec.set_security_descriptor(path, sd) + + # setting the mode, it should remove extra aces + winsec.set_mode(0770, path) + + # make sure it's gone + dacl = winsec.get_security_descriptor(path).dacl + aces = dacl.find_all { |ace| ace.sid == sids[:guest] } + aces.should be_empty + end + end + end end diff --git a/spec/unit/settings/path_setting_spec.rb b/spec/unit/settings/path_setting_spec.rb index d2ec46b5d..b7bc3198c 100755 --- a/spec/unit/settings/path_setting_spec.rb +++ b/spec/unit/settings/path_setting_spec.rb @@ -1,30 +1,30 @@ #! /usr/bin/env ruby require 'spec_helper' describe Puppet::Settings::PathSetting do subject { described_class.new(:settings => mock('settings'), :desc => "test") } context "#munge" do it "should expand all path elements" do munged = subject.munge("hello#{File::PATH_SEPARATOR}good/morning#{File::PATH_SEPARATOR}goodbye") munged.split(File::PATH_SEPARATOR).each do |p| Puppet::Util.should be_absolute_path(p) end end it "should leave nil as nil" do subject.munge(nil).should be_nil end context "on Windows", :if => Puppet.features.microsoft_windows? do it "should convert \\ to /" do subject.munge('C:\test\directory').should == 'C:/test/directory' end it "should work with UNC paths" do - subject.munge('//server/some/path').should == '//server/some/path' - subject.munge('\\\\server\some\path').should == '//server/some/path' + subject.munge('//localhost/some/path').should == '//localhost/some/path' + subject.munge('\\\\localhost\some\path').should == '//localhost/some/path' end end end end diff --git a/spec/unit/type/file_spec.rb b/spec/unit/type/file_spec.rb index 2845fc75b..75b58926a 100755 --- a/spec/unit/type/file_spec.rb +++ b/spec/unit/type/file_spec.rb @@ -1,1464 +1,1464 @@ #! /usr/bin/env ruby require 'spec_helper' describe Puppet::Type.type(:file) do include PuppetSpec::Files let(:path) { tmpfile('file_testing') } let(:file) { described_class.new(:path => path, :catalog => catalog) } let(:provider) { file.provider } let(:catalog) { Puppet::Resource::Catalog.new } before do Puppet.features.stubs("posix?").returns(true) end describe "the path parameter" do describe "on POSIX systems", :if => Puppet.features.posix? do it "should remove trailing slashes" do file[:path] = "/foo/bar/baz/" file[:path].should == "/foo/bar/baz" end it "should remove double slashes" do file[:path] = "/foo/bar//baz" file[:path].should == "/foo/bar/baz" end it "should remove triple slashes" do file[:path] = "/foo/bar///baz" file[:path].should == "/foo/bar/baz" end it "should remove trailing double slashes" do file[:path] = "/foo/bar/baz//" file[:path].should == "/foo/bar/baz" end it "should leave a single slash alone" do file[:path] = "/" file[:path].should == "/" end it "should accept and collapse a double-slash at the start of the path" do file[:path] = "//tmp/xxx" file[:path].should == '/tmp/xxx' end it "should accept and collapse a triple-slash at the start of the path" do file[:path] = "///tmp/xxx" file[:path].should == '/tmp/xxx' end end describe "on Windows systems", :if => Puppet.features.microsoft_windows? do it "should remove trailing slashes" do file[:path] = "X:/foo/bar/baz/" file[:path].should == "X:/foo/bar/baz" end it "should remove double slashes" do file[:path] = "X:/foo/bar//baz" file[:path].should == "X:/foo/bar/baz" end it "should remove trailing double slashes" do file[:path] = "X:/foo/bar/baz//" file[:path].should == "X:/foo/bar/baz" end it "should leave a drive letter with a slash alone" do file[:path] = "X:/" file[:path].should == "X:/" end it "should not accept a drive letter without a slash" do expect { file[:path] = "X:" }.to raise_error(/File paths must be fully qualified/) end describe "when using UNC filenames", :if => Puppet.features.microsoft_windows? do it "should remove trailing slashes" do - file[:path] = "//server/foo/bar/baz/" - file[:path].should == "//server/foo/bar/baz" + file[:path] = "//localhost/foo/bar/baz/" + file[:path].should == "//localhost/foo/bar/baz" end it "should remove double slashes" do - file[:path] = "//server/foo/bar//baz" - file[:path].should == "//server/foo/bar/baz" + file[:path] = "//localhost/foo/bar//baz" + file[:path].should == "//localhost/foo/bar/baz" end it "should remove trailing double slashes" do - file[:path] = "//server/foo/bar/baz//" - file[:path].should == "//server/foo/bar/baz" + file[:path] = "//localhost/foo/bar/baz//" + file[:path].should == "//localhost/foo/bar/baz" end it "should remove a trailing slash from a sharename" do - file[:path] = "//server/foo/" - file[:path].should == "//server/foo" + file[:path] = "//localhost/foo/" + file[:path].should == "//localhost/foo" end it "should not modify a sharename" do - file[:path] = "//server/foo" - file[:path].should == "//server/foo" + file[:path] = "//localhost/foo" + file[:path].should == "//localhost/foo" end end end end describe "the backup parameter" do [false, 'false', :false].each do |value| it "should disable backup if the value is #{value.inspect}" do file[:backup] = value file[:backup].should == false end end [true, 'true', '.puppet-bak'].each do |value| it "should use .puppet-bak if the value is #{value.inspect}" do file[:backup] = value file[:backup].should == '.puppet-bak' end end it "should use the provided value if it's any other string" do file[:backup] = "over there" file[:backup].should == "over there" end it "should fail if backup is set to anything else" do expect do file[:backup] = 97 end.to raise_error(Puppet::Error, /Invalid backup type 97/) end end describe "the recurse parameter" do it "should default to recursion being disabled" do file[:recurse].should be_false end [true, "true", "inf", "remote"].each do |value| it "should consider #{value} to enable recursion" do file[:recurse] = value file[:recurse].should be_true end end it "should not allow numbers" do expect { file[:recurse] = 10 }.to raise_error( Puppet::Error, /Parameter recurse failed on File\[[^\]]+\]: Invalid recurse value 10/) end [false, "false"].each do |value| it "should consider #{value} to disable recursion" do file[:recurse] = value file[:recurse].should be_false end end end describe "the recurselimit parameter" do it "should accept integers" do file[:recurselimit] = 12 file[:recurselimit].should == 12 end it "should munge string numbers to number numbers" do file[:recurselimit] = '12' file[:recurselimit].should == 12 end it "should fail if given a non-number" do expect do file[:recurselimit] = 'twelve' end.to raise_error(Puppet::Error, /Invalid value "twelve"/) end end describe "the replace parameter" do [true, :true, :yes].each do |value| it "should consider #{value} to be true" do file[:replace] = value file[:replace].should be_true end end [false, :false, :no].each do |value| it "should consider #{value} to be false" do file[:replace] = value file[:replace].should be_false end end end describe ".instances" do it "should return an empty array" do described_class.instances.should == [] end end describe "#bucket" do it "should return nil if backup is off" do file[:backup] = false file.bucket.should == nil end it "should not return a bucket if using a file extension for backup" do file[:backup] = '.backup' file.bucket.should == nil end it "should return the default filebucket if using the 'puppet' filebucket" do file[:backup] = 'puppet' bucket = stub('bucket') file.stubs(:default_bucket).returns bucket file.bucket.should == bucket end it "should fail if using a remote filebucket and no catalog exists" do file.catalog = nil file[:backup] = 'my_bucket' expect { file.bucket }.to raise_error(Puppet::Error, "Can not find filebucket for backups without a catalog") end it "should fail if the specified filebucket isn't in the catalog" do file[:backup] = 'my_bucket' expect { file.bucket }.to raise_error(Puppet::Error, "Could not find filebucket my_bucket specified in backup") end it "should use the specified filebucket if it is in the catalog" do file[:backup] = 'my_bucket' filebucket = Puppet::Type.type(:filebucket).new(:name => 'my_bucket') catalog.add_resource(filebucket) file.bucket.should == filebucket.bucket end end describe "#asuser" do before :each do # Mocha won't let me just stub SUIDManager.asuser to yield and return, # but it will do exactly that if we're not root. Puppet.features.stubs(:root?).returns false end it "should return the desired owner if they can write to the parent directory" do file[:owner] = 1001 FileTest.stubs(:writable?).with(File.dirname file[:path]).returns true file.asuser.should == 1001 end it "should return nil if the desired owner can't write to the parent directory" do file[:owner] = 1001 FileTest.stubs(:writable?).with(File.dirname file[:path]).returns false file.asuser.should == nil end it "should return nil if not managing owner" do file.asuser.should == nil end end describe "#exist?" do it "should be considered existent if it can be stat'ed" do file.expects(:stat).returns mock('stat') file.must be_exist end it "should be considered nonexistent if it can not be stat'ed" do file.expects(:stat).returns nil file.must_not be_exist end end describe "#eval_generate" do before do @graph = stub 'graph', :add_edge => nil catalog.stubs(:relationship_graph).returns @graph end it "should recurse if recursion is enabled" do resource = stub('resource', :[] => 'resource') file.expects(:recurse).returns [resource] file[:recurse] = true file.eval_generate.should == [resource] end it "should not recurse if recursion is disabled" do file.expects(:recurse).never file[:recurse] = false file.eval_generate.should == [] end end describe "#ancestors" do it "should return the ancestors of the file, in ascending order" do file = described_class.new(:path => make_absolute("/tmp/foo/bar/baz/qux")) pieces = %W[#{make_absolute('/')} tmp foo bar baz] ancestors = file.ancestors ancestors.should_not be_empty ancestors.reverse.each_with_index do |path,i| path.should == File.join(*pieces[0..i]) end end end describe "#flush" do it "should flush all properties that respond to :flush" do file[:source] = File.expand_path(__FILE__) file.parameter(:source).expects(:flush) file.flush end it "should reset its stat reference" do FileUtils.touch(path) stat1 = file.stat file.stat.should equal(stat1) file.flush file.stat.should_not equal(stat1) end end describe "#initialize" do it "should remove a trailing slash from the title to create the path" do title = File.expand_path("/abc/\n\tdef/") file = described_class.new(:title => title) file[:path].should == title end it "should set a desired 'ensure' value if none is set and 'content' is set" do file = described_class.new(:path => path, :content => "/foo/bar") file[:ensure].should == :file end it "should set a desired 'ensure' value if none is set and 'target' is set", :if => described_class.defaultprovider.feature?(:manages_symlinks) do file = described_class.new(:path => path, :target => File.expand_path(__FILE__)) file[:ensure].should == :link end end describe "#mark_children_for_purging" do it "should set each child's ensure to absent" do paths = %w[foo bar baz] children = paths.inject({}) do |children,child| children.merge child => described_class.new(:path => File.join(path, child), :ensure => :present) end file.mark_children_for_purging(children) children.length.should == 3 children.values.each do |child| child[:ensure].should == :absent end end it "should skip children which have a source" do child = described_class.new(:path => path, :ensure => :present, :source => File.expand_path(__FILE__)) file.mark_children_for_purging('foo' => child) child[:ensure].should == :present end end describe "#newchild" do it "should create a new resource relative to the parent" do child = file.newchild('bar') child.must be_a(described_class) child[:path].should == File.join(file[:path], 'bar') end { :ensure => :present, :recurse => true, :recurselimit => 5, :target => "some_target", :source => File.expand_path("some_source"), }.each do |param, value| it "should omit the #{param} parameter", :if => described_class.defaultprovider.feature?(:manages_symlinks) do # Make a new file, because we have to set the param at initialization # or it wouldn't be copied regardless. file = described_class.new(:path => path, param => value) child = file.newchild('bar') child[param].should_not == value end end it "should copy all of the parent resource's 'should' values that were set at initialization" do parent = described_class.new(:path => path, :owner => 'root', :group => 'wheel') child = parent.newchild("my/path") child[:owner].should == 'root' child[:group].should == 'wheel' end it "should not copy default values to the new child" do child = file.newchild("my/path") child.original_parameters.should_not include(:backup) end it "should not copy values to the child which were set by the source" do source = File.expand_path(__FILE__) file[:source] = source metadata = stub 'metadata', :owner => "root", :group => "root", :mode => 0755, :ftype => "file", :checksum => "{md5}whatever", :source => source file.parameter(:source).stubs(:metadata).returns metadata file.parameter(:source).copy_source_values file.class.expects(:new).with { |params| params[:group].nil? } file.newchild("my/path") end end describe "#purge?" do it "should return false if purge is not set" do file.must_not be_purge end it "should return true if purge is set to true" do file[:purge] = true file.must be_purge end it "should return false if purge is set to false" do file[:purge] = false file.must_not be_purge end end describe "#recurse" do before do file[:recurse] = true @metadata = Puppet::FileServing::Metadata end describe "and a source is set" do it "should pass the already-discovered resources to recurse_remote" do file[:source] = File.expand_path(__FILE__) file.stubs(:recurse_local).returns(:foo => "bar") file.expects(:recurse_remote).with(:foo => "bar").returns [] file.recurse end end describe "and a target is set" do it "should use recurse_link" do file[:target] = File.expand_path(__FILE__) file.stubs(:recurse_local).returns(:foo => "bar") file.expects(:recurse_link).with(:foo => "bar").returns [] file.recurse end end it "should use recurse_local if recurse is not remote" do file.expects(:recurse_local).returns({}) file.recurse end it "should not use recurse_local if recurse is remote" do file[:recurse] = :remote file.expects(:recurse_local).never file.recurse end it "should return the generated resources as an array sorted by file path" do one = stub 'one', :[] => "/one" two = stub 'two', :[] => "/one/two" three = stub 'three', :[] => "/three" file.expects(:recurse_local).returns(:one => one, :two => two, :three => three) file.recurse.should == [one, two, three] end describe "and purging is enabled" do before do file[:purge] = true end it "should mark each file for removal" do local = described_class.new(:path => path, :ensure => :present) file.expects(:recurse_local).returns("local" => local) file.recurse local[:ensure].should == :absent end it "should not remove files that exist in the remote repository" do file[:source] = File.expand_path(__FILE__) file.expects(:recurse_local).returns({}) remote = described_class.new(:path => path, :source => File.expand_path(__FILE__), :ensure => :present) file.expects(:recurse_remote).with { |hash| hash["remote"] = remote } file.recurse remote[:ensure].should_not == :absent end end end describe "#remove_less_specific_files" do it "should remove any nested files that are already in the catalog" do foo = described_class.new :path => File.join(file[:path], 'foo') bar = described_class.new :path => File.join(file[:path], 'bar') baz = described_class.new :path => File.join(file[:path], 'baz') catalog.add_resource(foo) catalog.add_resource(bar) file.remove_less_specific_files([foo, bar, baz]).should == [baz] end end describe "#remove_less_specific_files" do it "should remove any nested files that are already in the catalog" do foo = described_class.new :path => File.join(file[:path], 'foo') bar = described_class.new :path => File.join(file[:path], 'bar') baz = described_class.new :path => File.join(file[:path], 'baz') catalog.add_resource(foo) catalog.add_resource(bar) file.remove_less_specific_files([foo, bar, baz]).should == [baz] end end describe "#recurse?" do it "should be true if recurse is true" do file[:recurse] = true file.must be_recurse end it "should be true if recurse is remote" do file[:recurse] = :remote file.must be_recurse end it "should be false if recurse is false" do file[:recurse] = false file.must_not be_recurse end end describe "#recurse_link" do before do @first = stub 'first', :relative_path => "first", :full_path => "/my/first", :ftype => "directory" @second = stub 'second', :relative_path => "second", :full_path => "/my/second", :ftype => "file" @resource = stub 'file', :[]= => nil end it "should pass its target to the :perform_recursion method" do file[:target] = "mylinks" file.expects(:perform_recursion).with("mylinks").returns [@first] file.stubs(:newchild).returns @resource file.recurse_link({}) end it "should ignore the recursively-found '.' file and configure the top-level file to create a directory" do @first.stubs(:relative_path).returns "." file[:target] = "mylinks" file.expects(:perform_recursion).with("mylinks").returns [@first] file.stubs(:newchild).never file.expects(:[]=).with(:ensure, :directory) file.recurse_link({}) end it "should create a new child resource for each generated metadata instance's relative path that doesn't already exist in the children hash" do file.expects(:perform_recursion).returns [@first, @second] file.expects(:newchild).with(@first.relative_path).returns @resource file.recurse_link("second" => @resource) end it "should not create a new child resource for paths that already exist in the children hash" do file.expects(:perform_recursion).returns [@first] file.expects(:newchild).never file.recurse_link("first" => @resource) end it "should set the target to the full path of discovered file and set :ensure to :link if the file is not a directory", :if => described_class.defaultprovider.feature?(:manages_symlinks) do file.stubs(:perform_recursion).returns [@first, @second] file.recurse_link("first" => @resource, "second" => file) file[:ensure].should == :link file[:target].should == "/my/second" end it "should :ensure to :directory if the file is a directory" do file.stubs(:perform_recursion).returns [@first, @second] file.recurse_link("first" => file, "second" => @resource) file[:ensure].should == :directory end it "should return a hash with both created and existing resources with the relative paths as the hash keys" do file.expects(:perform_recursion).returns [@first, @second] file.stubs(:newchild).returns file file.recurse_link("second" => @resource).should == {"second" => @resource, "first" => file} end end describe "#recurse_local" do before do @metadata = stub 'metadata', :relative_path => "my/file" end it "should pass its path to the :perform_recursion method" do file.expects(:perform_recursion).with(file[:path]).returns [@metadata] file.stubs(:newchild) file.recurse_local end it "should return an empty hash if the recursion returns nothing" do file.expects(:perform_recursion).returns nil file.recurse_local.should == {} end it "should create a new child resource with each generated metadata instance's relative path" do file.expects(:perform_recursion).returns [@metadata] file.expects(:newchild).with(@metadata.relative_path).returns "fiebar" file.recurse_local end it "should not create a new child resource for the '.' directory" do @metadata.stubs(:relative_path).returns "." file.expects(:perform_recursion).returns [@metadata] file.expects(:newchild).never file.recurse_local end it "should return a hash of the created resources with the relative paths as the hash keys" do file.expects(:perform_recursion).returns [@metadata] file.expects(:newchild).with("my/file").returns "fiebar" file.recurse_local.should == {"my/file" => "fiebar"} end it "should set checksum_type to none if this file checksum is none" do file[:checksum] = :none Puppet::FileServing::Metadata.indirection.expects(:search).with { |path,params| params[:checksum_type] == :none }.returns [@metadata] file.expects(:newchild).with("my/file").returns "fiebar" file.recurse_local end end describe "#recurse_remote" do let(:my) { File.expand_path('/my') } before do file[:source] = "puppet://foo/bar" @first = Puppet::FileServing::Metadata.new(my, :relative_path => "first") @second = Puppet::FileServing::Metadata.new(my, :relative_path => "second") @first.stubs(:ftype).returns "directory" @second.stubs(:ftype).returns "directory" @parameter = stub 'property', :metadata= => nil @resource = stub 'file', :[]= => nil, :parameter => @parameter end it "should pass its source to the :perform_recursion method" do data = Puppet::FileServing::Metadata.new(File.expand_path("/whatever"), :relative_path => "foobar") file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] file.stubs(:newchild).returns @resource file.recurse_remote({}) end it "should not recurse when the remote file is not a directory" do data = Puppet::FileServing::Metadata.new(File.expand_path("/whatever"), :relative_path => ".") data.stubs(:ftype).returns "file" file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] file.expects(:newchild).never file.recurse_remote({}) end it "should set the source of each returned file to the searched-for URI plus the found relative path" do @first.expects(:source=).with File.join("puppet://foo/bar", @first.relative_path) file.expects(:perform_recursion).returns [@first] file.stubs(:newchild).returns @resource file.recurse_remote({}) end it "should create a new resource for any relative file paths that do not already have a resource" do file.stubs(:perform_recursion).returns [@first] file.expects(:newchild).with("first").returns @resource file.recurse_remote({}).should == {"first" => @resource} end it "should not create a new resource for any relative file paths that do already have a resource" do file.stubs(:perform_recursion).returns [@first] file.expects(:newchild).never file.recurse_remote("first" => @resource) end it "should set the source of each resource to the source of the metadata" do file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:source, File.join("puppet://foo/bar", @first.relative_path)) file.recurse_remote("first" => @resource) end # LAK:FIXME This is a bug, but I can't think of a fix for it. Fortunately it's already # filed, and when it's fixed, we'll just fix the whole flow. it "should set the checksum type to :md5 if the remote file is a file" do @first.stubs(:ftype).returns "file" file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:checksum, :md5) file.recurse_remote("first" => @resource) end it "should store the metadata in the source property for each resource so the source does not have to requery the metadata" do file.stubs(:perform_recursion).returns [@first] @resource.expects(:parameter).with(:source).returns @parameter @parameter.expects(:metadata=).with(@first) file.recurse_remote("first" => @resource) end it "should not create a new resource for the '.' file" do @first.stubs(:relative_path).returns "." file.stubs(:perform_recursion).returns [@first] file.expects(:newchild).never file.recurse_remote({}) end it "should store the metadata in the main file's source property if the relative path is '.'" do @first.stubs(:relative_path).returns "." file.stubs(:perform_recursion).returns [@first] file.parameter(:source).expects(:metadata=).with @first file.recurse_remote("first" => @resource) end describe "and multiple sources are provided" do let(:sources) do h = {} %w{/a /b /c /d}.each do |key| h[key] = URI.unescape(Puppet::Util.path_to_uri(File.expand_path(key)).to_s) end h end describe "and :sourceselect is set to :first" do it "should create file instances for the results for the first source to return any values" do data = Puppet::FileServing::Metadata.new(File.expand_path("/whatever"), :relative_path => "foobar") file[:source] = sources.keys.sort.map { |key| File.expand_path(key) } file.expects(:perform_recursion).with(sources['/a']).returns nil file.expects(:perform_recursion).with(sources['/b']).returns [] file.expects(:perform_recursion).with(sources['/c']).returns [data] file.expects(:perform_recursion).with(sources['/d']).never file.expects(:newchild).with("foobar").returns @resource file.recurse_remote({}) end end describe "and :sourceselect is set to :all" do before do file[:sourceselect] = :all end it "should return every found file that is not in a previous source" do klass = Puppet::FileServing::Metadata file[:source] = abs_path = %w{/a /b /c /d}.map {|f| File.expand_path(f) } file.stubs(:newchild).returns @resource one = [klass.new(abs_path[0], :relative_path => "a")] file.expects(:perform_recursion).with(sources['/a']).returns one file.expects(:newchild).with("a").returns @resource two = [klass.new(abs_path[1], :relative_path => "a"), klass.new(abs_path[1], :relative_path => "b")] file.expects(:perform_recursion).with(sources['/b']).returns two file.expects(:newchild).with("b").returns @resource three = [klass.new(abs_path[2], :relative_path => "a"), klass.new(abs_path[2], :relative_path => "c")] file.expects(:perform_recursion).with(sources['/c']).returns three file.expects(:newchild).with("c").returns @resource file.expects(:perform_recursion).with(sources['/d']).returns [] file.recurse_remote({}) end end end end describe "#perform_recursion" do it "should use Metadata to do its recursion" do Puppet::FileServing::Metadata.indirection.expects(:search) file.perform_recursion(file[:path]) end it "should use the provided path as the key to the search" do Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| key == "/foo" } file.perform_recursion("/foo") end it "should return the results of the metadata search" do Puppet::FileServing::Metadata.indirection.expects(:search).returns "foobar" file.perform_recursion(file[:path]).should == "foobar" end it "should pass its recursion value to the search" do file[:recurse] = true Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true } file.perform_recursion(file[:path]) end it "should pass true if recursion is remote" do file[:recurse] = :remote Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true } file.perform_recursion(file[:path]) end it "should pass its recursion limit value to the search" do file[:recurselimit] = 10 Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurselimit] == 10 } file.perform_recursion(file[:path]) end it "should configure the search to ignore or manage links" do file[:links] = :manage Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:links] == :manage } file.perform_recursion(file[:path]) end it "should pass its 'ignore' setting to the search if it has one" do file[:ignore] = %w{.svn CVS} Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:ignore] == %w{.svn CVS} } file.perform_recursion(file[:path]) end end describe "#remove_existing" do it "should do nothing if the file doesn't exist" do file.remove_existing(:file).should == false end it "should fail if it can't backup the file" do file.stubs(:stat).returns stub('stat', :ftype => 'file') file.stubs(:perform_backup).returns false expect { file.remove_existing(:file) }.to raise_error(Puppet::Error, /Could not back up; will not replace/) end describe "backing up directories" do it "should not backup directories if force is false" do file[:force] = false file.stubs(:stat).returns stub('stat', :ftype => 'directory') file.expects(:perform_backup).never file.remove_existing(:file).should == false end it "should backup directories if force is true" do file[:force] = true FileUtils.expects(:rmtree).with(file[:path]) file.stubs(:stat).returns stub('stat', :ftype => 'directory') file.expects(:perform_backup).once.returns(true) file.remove_existing(:file).should == true end end it "should not do anything if the file is already the right type and not a link" do file.stubs(:stat).returns stub('stat', :ftype => 'file') file.remove_existing(:file).should == false end it "should not remove directories and should not invalidate the stat unless force is set" do # Actually call stat to set @needs_stat to nil file.stat file.stubs(:stat).returns stub('stat', :ftype => 'directory') file.remove_existing(:file) file.instance_variable_get(:@stat).should == nil @logs.should be_any {|log| log.level == :notice and log.message =~ /Not removing directory; use 'force' to override/} end it "should remove a directory if force is set" do file[:force] = true file.stubs(:stat).returns stub('stat', :ftype => 'directory') FileUtils.expects(:rmtree).with(file[:path]) file.remove_existing(:file).should == true end it "should remove an existing file" do file.stubs(:perform_backup).returns true FileUtils.touch(path) file.remove_existing(:directory).should == true Puppet::FileSystem::File.exist?(file[:path]).should == false end it "should remove an existing link", :if => described_class.defaultprovider.feature?(:manages_symlinks) do file.stubs(:perform_backup).returns true target = tmpfile('link_target') FileUtils.touch(target) Puppet::FileSystem::File.new(target).symlink(path) file[:target] = target file.remove_existing(:directory).should == true Puppet::FileSystem::File.exist?(file[:path]).should == false end it "should fail if the file is not a file, link, or directory" do file.stubs(:stat).returns stub('stat', :ftype => 'socket') expect { file.remove_existing(:file) }.to raise_error(Puppet::Error, /Could not back up files of type socket/) end it "should invalidate the existing stat of the file" do # Actually call stat to set @needs_stat to nil file.stat file.stubs(:stat).returns stub('stat', :ftype => 'file') Puppet::FileSystem::File.stubs(:unlink) file.remove_existing(:directory).should == true file.instance_variable_get(:@stat).should == :needs_stat end end describe "#retrieve" do it "should copy the source values if the 'source' parameter is set" do file[:source] = File.expand_path('/foo/bar') file.parameter(:source).expects(:copy_source_values) file.retrieve end end describe "#should_be_file?" do it "should have a method for determining if the file should be a normal file" do file.must respond_to(:should_be_file?) end it "should be a file if :ensure is set to :file" do file[:ensure] = :file file.must be_should_be_file end it "should be a file if :ensure is set to :present and the file exists as a normal file" do file.stubs(:stat).returns(mock('stat', :ftype => "file")) file[:ensure] = :present file.must be_should_be_file end it "should not be a file if :ensure is set to something other than :file" do file[:ensure] = :directory file.must_not be_should_be_file end it "should not be a file if :ensure is set to :present and the file exists but is not a normal file" do file.stubs(:stat).returns(mock('stat', :ftype => "directory")) file[:ensure] = :present file.must_not be_should_be_file end it "should be a file if :ensure is not set and :content is" do file[:content] = "foo" file.must be_should_be_file end it "should be a file if neither :ensure nor :content is set but the file exists as a normal file" do file.stubs(:stat).returns(mock("stat", :ftype => "file")) file.must be_should_be_file end it "should not be a file if neither :ensure nor :content is set but the file exists but not as a normal file" do file.stubs(:stat).returns(mock("stat", :ftype => "directory")) file.must_not be_should_be_file end end describe "#stat", :if => described_class.defaultprovider.feature?(:manages_symlinks) do before do target = tmpfile('link_target') FileUtils.touch(target) Puppet::FileSystem::File.new(target).symlink(path) file[:target] = target file[:links] = :manage # so we always use :lstat end it "should stat the target if it is following links" do file[:links] = :follow file.stat.ftype.should == 'file' end it "should stat the link if is it not following links" do file[:links] = :manage file.stat.ftype.should == 'link' end it "should return nil if the file does not exist" do file[:path] = make_absolute('/foo/bar/baz/non-existent') file.stat.should be_nil end it "should return nil if the file cannot be stat'ed" do dir = tmpfile('link_test_dir') child = File.join(dir, 'some_file') Dir.mkdir(dir) File.chmod(0, dir) file[:path] = child file.stat.should be_nil # chmod it back so we can clean it up File.chmod(0777, dir) end it "should return nil if parts of path are no directories" do regular_file = tmpfile('ENOTDIR_test') FileUtils.touch(regular_file) impossible_child = File.join(regular_file, 'some_file') file[:path] = impossible_child file.stat.should be_nil end it "should return the stat instance" do file.stat.should be_a(File::Stat) end it "should cache the stat instance" do file.stat.should equal(file.stat) end end describe "#write" do it "should propagate failures encountered when renaming the temporary file" do File.stubs(:open) File.expects(:rename).raises ArgumentError file[:backup] = 'puppet' file.stubs(:validate_checksum?).returns(false) property = stub('content_property', :actual_content => "something", :length => "something".length) file.stubs(:property).with(:content).returns(property) expect { file.write(:content) }.to raise_error(Puppet::Error) end it "should delegate writing to the content property" do filehandle = stub_everything 'fh' File.stubs(:open).yields(filehandle) File.stubs(:rename) property = stub('content_property', :actual_content => "something", :length => "something".length) file[:backup] = 'puppet' file.stubs(:validate_checksum?).returns(false) file.stubs(:property).with(:content).returns(property) property.expects(:write).with(filehandle) file.write(:content) end describe "when validating the checksum" do before { file.stubs(:validate_checksum?).returns(true) } it "should fail if the checksum parameter and content checksums do not match" do checksum = stub('checksum_parameter', :sum => 'checksum_b', :sum_file => 'checksum_b') file.stubs(:parameter).with(:checksum).returns(checksum) property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') file.stubs(:property).with(:content).returns(property) expect { file.write :NOTUSED }.to raise_error(Puppet::Error) end end describe "when not validating the checksum" do before { file.stubs(:validate_checksum?).returns(false) } it "should not fail if the checksum property and content checksums do not match" do checksum = stub('checksum_parameter', :sum => 'checksum_b') file.stubs(:parameter).with(:checksum).returns(checksum) property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') file.stubs(:property).with(:content).returns(property) expect { file.write :NOTUSED }.to_not raise_error end end end describe "#fail_if_checksum_is_wrong" do it "should fail if the checksum of the file doesn't match the expected one" do expect do file.instance_eval do parameter(:checksum).stubs(:sum_file).returns('wrong!!') fail_if_checksum_is_wrong(self[:path], 'anything!') end end.to raise_error(Puppet::Error, /File written to disk did not match checksum/) end it "should not fail if the checksum is correct" do file.instance_eval do parameter(:checksum).stubs(:sum_file).returns('anything!') fail_if_checksum_is_wrong(self[:path], 'anything!').should == nil end end it "should not fail if the checksum is absent" do file.instance_eval do parameter(:checksum).stubs(:sum_file).returns(nil) fail_if_checksum_is_wrong(self[:path], 'anything!').should == nil end end end describe "#write_content" do it "should delegate writing the file to the content property" do io = stub('io') file[:content] = "some content here" file.property(:content).expects(:write).with(io) file.send(:write_content, io) end end describe "#write_temporary_file?" do it "should be true if the file has specified content" do file[:content] = 'some content' file.send(:write_temporary_file?).should be_true end it "should be true if the file has specified source" do file[:source] = File.expand_path('/tmp/foo') file.send(:write_temporary_file?).should be_true end it "should be false if the file has neither content nor source" do file.send(:write_temporary_file?).should be_false end end describe "#property_fix" do { :mode => 0777, :owner => 'joeuser', :group => 'joeusers', :seluser => 'seluser', :selrole => 'selrole', :seltype => 'seltype', :selrange => 'selrange' }.each do |name,value| it "should sync the #{name} property if it's not in sync" do file[name] = value prop = file.property(name) prop.expects(:retrieve) prop.expects(:safe_insync?).returns false prop.expects(:sync) file.send(:property_fix) end end end describe "when autorequiring" do describe "target" do it "should require file resource when specified with the target property", :if => described_class.defaultprovider.feature?(:manages_symlinks) do file = described_class.new(:path => File.expand_path("/foo"), :ensure => :directory) link = described_class.new(:path => File.expand_path("/bar"), :ensure => :link, :target => File.expand_path("/foo")) catalog.add_resource file catalog.add_resource link reqs = link.autorequire reqs.size.must == 1 reqs[0].source.must == file reqs[0].target.must == link end it "should require file resource when specified with the ensure property" do file = described_class.new(:path => File.expand_path("/foo"), :ensure => :directory) link = described_class.new(:path => File.expand_path("/bar"), :ensure => File.expand_path("/foo")) catalog.add_resource file catalog.add_resource link reqs = link.autorequire reqs.size.must == 1 reqs[0].source.must == file reqs[0].target.must == link end it "should not require target if target is not managed", :if => described_class.defaultprovider.feature?(:manages_symlinks) do link = described_class.new(:path => File.expand_path('/foo'), :ensure => :link, :target => '/bar') catalog.add_resource link link.autorequire.size.should == 0 end end describe "directories" do it "should autorequire its parent directory" do dir = described_class.new(:path => File.dirname(path)) catalog.add_resource file catalog.add_resource dir reqs = file.autorequire reqs[0].source.must == dir reqs[0].target.must == file end it "should autorequire its nearest ancestor directory" do dir = described_class.new(:path => File.dirname(path)) grandparent = described_class.new(:path => File.dirname(File.dirname(path))) catalog.add_resource file catalog.add_resource dir catalog.add_resource grandparent reqs = file.autorequire reqs.length.must == 1 reqs[0].source.must == dir reqs[0].target.must == file end it "should not autorequire anything when there is no nearest ancestor directory" do catalog.add_resource file file.autorequire.should be_empty end it "should not autorequire its parent dir if its parent dir is itself" do file[:path] = File.expand_path('/') catalog.add_resource file file.autorequire.should be_empty end describe "on Windows systems", :if => Puppet.features.microsoft_windows? do describe "when using UNC filenames" do it "should autorequire its parent directory" do - file[:path] = '//server/foo/bar/baz' - dir = described_class.new(:path => "//server/foo/bar") + file[:path] = '//localhost/foo/bar/baz' + dir = described_class.new(:path => "//localhost/foo/bar") catalog.add_resource file catalog.add_resource dir reqs = file.autorequire reqs[0].source.must == dir reqs[0].target.must == file end it "should autorequire its nearest ancestor directory" do - file = described_class.new(:path => "//server/foo/bar/baz/qux") - dir = described_class.new(:path => "//server/foo/bar/baz") - grandparent = described_class.new(:path => "//server/foo/bar") + file = described_class.new(:path => "//localhost/foo/bar/baz/qux") + dir = described_class.new(:path => "//localhost/foo/bar/baz") + grandparent = described_class.new(:path => "//localhost/foo/bar") catalog.add_resource file catalog.add_resource dir catalog.add_resource grandparent reqs = file.autorequire reqs.length.must == 1 reqs[0].source.must == dir reqs[0].target.must == file end it "should not autorequire anything when there is no nearest ancestor directory" do - file = described_class.new(:path => "//server/foo/bar/baz/qux") + file = described_class.new(:path => "//localhost/foo/bar/baz/qux") catalog.add_resource file file.autorequire.should be_empty end it "should not autorequire its parent dir if its parent dir is itself" do - file = described_class.new(:path => "//server/foo") + file = described_class.new(:path => "//localhost/foo") catalog.add_resource file puts file.autorequire file.autorequire.should be_empty end end end end end describe "when managing links", :if => Puppet.features.manages_symlinks? do require 'tempfile' before :each do Dir.mkdir(path) @target = File.join(path, "target") @link = File.join(path, "link") target = described_class.new( :ensure => :file, :path => @target, :catalog => catalog, :content => 'yayness', :mode => 0644) catalog.add_resource target @link_resource = described_class.new( :ensure => :link, :path => @link, :target => @target, :catalog => catalog, :mode => 0755) catalog.add_resource @link_resource # to prevent the catalog from trying to write state.yaml Puppet::Util::Storage.stubs(:store) end it "should preserve the original file mode and ignore the one set by the link" do @link_resource[:links] = :manage # default catalog.apply # I convert them to strings so they display correctly if there's an error. (Puppet::FileSystem::File.new(@target).stat.mode & 007777).to_s(8).should == '644' end it "should manage the mode of the followed link" do pending("Windows cannot presently manage the mode when following symlinks", :if => Puppet.features.microsoft_windows?) do @link_resource[:links] = :follow catalog.apply (Puppet::FileSystem::File.new(@target).stat.mode & 007777).to_s(8).should == '755' end end end describe "when using source" do before do file[:source] = File.expand_path('/one') end Puppet::Type::File::ParameterChecksum.value_collection.values.reject {|v| v == :none}.each do |checksum_type| describe "with checksum '#{checksum_type}'" do before do file[:checksum] = checksum_type end it 'should validate' do expect { file.validate }.to_not raise_error end end end describe "with checksum 'none'" do before do file[:checksum] = :none end it 'should raise an exception when validating' do expect { file.validate }.to raise_error(/You cannot specify source when using checksum 'none'/) end end end describe "when using content" do before do file[:content] = 'file contents' end (Puppet::Type::File::ParameterChecksum.value_collection.values - SOURCE_ONLY_CHECKSUMS).each do |checksum_type| describe "with checksum '#{checksum_type}'" do before do file[:checksum] = checksum_type end it 'should validate' do expect { file.validate }.to_not raise_error end end end SOURCE_ONLY_CHECKSUMS.each do |checksum_type| describe "with checksum '#{checksum_type}'" do it 'should raise an exception when validating' do file[:checksum] = checksum_type expect { file.validate }.to raise_error(/You cannot specify content when using checksum '#{checksum_type}'/) end end end end describe "when auditing" do before :each do # to prevent the catalog from trying to write state.yaml Puppet::Util::Storage.stubs(:store) end it "should not fail if creating a new file if group is not set" do file = described_class.new(:path => path, :audit => 'all', :content => 'content') catalog.add_resource(file) report = catalog.apply.report report.resource_statuses["File[#{path}]"].should_not be_failed File.read(path).should == 'content' end it "should not log errors if creating a new file with ensure present and no content" do file[:audit] = 'content' file[:ensure] = 'present' catalog.add_resource(file) catalog.apply Puppet::FileSystem::File.exist?(path).should be_true @logs.should_not be_any {|l| l.level != :notice } end end describe "when specifying both source and checksum" do it 'should use the specified checksum when source is first' do file[:source] = File.expand_path('/foo') file[:checksum] = :md5lite file[:checksum].should == :md5lite end it 'should use the specified checksum when source is last' do file[:checksum] = :md5lite file[:source] = File.expand_path('/foo') file[:checksum].should == :md5lite end end describe "when validating" do [[:source, :target], [:source, :content], [:target, :content]].each do |prop1,prop2| it "should fail if both #{prop1} and #{prop2} are specified" do file[prop1] = prop1 == :source ? File.expand_path("prop1 value") : "prop1 value" file[prop2] = "prop2 value" expect do file.validate end.to raise_error(Puppet::Error, /You cannot specify more than one of/) end end end end diff --git a/spec/unit/util/windows/access_control_entry_spec.rb b/spec/unit/util/windows/access_control_entry_spec.rb new file mode 100644 index 000000000..b139b0d42 --- /dev/null +++ b/spec/unit/util/windows/access_control_entry_spec.rb @@ -0,0 +1,67 @@ +#!/usr/bin/env ruby +require 'spec_helper' +require 'puppet/util/windows' + +describe "Puppet::Util::Windows::AccessControlEntry", :if => Puppet.features.microsoft_windows? do + let(:klass) { Puppet::Util::Windows::AccessControlEntry } + let(:sid) { 'S-1-5-18' } + let(:mask) { Windows::File::FILE_ALL_ACCESS } + + it "creates an access allowed ace" do + ace = klass.new(sid, mask) + + ace.type.should == klass::ACCESS_ALLOWED_ACE_TYPE + end + + it "creates an access denied ace" do + ace = klass.new(sid, mask, 0, klass::ACCESS_DENIED_ACE_TYPE) + + ace.type.should == klass::ACCESS_DENIED_ACE_TYPE + end + + it "creates a non-inherited ace by default" do + ace = klass.new(sid, mask) + + ace.should_not be_inherited + end + + it "creates an inherited ace" do + ace = klass.new(sid, mask, klass::INHERITED_ACE) + + ace.should be_inherited + end + + it "creates a non-inherit-only ace by default" do + ace = klass.new(sid, mask) + + ace.should_not be_inherit_only + end + + it "creates an inherit-only ace" do + ace = klass.new(sid, mask, klass::INHERIT_ONLY_ACE) + + ace.should be_inherit_only + end + + context "when comparing aces" do + let(:ace1) { klass.new(sid, mask, klass::INHERIT_ONLY_ACE, klass::ACCESS_DENIED_ACE_TYPE) } + let(:ace2) { klass.new(sid, mask, klass::INHERIT_ONLY_ACE, klass::ACCESS_DENIED_ACE_TYPE) } + + it "returns true if different objects have the same set of values" do + ace1.should == ace2 + end + + it "returns false if different objects have different sets of values" do + ace = klass.new(sid, mask) + ace.should_not == ace1 + end + + it "returns true when testing if two objects are eql?" do + ace1.eql?(ace2) + end + + it "returns false when comparing object identity" do + ace1.should_not be_equal(ace2) + end + end +end diff --git a/spec/unit/util/windows/access_control_list_spec.rb b/spec/unit/util/windows/access_control_list_spec.rb new file mode 100644 index 000000000..66c917b29 --- /dev/null +++ b/spec/unit/util/windows/access_control_list_spec.rb @@ -0,0 +1,133 @@ +#!/usr/bin/env ruby +require 'spec_helper' +require 'puppet/util/windows' + +describe "Puppet::Util::Windows::AccessControlList", :if => Puppet.features.microsoft_windows? do + let(:klass) { Puppet::Util::Windows::AccessControlList } + let(:system_sid) { 'S-1-5-18' } + let(:admins_sid) { 'S-1-5-544' } + let(:none_sid) { 'S-1-0-0' } + + let(:system_ace) do + Puppet::Util::Windows::AccessControlEntry.new(system_sid, 0x1) + end + let(:admins_ace) do + Puppet::Util::Windows::AccessControlEntry.new(admins_sid, 0x2) + end + let(:none_ace) do + Puppet::Util::Windows::AccessControlEntry.new(none_sid, 0x3) + end + + it "constructs an empty list" do + acl = klass.new + + acl.to_a.should be_empty + end + + it "supports copy constructor" do + aces = klass.new([system_ace]).to_a + + aces.to_a.should == [system_ace] + end + + context "appending" do + it "appends an allow ace" do + acl = klass.new + acl.allow(system_sid, 0x1, 0x2) + + acl.first.type.should == klass::ACCESS_ALLOWED_ACE_TYPE + end + + it "appends a deny ace" do + acl = klass.new + acl.deny(system_sid, 0x1, 0x2) + + acl.first.type.should == klass::ACCESS_DENIED_ACE_TYPE + end + + it "always appends, never overwrites an ACE" do + acl = klass.new([system_ace]) + acl.allow(admins_sid, admins_ace.mask, admins_ace.flags) + + aces = acl.to_a + aces.size.should == 2 + aces[0].should == system_ace + aces[1].sid.should == admins_sid + aces[1].mask.should == admins_ace.mask + aces[1].flags.should == admins_ace.flags + end + end + + context "reassigning" do + it "preserves the mask from the old sid when reassigning to the new sid" do + dacl = klass.new([system_ace]) + + dacl.reassign!(system_ace.sid, admins_ace.sid) + # we removed system, so ignore prepended ace + ace = dacl.to_a[1] + ace.sid.should == admins_sid + ace.mask.should == system_ace.mask + end + + it "matches multiple sids" do + dacl = klass.new([system_ace, system_ace]) + + dacl.reassign!(system_ace.sid, admins_ace.sid) + # we removed system, so ignore prepended ace + aces = dacl.to_a + aces.size.should == 3 + aces.to_a[1,2].each do |ace| + ace.sid.should == admins_ace.sid + end + end + + it "preserves aces for sids that don't match, in their original order" do + dacl = klass.new([system_ace, admins_ace]) + + dacl.reassign!(system_sid, none_sid) + aces = dacl.to_a + aces[1].sid == admins_ace.sid + end + + it "preserves inherited aces, even if the sids match" do + flags = Puppet::Util::Windows::AccessControlEntry::INHERITED_ACE + inherited_ace = Puppet::Util::Windows::AccessControlEntry.new(system_sid, 0x1, flags) + dacl = klass.new([inherited_ace, system_ace]) + dacl.reassign!(system_sid, none_sid) + aces = dacl.to_a + + aces[0].sid.should == system_sid + end + + it "prepends an explicit ace for the new sid with the same mask and basic inheritance as the inherited ace" do + expected_flags = + Puppet::Util::Windows::AccessControlEntry::OBJECT_INHERIT_ACE | + Puppet::Util::Windows::AccessControlEntry::CONTAINER_INHERIT_ACE | + Puppet::Util::Windows::AccessControlEntry::INHERIT_ONLY_ACE + + flags = Puppet::Util::Windows::AccessControlEntry::INHERITED_ACE | expected_flags + + inherited_ace = Puppet::Util::Windows::AccessControlEntry.new(system_sid, 0x1, flags) + dacl = klass.new([inherited_ace]) + dacl.reassign!(system_sid, none_sid) + aces = dacl.to_a + + aces.size.should == 2 + aces[0].sid.should == none_sid + aces[0].should_not be_inherited + aces[0].flags.should == expected_flags + + aces[1].sid.should == system_sid + aces[1].should be_inherited + end + + it "makes a copy of the ace prior to modifying it" do + arr = [system_ace] + + acl = klass.new(arr) + acl.reassign!(system_sid, none_sid) + + arr[0].sid.should == system_sid + end + end +end diff --git a/spec/unit/util/windows/security_descriptor_spec.rb b/spec/unit/util/windows/security_descriptor_spec.rb new file mode 100644 index 000000000..61db2f598 --- /dev/null +++ b/spec/unit/util/windows/security_descriptor_spec.rb @@ -0,0 +1,117 @@ +#!/usr/bin/env ruby + +require 'spec_helper' +require 'puppet/util/windows' + +describe "Puppet::Util::Windows::SecurityDescriptor", :if => Puppet.features.microsoft_windows? do + let(:system_sid) { Win32::Security::SID::LocalSystem } + let(:admins_sid) { Win32::Security::SID::BuiltinAdministrators } + let(:group_sid) { Win32::Security::SID::Nobody } + let(:new_sid) { 'S-1-5-32-500-1-2-3' } + + def empty_dacl + Puppet::Util::Windows::AccessControlList.new + end + + def system_ace_dacl + dacl = Puppet::Util::Windows::AccessControlList.new + dacl.allow(system_sid, 0x1) + dacl + end + + context "owner" do + it "changes the owner" do + sd = Puppet::Util::Windows::SecurityDescriptor.new(system_sid, group_sid, system_ace_dacl) + sd.owner = new_sid + + sd.owner.should == new_sid + end + + it "performs a noop if the new owner is the same as the old one" do + dacl = system_ace_dacl + sd = Puppet::Util::Windows::SecurityDescriptor.new(system_sid, group_sid, dacl) + sd.owner = sd.owner + + sd.dacl.object_id.should == dacl.object_id + end + + it "prepends SYSTEM when security descriptor owner is no longer SYSTEM" do + sd = Puppet::Util::Windows::SecurityDescriptor.new(system_sid, group_sid, system_ace_dacl) + sd.owner = new_sid + + aces = sd.dacl.to_a + aces.size.should == 2 + aces[0].sid.should == system_sid + aces[1].sid.should == new_sid + end + + it "does not prepend SYSTEM when DACL already contains inherited SYSTEM ace" do + sd = Puppet::Util::Windows::SecurityDescriptor.new(admins_sid, system_sid, empty_dacl) + sd.dacl.allow(admins_sid, 0x1) + sd.dacl.allow(system_sid, 0x1, Puppet::Util::Windows::AccessControlEntry::INHERITED_ACE) + sd.owner = new_sid + + aces = sd.dacl.to_a + aces.size.should == 2 + aces[0].sid.should == new_sid + end + + it "does not prepend SYSTEM when security descriptor owner wasn't SYSTEM" do + sd = Puppet::Util::Windows::SecurityDescriptor.new(group_sid, group_sid, empty_dacl) + sd.dacl.allow(group_sid, 0x1) + sd.owner = new_sid + + aces = sd.dacl.to_a + aces.size.should == 1 + aces[0].sid.should == new_sid + end + end + + context "group" do + it "changes the group" do + sd = Puppet::Util::Windows::SecurityDescriptor.new(system_sid, group_sid, system_ace_dacl) + sd.group = new_sid + + sd.group.should == new_sid + end + + it "performs a noop if the new group is the same as the old one" do + dacl = system_ace_dacl + sd = Puppet::Util::Windows::SecurityDescriptor.new(system_sid, group_sid, dacl) + sd.group = sd.group + + sd.dacl.object_id.should == dacl.object_id + end + + it "prepends SYSTEM when security descriptor group is no longer SYSTEM" do + sd = Puppet::Util::Windows::SecurityDescriptor.new(new_sid, system_sid, system_ace_dacl) + sd.group = new_sid + + aces = sd.dacl.to_a + aces.size.should == 2 + aces[0].sid.should == system_sid + aces[1].sid.should == new_sid + end + + it "does not prepend SYSTEM when DACL already contains inherited SYSTEM ace" do + sd = Puppet::Util::Windows::SecurityDescriptor.new(admins_sid, admins_sid, empty_dacl) + sd.dacl.allow(admins_sid, 0x1) + sd.dacl.allow(system_sid, 0x1, Puppet::Util::Windows::AccessControlEntry::INHERITED_ACE) + sd.group = new_sid + + aces = sd.dacl.to_a + aces.size.should == 2 + aces[0].sid.should == new_sid + end + + it "does not prepend SYSTEM when security descriptor group wasn't SYSTEM" do + sd = Puppet::Util::Windows::SecurityDescriptor.new(group_sid, group_sid, empty_dacl) + sd.dacl.allow(group_sid, 0x1) + sd.group = new_sid + + aces = sd.dacl.to_a + aces.size.should == 1 + aces[0].sid.should == new_sid + end + end +end