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..254fba303 100644 --- a/lib/puppet/provider/file/windows.rb +++ b/lib/puppet/provider/file/windows.rb @@ -1,100 +1,107 @@ 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 + + def validate + if [:owner, :group, :mode].any?{|p| resource[p]} and !supports_acl?(resource[:path]) + resource.fail("Can only manage owner, group, and mode on filesystems that support Windows ACLs, such as NTFS") + end + end end diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb index b1e65b390..e1ddf1fbf 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -1,804 +1,806 @@ require 'digest/md5' require 'cgi' require 'etc' require 'uri' require 'fileutils' require 'enumerator' require 'pathname' require 'puppet/network/handler' require 'puppet/util/diff' require 'puppet/util/checksums' require 'puppet/util/backups' Puppet::Type.newtype(:file) do include Puppet::Util::MethodHelper include Puppet::Util::Checksums include Puppet::Util::Backups @doc = "Manages local files, including setting ownership and permissions, creation of both files and directories, and retrieving entire files from remote servers. As Puppet matures, it expected that the `file` resource will be used less and less to manage content, and instead native resources will be used to do so. If you find that you are often copying files in from a central location, rather than using native resources, please contact Puppet Labs and we can hopefully work with you to develop a native resource to support what you are doing. **Autorequires:** If Puppet is managing the user or group that owns a file, the file resource will autorequire them. If Puppet is managing any parent directories of a file, the file resource will autorequire them." def self.title_patterns [ [ /^(.*?)\/*\Z/m, [ [ :path, lambda{|x| x} ] ] ] ] end newparam(:path) do desc "The path to the file to manage. Must be fully qualified." isnamevar validate do |value| unless Puppet::Util.absolute_path?(value) fail Puppet::Error, "File paths must be fully qualified, not '#{value}'" end end # convert the current path in an index into the collection and the last # path name. The aim is to use less storage for all common paths in a hierarchy munge do |value| # We know the value is absolute, so expanding it will just standardize it. path, name = ::File.split(::File.expand_path(value)) { :index => Puppet::FileCollection.collection.index(path), :name => name } end # and the reverse unmunge do |value| basedir = Puppet::FileCollection.collection.path(value[:index]) ::File.expand_path ::File.join( basedir, value[:name] ) end end newparam(:backup) do desc "Whether files should be backed up before being replaced. The preferred method of backing files up is via a `filebucket`, which stores files by their MD5 sums and allows easy retrieval without littering directories with backups. You can specify a local filebucket or a network-accessible server-based filebucket by setting `backup => bucket-name`. Alternatively, if you specify any value that begins with a `.` (e.g., `.puppet-bak`), then Puppet will use copy the file in the same directory with that value as the extension of the backup. Setting `backup => false` disables all backups of the file in question. Puppet automatically creates a local filebucket named `puppet` and defaults to backing up there. To use a server-based filebucket, you must specify one in your configuration. filebucket { main: server => puppet, path => false, # The path => false line works around a known issue with the filebucket type. } The `puppet master` daemon creates a filebucket by default, so you can usually back up to your main server with this configuration. Once you've described the bucket in your configuration, you can use it in any file's backup attribute: file { \"/my/file\": source => \"/path/in/nfs/or/something\", backup => main } This will back the file up to the central server. At this point, the benefits of using a central filebucket are that you do not have backup files lying around on each of your machines, a given version of a file is only backed up once, you can restore any given file manually (no matter how old), and you can use Puppet Dashboard to view file contents. Eventually, transactional support will be able to automatically restore filebucketed files. " defaultto "puppet" munge do |value| # I don't really know how this is happening. value = value.shift if value.is_a?(Array) case value when false, "false", :false false when true, "true", ".puppet-bak", :true ".puppet-bak" when String value else self.fail "Invalid backup type #{value.inspect}" end end end newparam(:recurse) do desc "Whether and how deeply to do recursive management. Options are: * `inf,true` --- Regular style recursion on both remote and local directory structure. * `remote` --- Descends recursively into the remote directory but not the local directory. Allows copying of a few files into a directory containing many unmanaged files without scanning all the local files. * `false` --- Default of no recursion. * `[0-9]+` --- Same as true, but limit recursion. Warning: this syntax has been deprecated in favor of the `recurselimit` attribute. " newvalues(:true, :false, :inf, :remote, /^[0-9]+$/) # Replace the validation so that we allow numbers in # addition to string representations of them. validate { |arg| } munge do |value| newval = super(value) case newval when :true, :inf; true when :false; false when :remote; :remote when Integer, Fixnum, Bignum self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit" # recurse == 0 means no recursion return false if value == 0 resource[:recurselimit] = value true when /^\d+$/ self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit" value = Integer(value) # recurse == 0 means no recursion return false if value == 0 resource[:recurselimit] = value true else self.fail "Invalid recurse value #{value.inspect}" end end end newparam(:recurselimit) do desc "How deeply to do recursive management." newvalues(/^[0-9]+$/) munge do |value| newval = super(value) case newval when Integer, Fixnum, Bignum; value when /^\d+$/; Integer(value) else self.fail "Invalid recurselimit value #{value.inspect}" end end end newparam(:replace, :boolean => true) do desc "Whether or not to replace a file that is sourced but exists. This is useful for using file sources purely for initialization." newvalues(:true, :false) aliasvalue(:yes, :true) aliasvalue(:no, :false) defaultto :true end newparam(:force, :boolean => true) do desc "Force the file operation. Currently only used when replacing directories with links." newvalues(:true, :false) defaultto false end newparam(:ignore) do desc "A parameter which omits action on files matching specified patterns during recursion. Uses Ruby's builtin globbing engine, so shell metacharacters are fully supported, e.g. `[a-z]*`. Matches that would descend into the directory structure are ignored, e.g., `*/*`." validate do |value| unless value.is_a?(Array) or value.is_a?(String) or value == false self.devfail "Ignore must be a string or an Array" end end end newparam(:links) do desc "How to handle links during file actions. During file copying, `follow` will copy the target file instead of the link, `manage` will copy the link itself, and `ignore` will just pass it by. When not copying, `manage` and `ignore` behave equivalently (because you cannot really ignore links entirely during local recursion), and `follow` will manage the file to which the link points." newvalues(:follow, :manage) defaultto :manage end newparam(:purge, :boolean => true) do desc "Whether unmanaged files should be purged. If you have a filebucket configured the purged files will be uploaded, but if you do not, this will destroy data. Only use this option for generated files unless you really know what you are doing. This option only makes sense when recursively managing directories. Note that when using `purge` with `source`, Puppet will purge any files that are not on the remote system." defaultto :false newvalues(:true, :false) end newparam(:sourceselect) do desc "Whether to copy all valid sources, or just the first one. This parameter is only used in recursive copies; by default, the first valid source is the only one used as a recursive source, but if this parameter is set to `all`, then all valid sources will have all of their contents copied to the local host, and for sources that have the same file, the source earlier in the list will be used." defaultto :first newvalues(:first, :all) end # Autorequire the nearest ancestor directory found in the catalog. autorequire(:file) do path = Pathname(self[:path]) if !path.root? # Start at our parent, to avoid autorequiring ourself parents = path.parent.enum_for(:ascend) found = parents.find { |p| catalog.resource(:file, p.to_s) } found and found.to_s end end # Autorequire the owner and group of the file. {:user => :owner, :group => :group}.each do |type, property| autorequire(type) do if @parameters.include?(property) # The user/group property automatically converts to IDs next unless should = @parameters[property].shouldorig val = should[0] if val.is_a?(Integer) or val =~ /^\d+$/ nil else val end end end end CREATORS = [:content, :source, :target] SOURCE_ONLY_CHECKSUMS = [:none, :ctime, :mtime] validate do creator_count = 0 CREATORS.each do |param| creator_count += 1 if self.should(param) end creator_count += 1 if @parameters.include?(:source) self.fail "You cannot specify more than one of #{CREATORS.collect { |p| p.to_s}.join(", ")}" if creator_count > 1 self.fail "You cannot specify a remote recursion without a source" if !self[:source] and self[:recurse] == :remote self.fail "You cannot specify source when using checksum 'none'" if self[:checksum] == :none && !self[:source].nil? SOURCE_ONLY_CHECKSUMS.each do |checksum_type| self.fail "You cannot specify content when using checksum '#{checksum_type}'" if self[:checksum] == checksum_type && !self[:content].nil? end self.warning "Possible error: recurselimit is set but not recurse, no recursion will happen" if !self[:recurse] and self[:recurselimit] + + provider.validate if provider.respond_to?(:validate) end def self.[](path) return nil unless path super(path.gsub(/\/+/, '/').sub(/\/$/, '')) end def self.instances return [] end # Determine the user to write files as. def asuser if self.should(:owner) and ! self.should(:owner).is_a?(Symbol) writeable = Puppet::Util::SUIDManager.asuser(self.should(:owner)) { FileTest.writable?(::File.dirname(self[:path])) } # If the parent directory is writeable, then we execute # as the user in question. Otherwise we'll rely on # the 'owner' property to do things. asuser = self.should(:owner) if writeable end asuser end def bucket return @bucket if @bucket backup = self[:backup] return nil unless backup return nil if backup =~ /^\./ unless catalog or backup == "puppet" fail "Can not find filebucket for backups without a catalog" end unless catalog and filebucket = catalog.resource(:filebucket, backup) or backup == "puppet" fail "Could not find filebucket #{backup} specified in backup" end return default_bucket unless filebucket @bucket = filebucket.bucket @bucket end def default_bucket Puppet::Type.type(:filebucket).mkdefaultbucket.bucket end # Does the file currently exist? Just checks for whether # we have a stat def exist? stat ? true : false end # We have to do some extra finishing, to retrieve our bucket if # there is one. def finish # Look up our bucket, if there is one bucket super end # Create any children via recursion or whatever. def eval_generate return [] unless self.recurse? recurse #recurse.reject do |resource| # catalog.resource(:file, resource[:path]) #end.each do |child| # catalog.add_resource child # catalog.relationship_graph.add_edge self, child #end end def flush # We want to make sure we retrieve metadata anew on each transaction. @parameters.each do |name, param| param.flush if param.respond_to?(:flush) end @stat = :needs_stat end def initialize(hash) # Used for caching clients @clients = {} super # If they've specified a source, we get our 'should' values # from it. unless self[:ensure] if self[:target] self[:ensure] = :symlink elsif self[:content] self[:ensure] = :file end end @stat = :needs_stat end # Configure discovered resources to be purged. def mark_children_for_purging(children) children.each do |name, child| next if child[:source] child[:ensure] = :absent end end # Create a new file or directory object as a child to the current # object. def newchild(path) full_path = ::File.join(self[:path], path) # Add some new values to our original arguments -- these are the ones # set at initialization. We specifically want to exclude any param # values set by the :source property or any default values. # LAK:NOTE This is kind of silly, because the whole point here is that # the values set at initialization should live as long as the resource # but values set by default or by :source should only live for the transaction # or so. Unfortunately, we don't have a straightforward way to manage # the different lifetimes of this data, so we kludge it like this. # The right-side hash wins in the merge. options = @original_parameters.merge(:path => full_path).reject { |param, value| value.nil? } # These should never be passed to our children. [:parent, :ensure, :recurse, :recurselimit, :target, :alias, :source].each do |param| options.delete(param) if options.include?(param) end self.class.new(options) end # Files handle paths specially, because they just lengthen their # path names, rather than including the full parent's title each # time. def pathbuilder # We specifically need to call the method here, so it looks # up our parent in the catalog graph. if parent = parent() # We only need to behave specially when our parent is also # a file if parent.is_a?(self.class) # Remove the parent file name list = parent.pathbuilder list.pop # remove the parent's path info return list << self.ref else return super end else return [self.ref] end end # Should we be purging? def purge? @parameters.include?(:purge) and (self[:purge] == :true or self[:purge] == "true") end # Recursively generate a list of file resources, which will # be used to copy remote files, manage local files, and/or make links # to map to another directory. def recurse children = (self[:recurse] == :remote) ? {} : recurse_local if self[:target] recurse_link(children) elsif self[:source] recurse_remote(children) end # If we're purging resources, then delete any resource that isn't on the # remote system. mark_children_for_purging(children) if self.purge? result = children.values.sort { |a, b| a[:path] <=> b[:path] } remove_less_specific_files(result) end # This is to fix bug #2296, where two files recurse over the same # set of files. It's a rare case, and when it does happen you're # not likely to have many actual conflicts, which is good, because # this is a pretty inefficient implementation. def remove_less_specific_files(files) mypath = self[:path].split(::File::Separator) other_paths = catalog.vertices. select { |r| r.is_a?(self.class) and r[:path] != self[:path] }. collect { |r| r[:path].split(::File::Separator) }. select { |p| p[0,mypath.length] == mypath } return files if other_paths.empty? files.reject { |file| path = file[:path].split(::File::Separator) other_paths.any? { |p| path[0,p.length] == p } } end # A simple method for determining whether we should be recursing. def recurse? self[:recurse] == true or self[:recurse] == :remote end # Recurse the target of the link. def recurse_link(children) perform_recursion(self[:target]).each do |meta| if meta.relative_path == "." self[:ensure] = :directory next end children[meta.relative_path] ||= newchild(meta.relative_path) if meta.ftype == "directory" children[meta.relative_path][:ensure] = :directory else children[meta.relative_path][:ensure] = :link children[meta.relative_path][:target] = meta.full_path end end children end # Recurse the file itself, returning a Metadata instance for every found file. def recurse_local result = perform_recursion(self[:path]) return {} unless result result.inject({}) do |hash, meta| next hash if meta.relative_path == "." hash[meta.relative_path] = newchild(meta.relative_path) hash end end # Recurse against our remote file. def recurse_remote(children) sourceselect = self[:sourceselect] total = self[:source].collect do |source| next unless result = perform_recursion(source) return if top = result.find { |r| r.relative_path == "." } and top.ftype != "directory" result.each { |data| data.source = "#{source}/#{data.relative_path}" } break result if result and ! result.empty? and sourceselect == :first result end.flatten # This only happens if we have sourceselect == :all unless sourceselect == :first found = [] total.reject! do |data| result = found.include?(data.relative_path) found << data.relative_path unless found.include?(data.relative_path) result end end total.each do |meta| if meta.relative_path == "." parameter(:source).metadata = meta next end children[meta.relative_path] ||= newchild(meta.relative_path) children[meta.relative_path][:source] = meta.source children[meta.relative_path][:checksum] = :md5 if meta.ftype == "file" children[meta.relative_path].parameter(:source).metadata = meta end children end def perform_recursion(path) Puppet::FileServing::Metadata.indirection.search( path, :links => self[:links], :recurse => (self[:recurse] == :remote ? true : self[:recurse]), :recurselimit => self[:recurselimit], :ignore => self[:ignore], :checksum_type => (self[:source] || self[:content]) ? self[:checksum] : :none ) end # Remove any existing data. This is only used when dealing with # links or directories. def remove_existing(should) return unless s = stat self.fail "Could not back up; will not replace" unless perform_backup unless should.to_s == "link" return if s.ftype.to_s == should.to_s end case s.ftype when "directory" if self[:force] == :true debug "Removing existing directory for replacement with #{should}" FileUtils.rmtree(self[:path]) else notice "Not removing directory; use 'force' to override" return end when "link", "file" debug "Removing existing #{s.ftype} for replacement with #{should}" ::File.unlink(self[:path]) else self.fail "Could not back up files of type #{s.ftype}" end @stat = :needs_stat true end def retrieve if source = parameter(:source) source.copy_source_values end super end # Set the checksum, from another property. There are multiple # properties that modify the contents of a file, and they need the # ability to make sure that the checksum value is in sync. def setchecksum(sum = nil) if @parameters.include? :checksum if sum @parameters[:checksum].checksum = sum else # If they didn't pass in a sum, then tell checksum to # figure it out. currentvalue = @parameters[:checksum].retrieve @parameters[:checksum].checksum = currentvalue end end end # Should this thing be a normal file? This is a relatively complex # way of determining whether we're trying to create a normal file, # and it's here so that the logic isn't visible in the content property. def should_be_file? return true if self[:ensure] == :file # I.e., it's set to something like "directory" return false if e = self[:ensure] and e != :present # The user doesn't really care, apparently if self[:ensure] == :present return true unless s = stat return(s.ftype == "file" ? true : false) end # If we've gotten here, then :ensure isn't set return true if self[:content] return true if stat and stat.ftype == "file" false end # Stat our file. Depending on the value of the 'links' attribute, we # use either 'stat' or 'lstat', and we expect the properties to use the # resulting stat object accordingly (mostly by testing the 'ftype' # value). # # We use the initial value :needs_stat to ensure we only stat the file once, # but can also keep track of a failed stat (@stat == nil). This also allows # us to re-stat on demand by setting @stat = :needs_stat. def stat return @stat unless @stat == :needs_stat method = :stat # Files are the only types that support links if (self.class.name == :file and self[:links] != :follow) or self.class.name == :tidy method = :lstat end @stat = begin ::File.send(method, self[:path]) rescue Errno::ENOENT => error nil rescue Errno::EACCES => error warning "Could not stat; permission denied" nil end end # We have to hack this just a little bit, because otherwise we'll get # an error when the target and the contents are created as properties on # the far side. def to_trans(retrieve = true) obj = super obj.delete(:target) if obj[:target] == :notlink obj end # Write out the file. Requires the property name for logging. # Write will be done by the content property, along with checksum computation def write(property) remove_existing(:file) use_temporary_file = write_temporary_file? if use_temporary_file path = "#{self[:path]}.puppettmp_#{rand(10000)}" path = "#{self[:path]}.puppettmp_#{rand(10000)}" while ::File.exists?(path) or ::File.symlink?(path) else path = self[:path] end mode = self.should(:mode) # might be nil umask = mode ? 000 : 022 mode_int = mode ? mode.to_i(8) : nil content_checksum = Puppet::Util.withumask(umask) { ::File.open(path, 'wb', mode_int ) { |f| write_content(f) } } # And put our new file in place if use_temporary_file # This is only not true when our file is empty. begin fail_if_checksum_is_wrong(path, content_checksum) if validate_checksum? ::File.rename(path, self[:path]) rescue => detail fail "Could not rename temporary file #{path} to #{self[:path]}: #{detail}" ensure # Make sure the created file gets removed ::File.unlink(path) if FileTest.exists?(path) end end # make sure all of the modes are actually correct property_fix end private # Should we validate the checksum of the file we're writing? def validate_checksum? self[:checksum] !~ /time/ end # Make sure the file we wrote out is what we think it is. def fail_if_checksum_is_wrong(path, content_checksum) newsum = parameter(:checksum).sum_file(path) return if [:absent, nil, content_checksum].include?(newsum) self.fail "File written to disk did not match checksum; discarding changes (#{content_checksum} vs #{newsum})" end # write the current content. Note that if there is no content property # simply opening the file with 'w' as done in write is enough to truncate # or write an empty length file. def write_content(file) (content = property(:content)) && content.write(file) end private def write_temporary_file? # unfortunately we don't know the source file size before fetching it # so let's assume the file won't be empty (c = property(:content) and c.length) || (s = @parameters[:source] and 1) end # There are some cases where all of the work does not get done on # file creation/modification, so we have to do some extra checking. def property_fix properties.each do |thing| next unless [:mode, :owner, :group, :seluser, :selrole, :seltype, :selrange].include?(thing.name) # Make sure we get a new stat objct @stat = :needs_stat currentvalue = thing.retrieve thing.sync unless thing.safe_insync?(currentvalue) end end end # We put all of the properties in separate files, because there are so many # of them. The order these are loaded is important, because it determines # the order they are in the property lit. require 'puppet/type/file/checksum' require 'puppet/type/file/content' # can create the file require 'puppet/type/file/source' # can create the file require 'puppet/type/file/target' # creates a different type of file require 'puppet/type/file/ensure' # can create the file require 'puppet/type/file/owner' require 'puppet/type/file/group' require 'puppet/type/file/mode' require 'puppet/type/file/type' require 'puppet/type/file/selcontext' # SELinux file context require 'puppet/type/file/ctime' require 'puppet/type/file/mtime' diff --git a/lib/puppet/util/windows/security.rb b/lib/puppet/util/windows/security.rb index a17378bcc..368ec50d9 100644 --- a/lib/puppet/util/windows/security.rb +++ b/lib/puppet/util/windows/security.rb @@ -1,625 +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) - set_attributes(path, get_attributes(path) | flags) + oldattrs = get_attributes(path) + + if (oldattrs | flags) != oldattrs + set_attributes(path, oldattrs | flags) + end end def remove_attributes(path, flags) - set_attributes(path, get_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") if SetFileAttributes(path, flags) == 0 + 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 - # 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 + 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") if IsValidSid(sid_ptr) == 0 + raise Puppet::Util::Windows::Error.new("Invalid SID") unless IsValidSid(sid_ptr) - if AddAccessAllowedAceEx(acl, ACL_REVISION, inherit, mask, sid_ptr) == 0 + 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") if IsValidSid(sid_ptr) == 0 + raise Puppet::Util::Windows::Error.new("Invalid SID") unless IsValidSid(sid_ptr) - if AddAccessDeniedAce(acl, ACL_REVISION, mask, sid_ptr) == 0 + 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") if IsValidAcl(dacl_ptr) == 0 + 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 if GetAce(dacl_ptr, i, ace_ptr) == 0 + + 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") if IsValidSid(psid) == 0 + raise Puppet::Util::Windows::Error.new("Invalid SID") unless IsValidSid(psid) - raise Puppet::Util::Windows::Error.new("Failed to convert binary SID") if ConvertSidToStringSid(psid, str_ptr) == 0 + 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. - if LookupPrivilegeValue("", privilege, tmpLuid) == 0 + 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') - if AdjustTokenPrivileges(token, 0, tkp, tkp.length , nil, nil) == 0 + 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 - if OpenProcessToken(GetCurrentProcess(), access, token) == 0 + 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 0eb35fad5..57e1ea8d3 100755 --- a/spec/integration/util/windows/security_spec.rb +++ b/spec/integration/util/windows/security_spec.rb @@ -1,605 +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 - - after :each do - winsec.set_mode(WindowsSecurityTester::S_IRWXU, parent) - winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) if File.exists?(path) - 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 - describe "#owner=" do - it "should allow setting to the current user" do - winsec.set_owner(sids[:current_user], path) - end - - it "should raise an exception when setting to a different user" do - lambda { winsec.set_owner(sids[:guest], path) }.should raise_error(Puppet::Error, /This security ID may not be assigned as the owner of this object./) + winsec.send("get_#{p}", path).should be_nil end end + end - describe "#owner" do - it "it should not be empty" do - winsec.get_owner(path).should_not be_empty + 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 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./) + after :each do + winsec.set_mode(WindowsSecurityTester::S_IRWXU, parent) + winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) if File.exists?(path) 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 "#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 - # 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 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 "#group" do - it "should not be empty" do - winsec.get_group(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_group("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 "#mode=" do - (0000..0700).step(0100).each do |mode| - it "should enforce mode #{mode.to_s(8)}" do - winsec.set_mode(mode, path) + describe "#owner" do + it "it should not be empty" do + winsec.get_owner(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_owner("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 "#group=" do + it "should allow setting to a group the current owner is a member of" do + winsec.set_group(sids[:users], path) + end - 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 + # Unlike unix, if the user has permission to WRITE_OWNER, which the file owner has by default, + # then they can set the primary group to a group that the user does not belong to. + it "should allow setting to a group the current owner is not a member of" do + winsec.set_group(sids[:power_users], path) end end - describe "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" + describe "#group" do + it "should not be empty" do + winsec.get_group(path).should_not be_empty end - it "should combine user and group modes when owner and group sids are equal" do - winsec.set_group(winsec.get_owner(path), path) - - winsec.set_mode(0410, path) - winsec.get_mode(path).to_s(8).should == "550" + 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 - 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 + describe "#mode=" do + (0000..0700).step(0100).each do |mode| + it "should enforce mode #{mode.to_s(8)}" do + winsec.set_mode(mode, path) + + check_access(mode, path) + end 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 + it "should round-trip all 128 modes that do not require deny ACEs" do + 0.upto(1).each do |s| + 0.upto(7).each do |u| + 0.upto(u).each do |g| + 0.upto(g).each do |o| + # if user is superset of group, and group superset of other, then + # no deny ace is required, and mode can be converted to win32 + # access mask, and back to mode without loss of information + # (provided the owner and group are not the same) + next if ((u & g) != g) or ((g & o) != o) + + mode = (s << 9 | u << 6 | g << 3 | o << 0) + winsec.set_mode(mode, path) + winsec.get_mode(path).to_s(8).should == mode.to_s(8) + end + end + end + end end - it "should 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 + describe "for modes that require deny aces" do + it "should map everyone to group and owner" do + winsec.set_mode(0426, path) + winsec.get_mode(path).to_s(8).should == "666" + end + + it "should combine user and group modes when owner and group sids are equal" do + winsec.set_group(winsec.get_owner(path), path) + + winsec.set_mode(0410, path) + winsec.get_mode(path).to_s(8).should == "550" + end end - 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 "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 "#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}") + 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 + 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 - 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_owner(sids[:guest], path) - winsec.set_group(sids[:guest], path) - winsec.set_mode(WindowsSecurityTester::S_IRWXU | WindowsSecurityTester::S_IRWXG, 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 allow owner and group to be the same sid" do - winsec.set_owner(sids[:power_users], path) - winsec.set_group(sids[:power_users], path) - winsec.set_mode(0610, path) + it "should accept a user sid" do + winsec.set_group(sids[:admin], path) + winsec.get_group(path).should == sids[:admin] + 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 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 - 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 sid is provided" do + lambda { winsec.set_group("foobar", path) }.should raise_error(Puppet::Error, /Failed to convert string SID/) + end + + it "should raise an exception if an invalid path is provided" do + lambda { winsec.set_group(sids[:guest], "c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) + end end - 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/lib/puppet_spec/files.rb b/spec/lib/puppet_spec/files.rb index 86afafbef..49e50a032 100755 --- a/spec/lib/puppet_spec/files.rb +++ b/spec/lib/puppet_spec/files.rb @@ -1,61 +1,57 @@ require 'fileutils' require 'tempfile' require 'pathname' # A support module for testing files. module PuppetSpec::Files # This code exists only to support tests that run as root, pretty much. # Once they have finally been eliminated this can all go... --daniel 2011-04-08 def self.in_tmp(path) tempdir = Dir.tmpdir Pathname.new(path).ascend do |dir| return true if File.identical?(tempdir, dir) end false end def self.cleanup $global_tempfiles ||= [] while path = $global_tempfiles.pop do fail "Not deleting tmpfile #{path} outside regular tmpdir" unless in_tmp(path) begin FileUtils.rm_r path, :secure => true rescue Errno::ENOENT # nothing to do end end end def make_absolute(path) - return path unless Puppet.features.microsoft_windows? - # REMIND UNC - return path if path =~ /^[A-Za-z]:/ - - pwd = Dir.getwd - return "#{pwd[0,2]}#{path}" if pwd =~ /^[A-Za-z]:/ - return "C:#{path}" + path = File.expand_path(path) + path[0] = 'c' if Puppet.features.microsoft_windows? + path end def tmpfile(name) # Generate a temporary file, just for the name... source = Tempfile.new(name) path = source.path source.close! # ...record it for cleanup, $global_tempfiles ||= [] $global_tempfiles << File.expand_path(path) # ...and bam. path end def tmpdir(name) path = tmpfile(name) FileUtils.mkdir_p(path) path end end diff --git a/spec/unit/configurer/downloader_spec.rb b/spec/unit/configurer/downloader_spec.rb index abb54a740..215ef897f 100755 --- a/spec/unit/configurer/downloader_spec.rb +++ b/spec/unit/configurer/downloader_spec.rb @@ -1,200 +1,200 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/configurer/downloader' describe Puppet::Configurer::Downloader do require 'puppet_spec/files' include PuppetSpec::Files it "should require a name" do lambda { Puppet::Configurer::Downloader.new }.should raise_error(ArgumentError) end it "should require a path and a source at initialization" do lambda { Puppet::Configurer::Downloader.new("name") }.should raise_error(ArgumentError) end it "should set the name, path and source appropriately" do dler = Puppet::Configurer::Downloader.new("facts", "path", "source") dler.name.should == "facts" dler.path.should == "path" dler.source.should == "source" end it "should be able to provide a timeout value" do Puppet::Configurer::Downloader.should respond_to(:timeout) end it "should use the configtimeout, converted to an integer, as its timeout" do Puppet.settings.expects(:value).with(:configtimeout).returns "50" Puppet::Configurer::Downloader.timeout.should == 50 end describe "when creating the file that does the downloading" do before do @dler = Puppet::Configurer::Downloader.new("foo", "path", "source") end it "should create a file instance with the right path and source" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:path] == "path" and opts[:source] == "source" } @dler.file end it "should tag the file with the downloader name" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:tag] == "foo" } @dler.file end it "should always recurse" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:recurse] == true } @dler.file end it "should always purge" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:purge] == true } @dler.file end it "should never be in noop" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:noop] == false } @dler.file end it "should always set the owner to the current UID" do Process.expects(:uid).returns 51 Puppet::Type.type(:file).expects(:new).with { |opts| opts[:owner] == 51 } @dler.file end it "should always set the group to the current GID" do Process.expects(:gid).returns 61 Puppet::Type.type(:file).expects(:new).with { |opts| opts[:group] == 61 } @dler.file end it "should always force the download" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:force] == true } @dler.file end it "should never back up when downloading" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:backup] == false } @dler.file end it "should support providing an 'ignore' parameter" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:ignore] == [".svn"] } @dler = Puppet::Configurer::Downloader.new("foo", "path", "source", ".svn") @dler.file end it "should split the 'ignore' parameter on whitespace" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:ignore] == %w{.svn CVS} } @dler = Puppet::Configurer::Downloader.new("foo", "path", "source", ".svn CVS") @dler.file end end describe "when creating the catalog to do the downloading" do before do - @path = File.expand_path("/download/path") - @dler = Puppet::Configurer::Downloader.new("foo", @path, File.expand_path("source")) + @path = make_absolute("/download/path") + @dler = Puppet::Configurer::Downloader.new("foo", @path, make_absolute("source")) end it "should create a catalog and add the file to it" do catalog = @dler.catalog catalog.resources.size.should == 1 catalog.resources.first.class.should == Puppet::Type::File catalog.resources.first.name.should == @path end it "should specify that it is not managing a host catalog" do @dler.catalog.host_config.should == false end end describe "when downloading" do before do @dl_name = tmpfile("downloadpath") source_name = tmpfile("source") File.open(source_name, 'w') {|f| f.write('hola mundo') } @dler = Puppet::Configurer::Downloader.new("foo", @dl_name, source_name) end it "should not skip downloaded resources when filtering on tags" do Puppet[:tags] = 'maytag' @dler.evaluate File.exists?(@dl_name).should be_true end it "should log that it is downloading" do Puppet.expects(:info) Timeout.stubs(:timeout) @dler.evaluate end it "should set a timeout for the download" do Puppet::Configurer::Downloader.expects(:timeout).returns 50 Timeout.expects(:timeout).with(50) @dler.evaluate end it "should apply the catalog within the timeout block" do catalog = mock 'catalog' @dler.expects(:catalog).returns(catalog) Timeout.expects(:timeout).yields catalog.expects(:apply) @dler.evaluate end it "should return all changed file paths" do trans = mock 'transaction' catalog = mock 'catalog' @dler.expects(:catalog).returns(catalog) catalog.expects(:apply).yields(trans) Timeout.expects(:timeout).yields resource = mock 'resource' resource.expects(:[]).with(:path).returns "/changed/file" trans.expects(:changed?).returns([resource]) @dler.evaluate.should == %w{/changed/file} end it "should yield the resources if a block is given" do trans = mock 'transaction' catalog = mock 'catalog' @dler.expects(:catalog).returns(catalog) catalog.expects(:apply).yields(trans) Timeout.expects(:timeout).yields resource = mock 'resource' resource.expects(:[]).with(:path).returns "/changed/file" trans.expects(:changed?).returns([resource]) yielded = nil @dler.evaluate { |r| yielded = r } yielded.should == resource end it "should catch and log exceptions" do Puppet.expects(:err) Timeout.stubs(:timeout).raises(Puppet::Error, "testing") lambda { @dler.evaluate }.should_not raise_error 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 diff --git a/spec/unit/provider/file/posix_spec.rb b/spec/unit/provider/file/posix_spec.rb index 32944f77b..c7a33510a 100755 --- a/spec/unit/provider/file/posix_spec.rb +++ b/spec/unit/provider/file/posix_spec.rb @@ -1,226 +1,232 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Type.type(:file).provider(:posix), :if => Puppet.features.posix? do include PuppetSpec::Files let(:path) { tmpfile('posix_file_spec') } let(:resource) { Puppet::Type.type(:file).new :path => path, :mode => 0777, :provider => described_class.name } let(:provider) { resource.provider } describe "#mode" do it "should return a string with the higher-order bits stripped away" do FileUtils.touch(path) File.chmod(0644, path) provider.mode.should == '644' end it "should return absent if the file doesn't exist" do provider.mode.should == :absent end end describe "#mode=" do it "should chmod the file to the specified value" do FileUtils.touch(path) File.chmod(0644, path) provider.mode = '0755' provider.mode.should == '755' end it "should pass along any errors encountered" do expect do provider.mode = '644' end.to raise_error(Puppet::Error, /failed to set mode/) end end describe "#uid2name" do it "should return the name of the user identified by the id" do Etc.stubs(:getpwuid).with(501).returns(Struct::Passwd.new('jilluser', nil, 501)) provider.uid2name(501).should == 'jilluser' end it "should return the argument if it's already a name" do provider.uid2name('jilluser').should == 'jilluser' end it "should return nil if the argument is above the maximum uid" do provider.uid2name(Puppet[:maximum_uid] + 1).should == nil end it "should return nil if the user doesn't exist" do Etc.expects(:getpwuid).raises(ArgumentError, "can't find user for 999") provider.uid2name(999).should == nil end end describe "#name2uid" do it "should return the id of the user if it exists" do passwd = Struct::Passwd.new('bobbo', nil, 502) Etc.stubs(:getpwnam).with('bobbo').returns(passwd) Etc.stubs(:getpwuid).with(502).returns(passwd) provider.name2uid('bobbo').should == 502 end it "should return the argument if it's already an id" do provider.name2uid('503').should == 503 end it "should return false if the user doesn't exist" do Etc.stubs(:getpwnam).with('chuck').raises(ArgumentError, "can't find user for chuck") provider.name2uid('chuck').should == false end end describe "#owner" do it "should return the uid of the file owner" do FileUtils.touch(path) owner = File.stat(path).uid provider.owner.should == owner end it "should return absent if the file can't be statted" do provider.owner.should == :absent end it "should warn and return :silly if the value is beyond the maximum uid" do stat = stub('stat', :uid => Puppet[:maximum_uid] + 1) resource.stubs(:stat).returns(stat) provider.owner.should == :silly @logs.should be_any {|log| log.level == :warning and log.message =~ /Apparently using negative UID/} end end describe "#owner=" do it "should set the owner but not the group of the file" do File.expects(:lchown).with(15, nil, resource[:path]) provider.owner = 15 end it "should chown a link if managing links" do resource[:links] = :manage File.expects(:lchown).with(20, nil, resource[:path]) provider.owner = 20 end it "should chown a link target if following links" do resource[:links] = :follow File.expects(:chown).with(20, nil, resource[:path]) provider.owner = 20 end it "should pass along any error encountered setting the owner" do File.expects(:lchown).raises(ArgumentError) expect { provider.owner = 25 }.to raise_error(Puppet::Error, /Failed to set owner to '25'/) end end describe "#gid2name" do it "should return the name of the group identified by the id" do Etc.stubs(:getgrgid).with(501).returns(Struct::Passwd.new('unicorns', nil, nil, 501)) provider.gid2name(501).should == 'unicorns' end it "should return the argument if it's already a name" do provider.gid2name('leprechauns').should == 'leprechauns' end it "should return nil if the argument is above the maximum gid" do provider.gid2name(Puppet[:maximum_uid] + 1).should == nil end it "should return nil if the group doesn't exist" do Etc.expects(:getgrgid).raises(ArgumentError, "can't find group for 999") provider.gid2name(999).should == nil end end describe "#name2gid" do it "should return the id of the group if it exists" do passwd = Struct::Passwd.new('penguins', nil, nil, 502) Etc.stubs(:getgrnam).with('penguins').returns(passwd) Etc.stubs(:getgrgid).with(502).returns(passwd) provider.name2gid('penguins').should == 502 end it "should return the argument if it's already an id" do provider.name2gid('503').should == 503 end it "should return false if the group doesn't exist" do Etc.stubs(:getgrnam).with('wombats').raises(ArgumentError, "can't find group for wombats") provider.name2gid('wombats').should == false end end describe "#group" do it "should return the gid of the file group" do FileUtils.touch(path) group = File.stat(path).gid provider.group.should == group end it "should return absent if the file can't be statted" do provider.group.should == :absent end it "should warn and return :silly if the value is beyond the maximum gid" do stat = stub('stat', :gid => Puppet[:maximum_uid] + 1) resource.stubs(:stat).returns(stat) provider.group.should == :silly @logs.should be_any {|log| log.level == :warning and log.message =~ /Apparently using negative GID/} end end describe "#group=" do it "should set the group but not the owner of the file" do File.expects(:lchown).with(nil, 15, resource[:path]) provider.group = 15 end it "should change the group for a link if managing links" do resource[:links] = :manage File.expects(:lchown).with(nil, 20, resource[:path]) provider.group = 20 end it "should change the group for a link target if following links" do resource[:links] = :follow File.expects(:chown).with(nil, 20, resource[:path]) provider.group = 20 end it "should pass along any error encountered setting the group" do File.expects(:lchown).raises(ArgumentError) expect { provider.group = 25 }.to raise_error(Puppet::Error, /Failed to set group to '25'/) end end + + describe "when validating" do + it "should not perform any validation" do + resource.validate + end + end end diff --git a/spec/unit/provider/file/windows_spec.rb b/spec/unit/provider/file/windows_spec.rb old mode 100644 new mode 100755 index 6cedeebf0..f9ab78615 --- a/spec/unit/provider/file/windows_spec.rb +++ b/spec/unit/provider/file/windows_spec.rb @@ -1,136 +1,154 @@ #!/usr/bin/env rspec require 'spec_helper' if Puppet.features.microsoft_windows? require 'puppet/util/windows' class WindowsSecurity extend Puppet::Util::Windows::Security end end describe Puppet::Type.type(:file).provider(:windows), :if => Puppet.features.microsoft_windows? do include PuppetSpec::Files let(:path) { tmpfile('windows_file_spec') } let(:resource) { Puppet::Type.type(:file).new :path => path, :mode => 0777, :provider => described_class.name } let(:provider) { resource.provider } describe "#mode" do it "should return a string with the higher-order bits stripped away" do FileUtils.touch(path) WindowsSecurity.set_mode(0644, path) provider.mode.should == '644' end it "should return absent if the file doesn't exist" do provider.mode.should == :absent end end describe "#mode=" do it "should chmod the file to the specified value" do FileUtils.touch(path) WindowsSecurity.set_mode(0644, path) provider.mode = '0755' provider.mode.should == '755' end it "should pass along any errors encountered" do expect do provider.mode = '644' end.to raise_error(Puppet::Error, /failed to set mode/) end end describe "#id2name" do it "should return the name of the user identified by the sid" do result = [stub('user', :name => 'quinn')] Puppet::Util::ADSI.stubs(:execquery).returns(result) provider.id2name('S-1-1-50').should == 'quinn' end it "should return the argument if it's already a name" do provider.id2name('flannigan').should == 'flannigan' end it "should return nil if the user doesn't exist" do Puppet::Util::ADSI.stubs(:execquery).returns [] provider.id2name('S-1-1-50').should == nil end end describe "#name2id" do it "should return the sid of the user" do Puppet::Util::ADSI.stubs(:execquery).returns [stub('account', :Sid => 'S-1-1-50')] provider.name2id('anybody').should == 'S-1-1-50' end it "should return the argument if it's already a sid" do provider.name2id('S-1-1-50').should == 'S-1-1-50' end it "should return nil if the user doesn't exist" do Puppet::Util::ADSI.stubs(:execquery).returns [] provider.name2id('someone').should == nil end end describe "#owner" do it "should return the sid of the owner if the file does exist" do FileUtils.touch(resource[:path]) provider.stubs(:get_owner).with(resource[:path]).returns('S-1-1-50') provider.owner.should == 'S-1-1-50' end it "should return absent if the file doesn't exist" do provider.owner.should == :absent end end describe "#owner=" do it "should set the owner to the specified value" do provider.expects(:set_owner).with('S-1-1-50', resource[:path]) provider.owner = 'S-1-1-50' end it "should propagate any errors encountered when setting the owner" do provider.stubs(:set_owner).raises(ArgumentError) expect { provider.owner = 'S-1-1-50' }.to raise_error(Puppet::Error, /Failed to set owner/) end end describe "#group" do it "should return the sid of the group if the file does exist" do FileUtils.touch(resource[:path]) provider.stubs(:get_group).with(resource[:path]).returns('S-1-1-50') provider.group.should == 'S-1-1-50' end it "should return absent if the file doesn't exist" do provider.group.should == :absent end end describe "#group=" do it "should set the group to the specified value" do provider.expects(:set_group).with('S-1-1-50', resource[:path]) provider.group = 'S-1-1-50' end it "should propagate any errors encountered when setting the group" do provider.stubs(:set_group).raises(ArgumentError) expect { provider.group = 'S-1-1-50' }.to raise_error(Puppet::Error, /Failed to set group/) end end + + describe "when validating" do + {:owner => 'foo', :group => 'foo', :mode => 0777}.each do |k,v| + it "should fail if the filesystem doesn't support ACLs and we're managing #{k}" do + described_class.any_instance.stubs(:supports_acl?).returns false + + expect { + Puppet::Type.type(:file).new :path => path, k => v + }.to raise_error(Puppet::Error, /Can only manage owner, group, and mode on filesystems that support Windows ACLs, such as NTFS/) + end + end + + it "should not fail if the filesystem doesn't support ACLs and we're not managing permissions" do + described_class.any_instance.stubs(:supports_acl?).returns false + + Puppet::Type.type(:file).new :path => path + end + end end diff --git a/spec/unit/util/rdoc/parser_spec.rb b/spec/unit/util/rdoc/parser_spec.rb index 29e3298f0..9c8cc7588 100755 --- a/spec/unit/util/rdoc/parser_spec.rb +++ b/spec/unit/util/rdoc/parser_spec.rb @@ -1,568 +1,568 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/resource/type_collection' require 'puppet/util/rdoc/parser' require 'puppet/util/rdoc/code_objects' require 'rdoc/options' require 'rdoc/rdoc' describe RDoc::Parser, :'fails_on_ruby_1.9.2' => true do include PuppetSpec::Files before :each do File.stubs(:stat).with("init.pp") @top_level = stub_everything 'toplevel', :file_relative_name => "init.pp" @parser = RDoc::Parser.new(@top_level, "module/manifests/init.pp", nil, Options.instance, RDoc::Stats.new) end describe "when scanning files" do it "should parse puppet files with the puppet parser" do @parser.stubs(:scan_top_level) parser = stub 'parser' Puppet::Parser::Parser.stubs(:new).returns(parser) parser.expects(:parse).returns(Puppet::Parser::AST::Hostclass.new('')).at_least_once parser.expects(:file=).with("module/manifests/init.pp") - parser.expects(:file=).with(make_absolute("/dev/null/manifests/site.pp")) + parser.expects(:file=).with(File.expand_path("/dev/null/manifests/site.pp")) @parser.scan end it "should scan the ast for Puppet files" do parser = stub_everything 'parser' Puppet::Parser::Parser.stubs(:new).returns(parser) parser.expects(:parse).returns(Puppet::Parser::AST::Hostclass.new('')).at_least_once @parser.expects(:scan_top_level) @parser.scan end it "should return a PuppetTopLevel to RDoc" do parser = stub_everything 'parser' Puppet::Parser::Parser.stubs(:new).returns(parser) parser.expects(:parse).returns(Puppet::Parser::AST::Hostclass.new('')).at_least_once @parser.expects(:scan_top_level) @parser.scan.should be_a(RDoc::PuppetTopLevel) end it "should scan the top level even if the file has already parsed" do known_type = stub 'known_types' env = stub 'env' Puppet::Node::Environment.stubs(:new).returns(env) env.stubs(:known_resource_types).returns(known_type) known_type.expects(:watching_file?).with("module/manifests/init.pp").returns(true) @parser.expects(:scan_top_level) @parser.scan end end describe "when scanning top level entities" do before :each do @resource_type_collection = resource_type_collection = stub_everything('resource_type_collection') @parser.instance_eval { @known_resource_types = resource_type_collection } @parser.stubs(:split_module).returns("module") @topcontainer = stub_everything 'topcontainer' @container = stub_everything 'container' @module = stub_everything 'module' @container.stubs(:add_module).returns(@module) @parser.stubs(:get_class_or_module).returns([@container, "module"]) end it "should read any present README as module documentation" do FileTest.stubs(:readable?).returns(true) File.stubs(:open).returns("readme") @parser.stubs(:parse_elements) @module.expects(:comment=).with("readme") @parser.scan_top_level(@topcontainer) end it "should tell the container its module name" do @parser.stubs(:parse_elements) @topcontainer.expects(:module_name=).with("module") @parser.scan_top_level(@topcontainer) end it "should not document our toplevel if it isn't a valid module" do @parser.stubs(:split_module).returns(nil) @topcontainer.expects(:document_self=).with(false) @parser.expects(:parse_elements).never @parser.scan_top_level(@topcontainer) end it "should set the module as global if we parse the global manifests (ie __site__ module)" do @parser.stubs(:split_module).returns(RDoc::Parser::SITE) @parser.stubs(:parse_elements) @topcontainer.expects(:global=).with(true) @parser.scan_top_level(@topcontainer) end it "should attach this module container to the toplevel container" do @parser.stubs(:parse_elements) @container.expects(:add_module).with(RDoc::PuppetModule, "module").returns(@module) @parser.scan_top_level(@topcontainer) end it "should defer ast parsing to parse_elements for this module" do @parser.expects(:parse_elements).with(@module) @parser.scan_top_level(@topcontainer) end it "should defer plugins parsing to parse_plugins for this module" do @parser.input_file_name = "module/lib/puppet/parser/function.rb" @parser.expects(:parse_plugins).with(@module) @parser.scan_top_level(@topcontainer) end end describe "when finding modules from filepath" do before :each do Puppet::Module.stubs(:modulepath).returns("/path/to/modules") end it "should return the module name for modulized puppet manifests" do File.stubs(:expand_path).returns("/path/to/module/manifests/init.pp") File.stubs(:identical?).with("/path/to", "/path/to/modules").returns(true) @parser.split_module("/path/to/modules/mymodule/manifests/init.pp").should == "module" end it "should return for manifests not under module path" do File.stubs(:expand_path).returns("/path/to/manifests/init.pp") File.stubs(:identical?).returns(false) @parser.split_module("/path/to/manifests/init.pp").should == RDoc::Parser::SITE end it "should handle windows paths with drive letters", :if => Puppet.features.microsoft_windows? do @parser.split_module("C:/temp/init.pp").should == RDoc::Parser::SITE end end describe "when parsing AST elements" do before :each do @klass = stub_everything 'klass', :file => "module/manifests/init.pp", :name => "myclass", :type => :hostclass @definition = stub_everything 'definition', :file => "module/manifests/init.pp", :type => :definition, :name => "mydef" @node = stub_everything 'node', :file => "module/manifests/init.pp", :type => :node, :name => "mynode" @resource_type_collection = resource_type_collection = Puppet::Resource::TypeCollection.new("env") @parser.instance_eval { @known_resource_types = resource_type_collection } @container = stub_everything 'container' end it "should document classes in the parsed file" do @resource_type_collection.add_hostclass(@klass) @parser.expects(:document_class).with("myclass", @klass, @container) @parser.parse_elements(@container) end it "should not document class parsed in an other file" do @klass.stubs(:file).returns("/not/same/path/file.pp") @resource_type_collection.add_hostclass(@klass) @parser.expects(:document_class).with("myclass", @klass, @container).never @parser.parse_elements(@container) end it "should document vardefs for the main class" do @klass.stubs(:name).returns :main @resource_type_collection.add_hostclass(@klass) code = stub 'code', :is_a? => false @klass.stubs(:name).returns("") @klass.stubs(:code).returns(code) @parser.expects(:scan_for_vardef).with(@container, code) @parser.parse_elements(@container) end it "should document definitions in the parsed file" do @resource_type_collection.add_definition(@definition) @parser.expects(:document_define).with("mydef", @definition, @container) @parser.parse_elements(@container) end it "should not document definitions parsed in an other file" do @definition.stubs(:file).returns("/not/same/path/file.pp") @resource_type_collection.add_definition(@definition) @parser.expects(:document_define).with("mydef", @definition, @container).never @parser.parse_elements(@container) end it "should document nodes in the parsed file" do @resource_type_collection.add_node(@node) @parser.expects(:document_node).with("mynode", @node, @container) @parser.parse_elements(@container) end it "should not document node parsed in an other file" do @node.stubs(:file).returns("/not/same/path/file.pp") @resource_type_collection.add_node(@node) @parser.expects(:document_node).with("mynode", @node, @container).never @parser.parse_elements(@container) end end describe "when documenting definition" do before(:each) do @define = stub_everything 'define', :arguments => [], :doc => "mydoc", :file => "file", :line => 42 @class = stub_everything 'class' @parser.stubs(:get_class_or_module).returns([@class, "mydef"]) end it "should register a RDoc method to the current container" do @class.expects(:add_method).with { |m| m.name == "mydef"} @parser.document_define("mydef", @define, @class) end it "should attach the documentation to this method" do @class.expects(:add_method).with { |m| m.comment = "mydoc" } @parser.document_define("mydef", @define, @class) end it "should produce a better error message on unhandled exception" do @class.expects(:add_method).raises(ArgumentError) lambda { @parser.document_define("mydef", @define, @class) }.should raise_error(Puppet::ParseError, /in file at line 42/) end it "should convert all definition parameter to string" do arg = stub 'arg' val = stub 'val' @define.stubs(:arguments).returns({arg => val}) arg.expects(:to_s).returns("arg") val.expects(:to_s).returns("val") @parser.document_define("mydef", @define, @class) end end describe "when documenting nodes" do before :each do @code = stub_everything 'code' @node = stub_everything 'node', :doc => "mydoc", :parent => "parent", :code => @code, :file => "file", :line => 42 @rdoc_node = stub_everything 'rdocnode' @class = stub_everything 'class' @class.stubs(:add_node).returns(@rdoc_node) end it "should add a node to the current container" do @class.expects(:add_node).with("mynode", "parent").returns(@rdoc_node) @parser.document_node("mynode", @node, @class) end it "should associate the node documentation to the rdoc node" do @rdoc_node.expects(:comment=).with("mydoc") @parser.document_node("mynode", @node, @class) end it "should scan for include and require" do @parser.expects(:scan_for_include_or_require).with(@rdoc_node, @code) @parser.document_node("mynode", @node, @class) end it "should scan for variable definition" do @parser.expects(:scan_for_vardef).with(@rdoc_node, @code) @parser.document_node("mynode", @node, @class) end it "should scan for resources if needed" do Puppet.settings.stubs(:[]).with(:document_all).returns(true) @parser.expects(:scan_for_resource).with(@rdoc_node, @code) @parser.document_node("mynode", @node, @class) end it "should produce a better error message on unhandled exception" do @class.stubs(:add_node).raises(ArgumentError) lambda { @parser.document_node("mynode", @node, @class) }.should raise_error(Puppet::ParseError, /in file at line 42/) end end describe "when documenting classes" do before :each do @code = stub_everything 'code' @class = stub_everything 'class', :doc => "mydoc", :parent => "parent", :code => @code, :file => "file", :line => 42 @rdoc_class = stub_everything 'rdoc-class' @module = stub_everything 'class' @module.stubs(:add_class).returns(@rdoc_class) @parser.stubs(:get_class_or_module).returns([@module, "myclass"]) end it "should add a class to the current container" do @module.expects(:add_class).with(RDoc::PuppetClass, "myclass", "parent").returns(@rdoc_class) @parser.document_class("mynode", @class, @module) end it "should set the superclass" do @rdoc_class.expects(:superclass=).with("parent") @parser.document_class("mynode", @class, @module) end it "should associate the node documentation to the rdoc class" do @rdoc_class.expects(:comment=).with("mydoc") @parser.document_class("mynode", @class, @module) end it "should scan for include and require" do @parser.expects(:scan_for_include_or_require).with(@rdoc_class, @code) @parser.document_class("mynode", @class, @module) end it "should scan for resources if needed" do Puppet.settings.stubs(:[]).with(:document_all).returns(true) @parser.expects(:scan_for_resource).with(@rdoc_class, @code) @parser.document_class("mynode", @class, @module) end it "should produce a better error message on unhandled exception" do @module.stubs(:add_class).raises(ArgumentError) lambda { @parser.document_class("mynode", @class, @module) }.should raise_error(Puppet::ParseError, /in file at line 42/) end end describe "when scanning for includes and requires" do def create_stmt(name) stmt_value = stub "#{name}_value", :to_s => "myclass" Puppet::Parser::AST::Function.new( :name => name, :arguments => [stmt_value], :doc => 'mydoc' ) end before(:each) do @class = stub_everything 'class' @code = stub_everything 'code' @code.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(true) end it "should also scan mono-instruction code" do @class.expects(:add_include).with { |i| i.is_a?(RDoc::Include) and i.name == "myclass" and i.comment == "mydoc" } @parser.scan_for_include_or_require(@class, create_stmt("include")) end it "should register recursively includes to the current container" do @code.stubs(:children).returns([ create_stmt("include") ]) @class.expects(:add_include)#.with { |i| i.is_a?(RDoc::Include) and i.name == "myclass" and i.comment == "mydoc" } @parser.scan_for_include_or_require(@class, [@code]) end it "should register requires to the current container" do @code.stubs(:children).returns([ create_stmt("require") ]) @class.expects(:add_require).with { |i| i.is_a?(RDoc::Include) and i.name == "myclass" and i.comment == "mydoc" } @parser.scan_for_include_or_require(@class, [@code]) end end describe "when scanning for realized virtual resources" do def create_stmt stmt_value = stub "resource_ref", :to_s => "File[\"/tmp/a\"]" Puppet::Parser::AST::Function.new( :name => 'realize', :arguments => [stmt_value], :doc => 'mydoc' ) end before(:each) do @class = stub_everything 'class' @code = stub_everything 'code' @code.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(true) end it "should also scan mono-instruction code" do @class.expects(:add_realize).with { |i| i.is_a?(RDoc::Include) and i.name == "File[\"/tmp/a\"]" and i.comment == "mydoc" } @parser.scan_for_realize(@class,create_stmt) end it "should register recursively includes to the current container" do @code.stubs(:children).returns([ create_stmt ]) @class.expects(:add_realize).with { |i| i.is_a?(RDoc::Include) and i.name == "File[\"/tmp/a\"]" and i.comment == "mydoc" } @parser.scan_for_realize(@class, [@code]) end end describe "when scanning for variable definition" do before :each do @class = stub_everything 'class' @stmt = stub_everything 'stmt', :name => "myvar", :value => "myvalue", :doc => "mydoc" @stmt.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(false) @stmt.stubs(:is_a?).with(Puppet::Parser::AST::VarDef).returns(true) @code = stub_everything 'code' @code.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(true) end it "should recursively register variables to the current container" do @code.stubs(:children).returns([ @stmt ]) @class.expects(:add_constant).with { |i| i.is_a?(RDoc::Constant) and i.name == "myvar" and i.comment == "mydoc" } @parser.scan_for_vardef(@class, [ @code ]) end it "should also scan mono-instruction code" do @class.expects(:add_constant).with { |i| i.is_a?(RDoc::Constant) and i.name == "myvar" and i.comment == "mydoc" } @parser.scan_for_vardef(@class, @stmt) end end describe "when scanning for resources" do before :each do @class = stub_everything 'class' @stmt = Puppet::Parser::AST::Resource.new( :type => "File", :instances => Puppet::Parser::AST::ASTArray.new(:children => [ Puppet::Parser::AST::ResourceInstance.new( :title => Puppet::Parser::AST::Name.new(:value => "myfile"), :parameters => Puppet::Parser::AST::ASTArray.new(:children => []) ) ]), :doc => 'mydoc' ) @code = stub_everything 'code' @code.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(true) end it "should register a PuppetResource to the current container" do @code.stubs(:children).returns([ @stmt ]) @class.expects(:add_resource).with { |i| i.is_a?(RDoc::PuppetResource) and i.title == "myfile" and i.comment == "mydoc" } @parser.scan_for_resource(@class, [ @code ]) end it "should also scan mono-instruction code" do @class.expects(:add_resource).with { |i| i.is_a?(RDoc::PuppetResource) and i.title == "myfile" and i.comment == "mydoc" } @parser.scan_for_resource(@class, @stmt) end end describe "when parsing plugins" do before :each do @container = stub 'container' end it "should delegate parsing custom facts to parse_facts" do @parser = RDoc::Parser.new(@top_level, "module/manifests/lib/puppet/facter/test.rb", nil, Options.instance, RDoc::Stats.new) @parser.expects(:parse_fact).with(@container) @parser.parse_plugins(@container) end it "should delegate parsing plugins to parse_plugins" do @parser = RDoc::Parser.new(@top_level, "module/manifests/lib/puppet/functions/test.rb", nil, Options.instance, RDoc::Stats.new) @parser.expects(:parse_puppet_plugin).with(@container) @parser.parse_plugins(@container) end end describe "when parsing plugins" do before :each do @container = stub_everything 'container' end it "should add custom functions to the container" do File.stubs(:open).yields("# documentation module Puppet::Parser::Functions newfunction(:myfunc, :type => :rvalue) do |args| File.dirname(args[0]) end end".split("\n")) @container.expects(:add_plugin).with do |plugin| plugin.comment == "documentation\n" #and plugin.name == "myfunc" end @parser.parse_puppet_plugin(@container) end it "should add custom types to the container" do File.stubs(:open).yields("# documentation Puppet::Type.newtype(:mytype) do end".split("\n")) @container.expects(:add_plugin).with do |plugin| plugin.comment == "documentation\n" #and plugin.name == "mytype" end @parser.parse_puppet_plugin(@container) end end describe "when parsing facts" do before :each do @container = stub_everything 'container' File.stubs(:open).yields(["# documentation", "Facter.add('myfact') do", "confine :kernel => :linux", "end"]) end it "should add facts to the container" do @container.expects(:add_fact).with do |fact| fact.comment == "documentation\n" and fact.name == "myfact" end @parser.parse_fact(@container) end it "should add confine to the parsed facts" do ourfact = nil @container.expects(:add_fact).with do |fact| ourfact = fact true end @parser.parse_fact(@container) ourfact.confine.should == { :type => "kernel", :value => ":linux" } end end end