diff --git a/lib/puppet/file_serving/metadata.rb b/lib/puppet/file_serving/metadata.rb index 4c863ee89..587c2196d 100644 --- a/lib/puppet/file_serving/metadata.rb +++ b/lib/puppet/file_serving/metadata.rb @@ -1,153 +1,156 @@ require 'puppet' require 'puppet/indirector' require 'puppet/file_serving' require 'puppet/file_serving/base' require 'puppet/util/checksums' require 'puppet/file_serving/indirection_hooks' # A class that handles retrieving file metadata. class Puppet::FileServing::Metadata < Puppet::FileServing::Base include Puppet::Util::Checksums extend Puppet::Indirector indirects :file_metadata, :extend => Puppet::FileServing::IndirectionHooks attr_reader :path, :owner, :group, :mode, :checksum_type, :checksum, :ftype, :destination PARAM_ORDER = [:mode, :ftype, :owner, :group] def attributes_with_tabs raise(ArgumentError, "Cannot manage files of type #{ftype}") unless ['file','directory','link'].include? ftype desc = [] PARAM_ORDER.each { |check| check = :ftype if check == :type desc << send(check) } desc << checksum desc << @destination rescue nil if ftype == 'link' desc.join("\t") end def checksum_type=(type) raise(ArgumentError, "Unsupported checksum type #{type}") unless respond_to?("#{type}_file") @checksum_type = type end class MetaStat extend Forwardable def initialize(stat) @stat = stat end def_delegator :@stat, :uid, :owner def_delegator :@stat, :gid, :group def_delegators :@stat, :mode, :ftype end class WindowsStat < MetaStat if Puppet.features.microsoft_windows? require 'puppet/util/windows/security' end def initialize(stat, path) super(stat) @path = path end - [:owner, :group, :mode].each do |method| + { :owner => 'S-1-5-32-544', + :group => 'S-1-0-0', + :mode => 0644 + }.each do |method, default_value| define_method method do - Puppet::Util::Windows::Security.send("get_#{method}", @path) + Puppet::Util::Windows::Security.send("get_#{method}", @path) || default_value end end end def collect_stat(path) stat = stat() if Puppet.features.microsoft_windows? WindowsStat.new(stat, path) else MetaStat.new(stat) end end # Retrieve the attributes for this file, relative to a base directory. # Note that File.stat raises Errno::ENOENT if the file is absent and this # method does not catch that exception. def collect real_path = full_path stat = collect_stat(real_path) @owner = stat.owner @group = stat.group @ftype = stat.ftype # We have to mask the mode, yay. @mode = stat.mode & 007777 case stat.ftype when "file" @checksum = ("{#{@checksum_type}}") + send("#{@checksum_type}_file", real_path).to_s when "directory" # Always just timestamp the directory. @checksum_type = "ctime" @checksum = ("{#{@checksum_type}}") + send("#{@checksum_type}_file", path).to_s when "link" @destination = File.readlink(real_path) @checksum = ("{#{@checksum_type}}") + send("#{@checksum_type}_file", real_path).to_s rescue nil else raise ArgumentError, "Cannot manage files of type #{stat.ftype}" end end def initialize(path,data={}) @owner = data.delete('owner') @group = data.delete('group') @mode = data.delete('mode') if checksum = data.delete('checksum') @checksum_type = checksum['type'] @checksum = checksum['value'] end @checksum_type ||= "md5" @ftype = data.delete('type') @destination = data.delete('destination') super(path,data) end PSON.register_document_type('FileMetadata',self) def to_pson_data_hash { 'document_type' => 'FileMetadata', 'data' => super['data'].update( { 'owner' => owner, 'group' => group, 'mode' => mode, 'checksum' => { 'type' => checksum_type, 'value' => checksum }, 'type' => ftype, 'destination' => destination, }), 'metadata' => { 'api_version' => 1 } } end def to_pson(*args) to_pson_data_hash.to_pson(*args) end def self.from_pson(data) new(data.delete('path'), data) end end diff --git a/lib/puppet/provider/file/windows.rb b/lib/puppet/provider/file/windows.rb index 5a7b9adf5..acf42c5d7 100644 --- a/lib/puppet/provider/file/windows.rb +++ b/lib/puppet/provider/file/windows.rb @@ -1,100 +1,101 @@ Puppet::Type.type(:file).provide :windows do desc "Uses Microsoft Windows functionality to manage file's users and rights." confine :operatingsystem => :windows include Puppet::Util::Warnings if Puppet.features.microsoft_windows? require 'puppet/util/windows' require 'puppet/util/adsi' include Puppet::Util::Windows::Security end ERROR_INVALID_SID_STRUCTURE = 1337 def id2name(id) # If it's a valid sid, get the name. Otherwise, it's already a name, so # just return it. begin if string_to_sid_ptr(id) name = nil Puppet::Util::ADSI.execquery( "SELECT Name FROM Win32_Account WHERE SID = '#{id}' AND LocalAccount = true" ).each { |a| name ||= a.name } return name end rescue Puppet::Util::Windows::Error => e raise unless e.code == ERROR_INVALID_SID_STRUCTURE end id end # Determine if the account is valid, and if so, return the UID def name2id(value) # If it's a valid sid, then return it. Else, it's a name we need to convert # to sid. begin return value if string_to_sid_ptr(value) rescue Puppet::Util::Windows::Error => e raise unless e.code == ERROR_INVALID_SID_STRUCTURE end Puppet::Util::ADSI.sid_for_account(value) rescue nil end # We use users and groups interchangeably, so use the same methods for both # (the type expects different methods, so we have to oblige). alias :uid2name :id2name alias :gid2name :id2name alias :name2gid :name2id alias :name2uid :name2id def owner return :absent unless resource.exist? get_owner(resource[:path]) end def owner=(should) begin set_owner(should, resource[:path]) rescue => detail raise Puppet::Error, "Failed to set owner to '#{should}': #{detail}" end end def group return :absent unless resource.exist? get_group(resource[:path]) end def group=(should) begin set_group(should, resource[:path]) rescue => detail raise Puppet::Error, "Failed to set group to '#{should}': #{detail}" end end def mode if resource.exist? - get_mode(resource[:path]).to_s(8) + mode = get_mode(resource[:path]) + mode ? mode.to_s(8) : :absent else :absent end end def mode=(value) begin set_mode(value.to_i(8), resource[:path]) rescue => detail error = Puppet::Error.new("failed to set mode #{mode} on #{resource[:path]}: #{detail.message}") error.set_backtrace detail.backtrace raise error end :file_changed end end diff --git a/lib/puppet/util/windows/security.rb b/lib/puppet/util/windows/security.rb index e195b1f12..368ec50d9 100644 --- a/lib/puppet/util/windows/security.rb +++ b/lib/puppet/util/windows/security.rb @@ -1,651 +1,657 @@ # 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. # * 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/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 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 # constants that are missing from Windows::Security PROTECTED_DACL_SECURITY_INFORMATION = 0x80000000 UNPROTECTED_DACL_SECURITY_INFORMATION = 0x20000000 NO_INHERITANCE = 0x0 # 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) change_sid(old_sid, owner_sid, OWNER_SECURITY_INFORMATION, path) 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) 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) change_sid(old_sid, group_sid, GROUP_SECURITY_INFORMATION, path) 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) 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 } # 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 with_privilege(SE_BACKUP_NAME) do open_file(path, READ_CONTROL) do |handle| mode = 0 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 else #puts "Warning, unable to map SID into POSIX mode: #{ace[:sid]}" mode |= S_IEXTRA end # 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)}" end end #puts "get_mode: #{mode.to_s(8)}" mode end end 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) well_known_world_sid = Win32::Security::SID::Everyone well_known_nobody_sid = Win32::Security::SID::Nobody 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 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 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 group_allow |= FILE_DELETE_CHILD end if (mode & (S_IWOTH | S_IXOTH)) == (S_IWOTH | S_IXOTH) and (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 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) # add inheritable aces for child dirs and files that are created within the dir if isdir inherit = INHERIT_ONLY_ACE | OBJECT_INHERIT_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_access_allowed_ace(acl, other_allow, well_known_world_sid, inherit) end end 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 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. # # 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} else Puppet.warning "Unsupported access control entry type: 0x#{ace_type.to_s(16)}" end 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 end # Convert a SID pointer to a string, e.g. "S-1-5-32-544". def sid_ptr_to_string(psid) sid_buf = 0.chr * 256 str_ptr = 0.chr * 4 raise Puppet::Util::Windows::Error.new("Invalid SID") unless IsValidSid(psid) raise Puppet::Util::Windows::Error.new("Failed to convert binary SID") unless ConvertSidToStringSid(psid, str_ptr) begin strncpy(sid_buf, str_ptr.unpack('L')[0], sid_buf.size - 1) sid_buf[sid_buf.size - 1] = 0.chr return sid_buf.strip ensure LocalFree(str_ptr.unpack('L')[0]) end end # Convert a SID string, e.g. "S-1-5-32-544" to a pointer (containing the # address of the binary SID structure). The returned value can be used in # Win32 APIs that expect a PSID, e.g. IsValidSid. def string_to_sid_ptr(string) sid_buf = 0.chr * 80 string_addr = [string].pack('p*').unpack('L')[0] raise Puppet::Util::Windows::Error.new("Failed to convert string SID: #{string}") unless ConvertStringSidToSid(string_addr, sid_buf) sid_ptr = sid_buf.unpack('L')[0] begin if block_given? yield sid_ptr else true end ensure LocalFree(sid_ptr) end 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_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 end diff --git a/spec/integration/type/file_spec.rb b/spec/integration/type/file_spec.rb index ec2fadcaf..dfdcd5805 100755 --- a/spec/integration/type/file_spec.rb +++ b/spec/integration/type/file_spec.rb @@ -1,1005 +1,1068 @@ #!/usr/bin/env rspec 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) { tmpfile('file_testing') } if Puppet.features.posix? def set_mode(mode, file) File.chmod(mode, file) end def get_mode(file) File.lstat(file).mode end def get_owner(file) File.lstat(file).uid end def get_group(file) File.lstat(file).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 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 file = described_class.new :path => path, :mode => 0755 catalog.add_resource file file.parameter(:mode).expects(:retrieve).never report = catalog.apply.report report.resource_statuses["File[#{path}]"].should_not be_failed File.should_not be_exist(path) end describe "when setting permissions" do it "should set the owner" do FileUtils.touch(path) owner = get_owner(path) file = described_class.new( :name => path, :owner => owner ) catalog.add_resource file catalog.apply get_owner(path).should == owner end it "should set the group" do FileUtils.touch(path) group = get_group(path) file = described_class.new( :name => path, :group => group ) catalog.add_resource file catalog.apply get_group(path).should == group end describe "when setting mode" do describe "for directories" do let(:path) { tmpdir('dir_mode') } it "should set executable bits for newly created directories" do catalog.add_resource described_class.new(:path => path, :ensure => :directory, :mode => 0600) catalog.apply (get_mode(path) & 07777).should == 0700 end it "should set executable bits for existing readable directories" do File.should be_directory(path) set_mode(0600, path) catalog.add_resource described_class.new(:path => path, :ensure => :directory, :mode => 0644) catalog.apply (get_mode(path) & 07777).should == 0755 end it "should not set executable bits for unreadable directories" do begin catalog.add_resource described_class.new(:path => path, :ensure => :directory, :mode => 0300) catalog.apply (get_mode(path) & 07777).should == 0300 ensure # so we can cleanup set_mode(0700, path) end end it "should set user, group, and other executable bits" do catalog.add_resource described_class.new(:path => path, :ensure => :directory, :mode => 0664) catalog.apply (get_mode(path) & 07777).should == 0775 end it "should set executable bits when overwriting a non-executable file" do FileUtils.rmdir(path) FileUtils.touch(path) set_mode(0444, path) catalog.add_resource described_class.new(:path => path, :ensure => :directory, :mode => 0666, :backup => false) catalog.apply (get_mode(path) & 07777).should == 0777 end end describe "for files" do let(:path) { tmpfile('file_mode') } 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", :unless => Puppet.features.microsoft_windows? do let(:link) { tmpfile('link_mode') } describe "when managing links" do let(:target) { tmpfile('target') } before :each do FileUtils.touch(target) File.chmod(0444, target) File.symlink(target, 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 => target, :links => :manage) catalog.apply (File.stat(link).mode & 07777) == 0666 (File.lstat(target).mode & 07777) == 0444 end it "should ignore dangling symlinks (#6856)" do File.delete(target) catalog.add_resource described_class.new(:path => link, :ensure => :link, :mode => 0666, :target => target, :links => :manage) catalog.apply File.should_not be_exist(link) end end describe "when following links" do it "should ignore dangling symlinks (#6856)" do target = tmpfile('dangling') FileUtils.touch(target) File.symlink(target, 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(:target) { tmpdir('dir_target') } before :each do File.chmod(0600, target) File.symlink(target, link) end after :each do File.chmod(0750, target) end describe "that is readable" do it "should set the executable bits when creating the destination (#10315)" do pending "bug #10315" catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0666, :links => :follow) catalog.apply (get_mode(path) & 07777).should == 0777 end it "should set the executable bits when overwriting the destination (#10315)" do pending "bug #10315" FileUtils.touch(path) catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0666, :links => :follow) catalog.apply (get_mode(path) & 07777).should == 0777 end end describe "that is not readable" do before :each do set_mode(0300, target) end # so we can cleanup after :each do set_mode(0700, target) end it "should not set executable bits when creating the destination (#10315)" do pending "bug #10315" catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0666, :links => :follow) catalog.apply (get_mode(path) & 07777).should == 0666 end it "should not 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) catalog.apply (get_mode(path) & 07777).should == 0666 end end end describe "to a file" do let(:target) { tmpfile('file_target') } it "should create the file, not a symlink (#2817, #10315)" do pending "bug #2817, #10315" 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) == 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) == 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 File.symlink(real_target, target) File.symlink(target, 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 pending "bug #10315" 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) == 0777 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) & 07777) == 0777 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], "wb") { |f| f.puts "bar" } md5 = Digest::MD5.hexdigest(Puppet::Util.binread(file[:path])) catalog.apply filebucket.bucket.getfile(md5).should == "bar\n" 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" FileTest.should be_exist(backup) File.read(backup).should == "bar\n" end it "should fail if no backup can be performed" do dir = tmpfile("backups") Dir.mkdir(dir) 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", :unless => Puppet.features.microsoft_windows? 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" } File.symlink(dest1, link) md5 = Digest::MD5.hexdigest(File.read(file[:path])) catalog.apply File.readlink(link).should == dest2 Find.find(bucket[:path]) { |f| File.file?(f) }.should be_nil 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", :unless => Puppet.features.microsoft_windows? 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) File.lstat(link_path).should be_directory end @files.each do |path| link_path = path.sub(source, dest) File.lstat(link_path).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) File.lstat(newpath).should be_directory end @files.each do |path| newpath = path.sub(source, dest) File.lstat(newpath).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')) obj = Puppet::Type.newfile( :path => path, :ensure => :directory, :backup => false, :recurse => true, :sourceselect => :first, :source => [one, two] ) catalog.add_resource obj catalog.apply File.should be_directory(path) File.should_not be_exist(File.join(path, 'one')) File.should be_exist(File.join(path, 'three', 'four')) end it "should recursively copy an empty directory" do one = File.expand_path('thisdoesnotexist') two = tmpdir('two') three = tmpdir('three') FileUtils.mkdir_p(two) FileUtils.mkdir_p(three) FileUtils.touch(File.join(three, 'a')) obj = Puppet::Type.newfile( :path => path, :ensure => :directory, :backup => false, :recurse => true, :sourceselect => :first, :source => [one, two, three] ) catalog.add_resource obj catalog.apply File.should be_directory(path) File.should_not be_exist(File.join(path, 'a')) 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')) obj = Puppet::Type.newfile( :path => path, :ensure => :directory, :backup => false, :recurse => true, :recurselimit => 1, :sourceselect => :first, :source => [one, two] ) catalog.add_resource obj catalog.apply File.should be_exist(File.join(path, 'a')) File.should_not be_exist(File.join(path, 'a', 'b')) File.should_not be_exist(File.join(path, 'z')) end end describe "for a file" do it "should copy the first file that exists" do one = File.expand_path('thisdoesnotexist') two = tmpfile('two') File.open(two, "w") { |f| f.print 'yay' } three = tmpfile('three') File.open(three, "w") { |f| f.print 'no' } obj = Puppet::Type.newfile( :path => path, :ensure => :file, :backup => false, :sourceselect => :first, :source => [one, two, three] ) catalog.add_resource obj catalog.apply File.read(path).should == 'yay' end it "should copy an empty file" do one = File.expand_path('thisdoesnotexist') two = tmpfile('two') FileUtils.touch(two) three = tmpfile('three') File.open(three, "w") { |f| f.print 'no' } obj = Puppet::Type.newfile( :path => path, :ensure => :file, :backup => false, :sourceselect => :first, :source => [one, two, three] ) catalog.add_resource obj 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 one = tmpdir('one') two = tmpdir('two') three = tmpdir('three') four = tmpdir('four') [one, two, three, four].each {|dir| FileUtils.mkdir_p(dir)} File.open(File.join(one, 'a'), "w") { |f| f.print one } File.open(File.join(two, 'a'), "w") { |f| f.print two } File.open(File.join(two, 'b'), "w") { |f| f.print two } File.open(File.join(three, 'a'), "w") { |f| f.print three } File.open(File.join(three, 'c'), "w") { |f| f.print three } obj = Puppet::Type.newfile( :path => path, :ensure => :directory, :backup => false, :recurse => true, :sourceselect => :all, :source => [one, two, three, four] ) catalog.add_resource obj catalog.apply File.read(File.join(path, 'a')).should == one File.read(File.join(path, 'b')).should == two File.read(File.join(path, '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 File.should be_exist(File.join(path, 'a')) File.should_not be_exist(File.join(path, 'a', 'b')) File.should be_exist(File.join(path, 'z')) File.should_not be_exist(File.join(path, 'z', 'y')) end end end end end describe "when generating resources" do before do source = tmpfile("generating_in_catalog_source") Dir.mkdir(source) s1 = File.join(source, "one") s2 = File.join(source, "two") File.open(s1, "w") { |f| f.puts "uno" } File.open(s2, "w") { |f| f.puts "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")).should be_a(described_class) catalog.resource(:file, File.join(path, "two")).should 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 # Ticket #285. it "should be able to copy files with pound signs in their names" do source = tmpfile("filewith#signs") dest = tmpfile("destwith#signs") File.open(source, "w") { |f| f.print "foo" } file = described_class.new(:name => dest, :source => source) catalog.add_resource file catalog.apply File.read(dest).should == "foo" end it "should be able to copy files with spaces in their names" do source = tmpfile("filewith spaces") dest = tmpfile("destwith spaces") File.open(source, "w") { |f| f.print "foo" } File.chmod(0755, source) file = described_class.new(:path => dest, :source => source) catalog.add_resource file catalog.apply expected_mode = Puppet.features.microsoft_windows? ? 0644 : 0755 File.read(dest).should == "foo" (File.stat(dest).mode & 007777).should == expected_mode end it "should be able to copy individual files even if recurse has been specified" do source = tmpfile("source") dest = tmpfile("dest") File.open(source, "w") { |f| f.print "foo" } file = described_class.new(:name => dest, :source => source, :recurse => true) catalog.add_resource file catalog.apply File.read(dest).should == "foo" end end it "should create a file with content if ensure is omitted" do file = described_class.new( :path => path, :content => "this is some content, yo" ) catalog.add_resource file 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("source_source_with_ensure") File.open(source, "w") { |f| f.puts "yay" } File.open(path, "w") { |f| f.puts "boo" } file = described_class.new( :path => path, :ensure => :absent, :source => source, :backup => false ) catalog.add_resource file catalog.apply File.should_not be_exist(path) end + describe "when sourcing" do + let(:source) { + source = tmpfile("source_default_values") + File.open(source, "w") { |f| f.puts "yay" } + source + } + + 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 + 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-32-544' + get_group(path).should == 'S-1-0-0' + get_mode(path).should == 0644 + end + end + end + describe "when purging files" do before do sourcedir = tmpfile("purge_source") destdir = tmpfile("purge_dest") Dir.mkdir(sourcedir) Dir.mkdir(destdir) 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 FileTest.should_not be_exist(@purgee) end end end diff --git a/spec/integration/util/windows/security_spec.rb b/spec/integration/util/windows/security_spec.rb index 158fbca20..57e1ea8d3 100755 --- a/spec/integration/util/windows/security_spec.rb +++ b/spec/integration/util/windows/security_spec.rb @@ -1,619 +1,631 @@ #!/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::ADSI.sid_for_account(Sys::Admin.get_login), :admin => Puppet::Util::ADSI.sid_for_account("Administrator"), :guest => Puppet::Util::ADSI.sid_for_account("Guest"), :users => Win32::Security::SID::BuiltinUsers, :power_users => Win32::Security::SID::PowerUsers, } end let (:sids) { @sids } let (:winsec) { WindowsSecurityTester.new } shared_examples_for "only child owner" do it "should allow child owner" do check_child_owner end it "should deny parent owner" do lambda { check_parent_owner }.should raise_error(Errno::EACCES) end it "should deny group" do lambda { check_group }.should raise_error(Errno::EACCES) end it "should deny other" do lambda { check_other }.should raise_error(Errno::EACCES) end end shared_examples_for "a securable object" do - describe "for a normal user" do - before :each do - Puppet.features.stubs(:root?).returns(false) - end + 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 - after :each do - winsec.set_mode(WindowsSecurityTester::S_IRWXU, parent) - winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) if File.exists?(path) + winsec.send("get_#{p}", path).should be_nil + end end + 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 + describe "on a volume that supports ACLs" do + describe "for a normal user" do + before :each do + Puppet.features.stubs(:root?).returns(false) 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/) + after :each do + winsec.set_mode(WindowsSecurityTester::S_IRWXU, parent) + winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) if File.exists?(path) end - end - describe "#owner=" do - it "should allow setting to the current user" do - winsec.set_owner(sids[:current_user], 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 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./) + 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 - end - describe "#owner" do - it "it should not be empty" do - winsec.get_owner(path).should_not be_empty - 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 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./) + 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 - 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 + describe "#owner" do + it "it should not be empty" do + winsec.get_owner(path).should_not be_empty + 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) + 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 - end - describe "#group" do - it "should not be empty" do - winsec.get_group(path).should_not be_empty - 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 - 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./) + # 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 - end - describe "#mode=" do - (0000..0700).step(0100).each do |mode| - it "should enforce mode #{mode.to_s(8)}" do - winsec.set_mode(mode, path) + describe "#group" do + it "should not be empty" do + winsec.get_group(path).should_not be_empty + end - check_access(mode, path) + 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 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) + describe "#mode=" do + (0000..0700).step(0100).each do |mode| + it "should enforce mode #{mode.to_s(8)}" do + winsec.set_mode(mode, path) - 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) + 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 - 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 + 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) + 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" + winsec.set_mode(0410, path) + winsec.get_mode(path).to_s(8).should == "550" + end end - end - describe "for read-only objects" do - before :each do - winsec.add_attributes(path, WindowsSecurityTester::FILE_ATTRIBUTE_READONLY) - (winsec.get_attributes(path) & WindowsSecurityTester::FILE_ATTRIBUTE_READONLY).should be_nonzero - end + describe "for read-only objects" do + before :each do + 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 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" do - winsec.set_mode(WindowsSecurityTester::S_IRUSR | WindowsSecurityTester::S_IXGRP, path) - (winsec.get_attributes(path) & WindowsSecurityTester::FILE_ATTRIBUTE_READONLY).should be_nonzero + it "should leave them read-only if no sid has write permission" do + winsec.set_mode(WindowsSecurityTester::S_IRUSR | WindowsSecurityTester::S_IXGRP, path) + (winsec.get_attributes(path) & WindowsSecurityTester::FILE_ATTRIBUTE_READONLY).should be_nonzero + end 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 + 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 - mode = winsec.get_mode(path) - (mode & WindowsSecurityTester::S_IEXTRA).should_not == 0 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]) + 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 + end + mode = winsec.get_mode(path) + (mode & WindowsSecurityTester::S_IEXTRA).should_not == 0 end - Puppet.expects(:warning).with("Unsupported access control entry type: 0x1") + 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 - winsec.get_mode(path) - end + Puppet.expects(:warning).with("Unsupported access control entry type: 0x1") - 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) + winsec.get_mode(path) end - (winsec.get_mode(path) & WindowsSecurityTester::S_IRWXO).should == 0 - 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 - 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 + (winsec.get_mode(path) & WindowsSecurityTester::S_IRWXO).should == 0 + end - describe "inherited access control entries" do - it "should be absent when the access control list is protected" do - winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) - (winsec.get_mode(path) & WindowsSecurityTester::S_IEXTRA).should == 0 + 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 - 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 + describe "inherited access control entries" do + it "should be absent when the access control list is protected" do + winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) + (winsec.get_mode(path) & WindowsSecurityTester::S_IEXTRA).should == 0 + 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 + 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) + (544..547).each do |rid| + winsec.add_access_allowed_ace(acl, WindowsSecurityTester::STANDARD_RIGHTS_ALL, "S-1-5-32-#{rid}", inherit) + end end - end - # 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 + # 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 - end - describe "for an administrator", :if => Puppet.features.root? do - before :each do - winsec.set_mode(WindowsSecurityTester::S_IRWXU | WindowsSecurityTester::S_IRWXG, path) - winsec.set_group(sids[:guest], path) - winsec.set_owner(sids[:guest], path) - lambda { File.open(path, 'r') }.should raise_error(Errno::EACCES) - end - - after :each do - if File.exists?(path) - winsec.set_owner(sids[:current_user], path) - winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) + describe "for an administrator", :if => Puppet.features.root? do + before :each do + winsec.set_mode(WindowsSecurityTester::S_IRWXU | WindowsSecurityTester::S_IRWXG, path) + winsec.set_group(sids[:guest], path) + winsec.set_owner(sids[:guest], path) + lambda { File.open(path, 'r') }.should raise_error(Errno::EACCES) 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] + after :each do + if File.exists?(path) + winsec.set_owner(sids[:current_user], path) + winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) + end end - it "should accept a group sid" do - winsec.set_owner(sids[:power_users], path) - winsec.get_owner(path).should == sids[:power_users] - 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 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 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 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 + 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 - 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] + 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 - it "should accept a user sid" do - winsec.set_group(sids[:admin], path) - winsec.get_group(path).should == sids[:admin] - 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) - winsec.set_owner(sids[:power_users], path) - winsec.set_group(sids[:power_users], path) + it "should allow owner and group to be the same sid" do + winsec.set_mode(0610, path) + winsec.set_owner(sids[:power_users], path) + winsec.set_group(sids[:power_users], 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" - end + 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" + 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 sid is provided" do - lambda { winsec.set_group("foobar", path) }.should raise_error(Puppet::Error, /Failed to convert string SID/) + 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 - 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./) + describe "when the sid is NULL" do + it "should retrieve an empty owner sid" + it "should retrieve an empty group sid" 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 - 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) + winsec.get_owner(path).should == sid + winsec.get_mode(path).should == WindowsSecurityTester::S_IRWXU 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) + 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 - 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" do - winsec.set_acl(path, true) { |acl| } - - winsec.get_mode(path).should == 0 - end + describe "#mode" do + it "should deny all access when the DACL is empty" do + winsec.set_acl(path, true) { |acl| } - # 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 + winsec.get_mode(path).should == 0 + end - describe "#string_to_sid_ptr" do - it "should raise an error if an invalid SID is specified" do - expect do - winsec.string_to_sid_ptr('foobar') - end.to raise_error(Puppet::Util::Windows::Error) { |error| error.code.should == 1337 } - 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 "#string_to_sid_ptr" do + it "should raise an error if an invalid SID is specified" do + expect do + winsec.string_to_sid_ptr('foobar') + end.to raise_error(Puppet::Util::Windows::Error) { |error| error.code.should == 1337 } + end - it "should yield if a block is given" do - yielded = nil - winsec.string_to_sid_ptr('S-1-1-0') do |sid| - yielded = sid + it "should yield if a block is given" do + yielded = nil + winsec.string_to_sid_ptr('S-1-1-0') do |sid| + yielded = sid + end + yielded.should_not be_nil end - yielded.should_not be_nil - end - it "should allow no block to be specified" do - winsec.string_to_sid_ptr('S-1-1-0').should be_true + it "should allow no block to be specified" do + winsec.string_to_sid_ptr('S-1-1-0').should be_true + end 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 + 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) + def check_child_owner + winsec.set_group(sids[:guest], parent) + winsec.set_owner(sids[:guest], 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) check_delete(path) end def check_group winsec.set_group(sids[:current_user], path) winsec.set_owner(sids[:guest], path) winsec.set_owner(sids[:guest], parent) check_delete(path) end def check_other winsec.set_group(sids[:guest], path) winsec.set_owner(sids[:guest], path) winsec.set_owner(sids[:guest], parent) check_delete(path) end describe "is writable and executable" do describe "and sticky bit is set" do before :each do winsec.set_mode(01777, parent) end it "should allow child owner" do check_child_owner end it "should allow parent owner" do check_parent_owner end it "should deny group" do lambda { check_group }.should raise_error(Errno::EACCES) end it "should deny other" do lambda { check_other }.should raise_error(Errno::EACCES) end end describe "and sticky bit is not set" do before :each do winsec.set_mode(0777, parent) end it "should allow child owner" do check_child_owner end it "should allow parent owner" do check_parent_owner end it "should allow group" do check_group end it "should allow other" do check_other end end end describe "is not writable" do before :each do winsec.set_mode(0555, parent) end it_behaves_like "only child owner" end describe "is not executable" do before :each do winsec.set_mode(0666, parent) 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| winsec.get_mode(p).to_s(8).should == mode640.to_s(8) end end end end end diff --git a/spec/unit/file_serving/metadata_spec.rb b/spec/unit/file_serving/metadata_spec.rb index 222b76de8..3842b05bc 100755 --- a/spec/unit/file_serving/metadata_spec.rb +++ b/spec/unit/file_serving/metadata_spec.rb @@ -1,329 +1,356 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/file_serving/metadata' describe Puppet::FileServing::Metadata do it "should should be a subclass of Base" do Puppet::FileServing::Metadata.superclass.should equal(Puppet::FileServing::Base) end it "should indirect file_metadata" do Puppet::FileServing::Metadata.indirection.name.should == :file_metadata end it "should should include the IndirectionHooks module in its indirection" do Puppet::FileServing::Metadata.indirection.singleton_class.included_modules.should include(Puppet::FileServing::IndirectionHooks) end it "should have a method that triggers attribute collection" do Puppet::FileServing::Metadata.new("/foo/bar").should respond_to(:collect) end it "should support pson serialization" do Puppet::FileServing::Metadata.new("/foo/bar").should respond_to(:to_pson) end it "should support to_pson_data_hash" do Puppet::FileServing::Metadata.new("/foo/bar").should respond_to(:to_pson_data_hash) end it "should support pson deserialization" do Puppet::FileServing::Metadata.should respond_to(:from_pson) end describe "when serializing" do before do @metadata = Puppet::FileServing::Metadata.new("/foo/bar") end it "should perform pson serialization by calling to_pson on it's pson_data_hash" do pdh = mock "data hash" pdh_as_pson = mock "data as pson" @metadata.expects(:to_pson_data_hash).returns pdh pdh.expects(:to_pson).returns pdh_as_pson @metadata.to_pson.should == pdh_as_pson end it "should serialize as FileMetadata" do @metadata.to_pson_data_hash['document_type'].should == "FileMetadata" end it "the data should include the path, relative_path, links, owner, group, mode, checksum, type, and destination" do @metadata.to_pson_data_hash['data'].keys.sort.should == %w{ path relative_path links owner group mode checksum type destination }.sort end it "should pass the path in the hash verbatum" do @metadata.to_pson_data_hash['data']['path'] == @metadata.path end it "should pass the relative_path in the hash verbatum" do @metadata.to_pson_data_hash['data']['relative_path'] == @metadata.relative_path end it "should pass the links in the hash verbatum" do @metadata.to_pson_data_hash['data']['links'] == @metadata.links end it "should pass the path owner in the hash verbatum" do @metadata.to_pson_data_hash['data']['owner'] == @metadata.owner end it "should pass the group in the hash verbatum" do @metadata.to_pson_data_hash['data']['group'] == @metadata.group end it "should pass the mode in the hash verbatum" do @metadata.to_pson_data_hash['data']['mode'] == @metadata.mode end it "should pass the ftype in the hash verbatum as the 'type'" do @metadata.to_pson_data_hash['data']['type'] == @metadata.ftype end it "should pass the destination verbatum" do @metadata.to_pson_data_hash['data']['destination'] == @metadata.destination end it "should pass the checksum in the hash as a nested hash" do @metadata.to_pson_data_hash['data']['checksum'].should be_is_a(Hash) end it "should pass the checksum_type in the hash verbatum as the checksum's type" do @metadata.to_pson_data_hash['data']['checksum']['type'] == @metadata.checksum_type end it "should pass the checksum in the hash verbatum as the checksum's value" do @metadata.to_pson_data_hash['data']['checksum']['value'] == @metadata.checksum end end end describe Puppet::FileServing::Metadata do include PuppetSpec::Files shared_examples_for "metadata collector" do let(:metadata) do data = described_class.new(path) data.collect data end describe "when collecting attributes" do describe "when managing files" do let(:path) { tmpfile('file_serving_metadata') } before :each do FileUtils.touch(path) end it "should be able to produce xmlrpc-style attribute information" do metadata.should respond_to(:attributes_with_tabs) end it "should set the owner to the file's current owner" do metadata.owner.should == owner end it "should set the group to the file's current group" do metadata.group.should == group end it "should set the mode to the file's masked mode" do set_mode(33261, path) metadata.mode.should == 0755 end describe "#checksum" do let(:checksum) { Digest::MD5.hexdigest("some content\n") } before :each do File.open(path, "wb") {|f| f.print("some content\n")} end it "should default to a checksum of type MD5 with the file's current checksum" do metadata.checksum.should == "{md5}#{checksum}" end it "should give a mtime checksum when checksum_type is set" do time = Time.now metadata.checksum_type = "mtime" metadata.expects(:mtime_file).returns(@time) metadata.collect metadata.checksum.should == "{mtime}#{@time}" end it "should produce tab-separated mode, type, owner, group, and checksum for xmlrpc" do set_mode(0755, path) metadata.attributes_with_tabs.should == "#{0755.to_s}\tfile\t#{owner}\t#{group}\t{md5}#{checksum}" end end end describe "when managing directories" do let(:path) { tmpdir('file_serving_metadata_dir') } let(:time) { Time.now } before :each do metadata.expects(:ctime_file).returns(time) end it "should only use checksums of type 'ctime' for directories" do metadata.collect metadata.checksum.should == "{ctime}#{time}" end it "should only use checksums of type 'ctime' for directories even if checksum_type set" do metadata.checksum_type = "mtime" metadata.expects(:mtime_file).never metadata.collect metadata.checksum.should == "{ctime}#{time}" end it "should produce tab-separated mode, type, owner, group, and checksum for xmlrpc" do set_mode(0755, path) metadata.collect metadata.attributes_with_tabs.should == "#{0755.to_s}\tdirectory\t#{owner}\t#{group}\t{ctime}#{time.to_s}" end end describe "when managing links", :unless => Puppet.features.microsoft_windows? do # 'path' is a link that points to 'target' let(:path) { tmpfile('file_serving_metadata_link') } let(:target) { tmpfile('file_serving_metadata_target') } let(:checksum) { Digest::MD5.hexdigest("some content\n") } let(:fmode) { File.lstat(path).mode & 0777 } before :each do File.open(target, "wb") {|f| f.print("some content\n")} set_mode(0644, target) FileUtils.symlink(target, path) end it "should read links instead of returning their checksums" do metadata.destination.should == target end pending "should produce tab-separated mode, type, owner, group, and destination for xmlrpc" do # "We'd like this to be true, but we need to always collect the checksum because in the server/client/server round trip we lose the distintion between manage and follow." metadata.attributes_with_tabs.should == "#{0755}\tlink\t#{owner}\t#{group}\t#{target}" end it "should produce tab-separated mode, type, owner, group, checksum, and destination for xmlrpc" do metadata.attributes_with_tabs.should == "#{fmode}\tlink\t#{owner}\t#{group}\t{md5}eb9c2bf0eb63f3a7bc0ea37ef18aeba5\t#{target}" end end end describe Puppet::FileServing::Metadata, " when finding the file to use for setting attributes" do let(:path) { tmpfile('file_serving_metadata_find_file') } before :each do File.open(path, "wb") {|f| f.print("some content\n")} set_mode(0755, path) end it "should accept a base path to which the file should be relative" do dir = tmpdir('metadata_dir') metadata = described_class.new(dir) metadata.relative_path = 'relative_path' FileUtils.touch(metadata.full_path) metadata.collect end it "should use the set base path if one is not provided" do metadata.collect end it "should raise an exception if the file does not exist" do File.delete(path) proc { metadata.collect}.should raise_error(Errno::ENOENT) end end end describe "on POSIX systems", :if => Puppet.features.posix? do let(:owner) {10} let(:group) {20} before :each do File::Stat.any_instance.stubs(:uid).returns owner File::Stat.any_instance.stubs(:gid).returns group end it_should_behave_like "metadata collector" def set_mode(mode, path) File.chmod(mode, path) end end describe "on Windows systems", :if => Puppet.features.microsoft_windows? do let(:owner) {'S-1-1-50'} let(:group) {'S-1-1-51'} before :each do require 'puppet/util/windows/security' Puppet::Util::Windows::Security.stubs(:get_owner).returns owner Puppet::Util::Windows::Security.stubs(:get_group).returns group end it_should_behave_like "metadata collector" + describe "if ACL metadata cannot be collected" do + let(:path) { tmpdir('file_serving_metadata_acl') } + let(:metadata) do + data = described_class.new(path) + data.collect + data + end + + it "should default owner" do + Puppet::Util::Windows::Security.stubs(:get_owner).returns nil + + metadata.owner.should == 'S-1-5-32-544' + end + + it "should default group" do + Puppet::Util::Windows::Security.stubs(:get_group).returns nil + + metadata.group.should == 'S-1-0-0' + end + + it "should default mode" do + Puppet::Util::Windows::Security.stubs(:get_mode).returns nil + + metadata.mode.should == 0644 + end + end + def set_mode(mode, path) Puppet::Util::Windows::Security.set_mode(mode, path) end end end describe Puppet::FileServing::Metadata, " when pointing to a link", :unless => Puppet.features.microsoft_windows? do describe "when links are managed" do before do @file = Puppet::FileServing::Metadata.new("/base/path/my/file", :links => :manage) File.expects(:lstat).with("/base/path/my/file").returns stub("stat", :uid => 1, :gid => 2, :ftype => "link", :mode => 0755) File.expects(:readlink).with("/base/path/my/file").returns "/some/other/path" @checksum = Digest::MD5.hexdigest("some content\n") # Remove these when :managed links are no longer checksumed. @file.stubs(:md5_file).returns(@checksum) # end it "should store the destination of the link in :destination if links are :manage" do @file.collect @file.destination.should == "/some/other/path" end pending "should not collect the checksum if links are :manage" do # We'd like this to be true, but we need to always collect the checksum because in the server/client/server round trip we lose the distintion between manage and follow. @file.collect @file.checksum.should be_nil end it "should collect the checksum if links are :manage" do # see pending note above @file.collect @file.checksum.should == "{md5}#{@checksum}" end end describe "when links are followed" do before do @file = Puppet::FileServing::Metadata.new("/base/path/my/file", :links => :follow) File.expects(:stat).with("/base/path/my/file").returns stub("stat", :uid => 1, :gid => 2, :ftype => "file", :mode => 0755) File.expects(:readlink).with("/base/path/my/file").never @checksum = Digest::MD5.hexdigest("some content\n") @file.stubs(:md5_file).returns(@checksum) end it "should not store the destination of the link in :destination if links are :follow" do @file.collect @file.destination.should be_nil end it "should collect the checksum if links are :follow" do @file.collect @file.checksum.should == "{md5}#{@checksum}" end end end