diff --git a/lib/puppet/file_serving/metadata.rb b/lib/puppet/file_serving/metadata.rb index 382ac9c96..4c863ee89 100644 --- a/lib/puppet/file_serving/metadata.rb +++ b/lib/puppet/file_serving/metadata.rb @@ -1,114 +1,153 @@ 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| + define_method method do + Puppet::Util::Windows::Security.send("get_#{method}", @path) + 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 = stat() - @owner = stat.uid - @group = stat.gid - @ftype = stat.ftype + 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/util/windows/security.rb b/lib/puppet/util/windows/security.rb index a0ec628f4..2c94f4d47 100644 --- a/lib/puppet/util/windows/security.rb +++ b/lib/puppet/util/windows/security.rb @@ -1,591 +1,593 @@ # 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 'win32/security' require 'windows/file' require 'windows/handle' require 'windows/security' require 'windows/process' require 'windows/memory' module Puppet::Util::Windows::Security include Windows::File include Windows::Handle include Windows::Security include Windows::Process include Windows::Memory include Windows::MSVCRT::Buffer + 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_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) 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) get_sid(GROUP_SECURITY_INFORMATION, path) 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) set_attributes(path, get_attributes(path) | flags) end def remove_attributes(path, flags) set_attributes(path, get_attributes(path) & ~flags) end def set_attributes(path, flags) raise Puppet::Util::Windows::Error.new("Failed to set file attributes") if SetFileAttributes(path, flags) == 0 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. Other # modes, e.g. S_ISVTX, are not supported. 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) owner_sid = get_owner(path) group_sid = get_group(path) well_known_world_sid = Win32::Security::SID::Everyone 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 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), (S_IWOTH | S_IXOTH) => FILE_DELETE_CHILD, } # 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. Other modes, e.g. S_ISVTX, are not supported. 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 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 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 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 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) # add inheritable aces for child dirs and files that are created within the dir if File.directory?(path) 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 # if any ACE allows write, then clear readonly bit if ((owner_allow | group_allow | other_allow ) & FILE_WRITE_DATA) == FILE_WRITE_DATA remove_attributes(path, FILE_ATTRIBUTE_READONLY) 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") if IsValidAcl(acl) == 0 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") if IsValidSid(sid_ptr) == 0 if AddAccessAllowedAceEx(acl, ACL_REVISION, inherit, mask, sid_ptr) == 0 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") if IsValidSid(sid_ptr) == 0 if AddAccessDeniedAce(acl, ACL_REVISION, mask, sid_ptr) == 0 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") if IsValidAcl(dacl_ptr) == 0 # 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 if GetAce(dacl_ptr, i, ace_ptr) == 0 # 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") if IsValidSid(psid) == 0 raise Puppet::Util::Windows::Error.new("Failed to convert binary SID") if ConvertSidToStringSid(psid, str_ptr) == 0 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. if LookupPrivilegeValue("", privilege, tmpLuid) == 0 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') if AdjustTokenPrivileges(token, 0, tkp, tkp.length , nil, nil) == 0 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 if OpenProcessToken(GetCurrentProcess(), access, token) == 0 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/unit/file_serving/metadata_spec.rb b/spec/unit/file_serving/metadata_spec.rb index 39f2a9548..7462fed5d 100755 --- a/spec/unit/file_serving/metadata_spec.rb +++ b/spec/unit/file_serving/metadata_spec.rb @@ -1,285 +1,329 @@ #!/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, " when finding the file to use for setting attributes" do - before do - @path = "/my/path" - @metadata = Puppet::FileServing::Metadata.new(@path) +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 - # Use a link because it's easier to test -- no checksumming - @stat = stub "stat", :uid => 10, :gid => 20, :mode => 0755, :ftype => "link" + describe "#checksum" do + let(:checksum) { Digest::MD5.hexdigest("some content\n") } - # Not quite. We don't want to checksum links, but we must because they might be being followed. - @checksum = Digest::MD5.hexdigest("some content\n") # Remove these when :managed links are no longer checksumed. - @metadata.stubs(:md5_file).returns(@checksum) # - end + before :each do + File.open(path, "w") {|f| f.print("some content\n")} + end - it "should accept a base path path to which the file should be relative" do - File.expects(:lstat).with(@path).returns @stat - File.expects(:readlink).with(@path).returns "/what/ever" - @metadata.collect - 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 use the set base path if one is not provided" do - File.expects(:lstat).with(@path).returns @stat - File.expects(:readlink).with(@path).returns "/what/ever" - @metadata.collect - 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 raise an exception if the file does not exist" do - File.expects(:lstat).with(@path).raises(Errno::ENOENT) - proc { @metadata.collect}.should raise_error(Errno::ENOENT) - end -end + it "should produce tab-separated mode, type, owner, group, and checksum for xmlrpc" do + set_mode(0755, path) -describe Puppet::FileServing::Metadata, " when collecting attributes" do - before do - @path = "/my/file" - # Use a real file mode, so we can validate the masking is done. - @stat = stub 'stat', :uid => 10, :gid => 20, :mode => 33261, :ftype => "file" - File.stubs(:lstat).returns(@stat) - @checksum = Digest::MD5.hexdigest("some content\n") - @metadata = Puppet::FileServing::Metadata.new("/my/file") - @metadata.stubs(:md5_file).returns(@checksum) - @metadata.collect - end + metadata.attributes_with_tabs.should == "#{0755.to_s}\tfile\t#{owner}\t#{group}\t{md5}#{checksum}" + end + end + end - it "should be able to produce xmlrpc-style attribute information" do - @metadata.should respond_to(:attributes_with_tabs) - end + describe "when managing directories" do + let(:path) { tmpdir('file_serving_metadata_dir') } + let(:time) { Time.now } - # LAK:FIXME This should actually change at some point - it "should set the owner by id" do - @metadata.owner.should be_instance_of(Fixnum) - end + before :each do + metadata.expects(:ctime_file).returns(time) + end - # LAK:FIXME This should actually change at some point - it "should set the group by id" do - @metadata.group.should be_instance_of(Fixnum) - end + it "should only use checksums of type 'ctime' for directories" do + metadata.collect + metadata.checksum.should == "{ctime}#{time}" + end - it "should set the owner to the file's current owner" do - @metadata.owner.should == 10 - 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 set the group to the file's current group" do - @metadata.group.should == 20 - end + it "should produce tab-separated mode, type, owner, group, and checksum for xmlrpc" do + set_mode(0755, path) + metadata.collect - it "should set the mode to the file's masked mode" do - @metadata.mode.should == 0755 - end + metadata.attributes_with_tabs.should == "#{0755.to_s}\tdirectory\t#{owner}\t#{group}\t{ctime}#{time.to_s}" + end + end - it "should set the checksum to the file's current checksum" do - @metadata.checksum.should == "{md5}#{@checksum}" - 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") } - describe "when managing files" do - it "should default to a checksum of type MD5" do - @metadata.checksum.should == "{md5}#{@checksum}" - end + before :each do + File.open(target, "w") {|f| f.print("some content\n")} + set_mode(0644, target) - 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 + FileUtils.symlink(target, path) + set_mode(0755, path) + end - it "should produce tab-separated mode, type, owner, group, and checksum for xmlrpc" do - @metadata.attributes_with_tabs.should == "#{0755.to_s}\tfile\t10\t20\t{md5}#{@checksum}" - end - end + it "should read links instead of returning their checksums" do + metadata.destination.should == target + end - describe "when managing directories" do - before do - @stat.stubs(:ftype).returns("directory") - @time = Time.now - @metadata.expects(:ctime_file).returns(@time) - 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 only use checksums of type 'ctime' for directories" do - @metadata.collect - @metadata.checksum.should == "{ctime}#{@time}" + it "should produce tab-separated mode, type, owner, group, checksum, and destination for xmlrpc" do + metadata.attributes_with_tabs.should == "#{0755}\tlink\t#{owner}\t#{group}\t{md5}eb9c2bf0eb63f3a7bc0ea37ef18aeba5\t#{target}" + end + end 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 + 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, "w") {|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 produce tab-separated mode, type, owner, group, and checksum for xmlrpc" do - @metadata.collect - @metadata.attributes_with_tabs.should == "#{0755.to_s}\tdirectory\t10\t20\t{ctime}#{@time.to_s}" + 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 "when managing links" do - before do - @stat.stubs(:ftype).returns("link") - File.expects(:readlink).with("/my/file").returns("/path/to/link") - @metadata.collect + describe "on POSIX systems", :if => Puppet.features.posix? do + let(:owner) {10} + let(:group) {20} - @checksum = Digest::MD5.hexdigest("some content\n") # Remove these when :managed links are no longer checksumed. - @file.stubs(:md5_file).returns(@checksum) # + before :each do + File::Stat.any_instance.stubs(:uid).returns owner + File::Stat.any_instance.stubs(:gid).returns group end - it "should read links instead of returning their checksums" do - @metadata.destination.should == "/path/to/link" + 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'} - 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\t10\t20\t/path/to/link" + 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 produce tab-separated mode, type, owner, group, checksum, and destination for xmlrpc" do - @metadata.attributes_with_tabs.should == "#{0755}\tlink\t10\t20\t{md5}eb9c2bf0eb63f3a7bc0ea37ef18aeba5\t/path/to/link" + it_should_behave_like "metadata collector" + + 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" do + +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