diff --git a/lib/puppet/type/file/mode.rb b/lib/puppet/type/file/mode.rb index 682e744cd..d7b521fa9 100644 --- a/lib/puppet/type/file/mode.rb +++ b/lib/puppet/type/file/mode.rb @@ -1,159 +1,159 @@ # Manage file modes. This state should support different formats # for specification (e.g., u+rwx, or -0011), but for now only supports # specifying the full mode. module Puppet Puppet::Type.type(:file).newproperty(:mode) do require 'puppet/util/symbolic_file_mode' include Puppet::Util::SymbolicFileMode desc <<-'EOT' The desired permissions mode for the file, in symbolic or numeric notation. Puppet uses traditional Unix permission schemes and translates them to equivalent permissions for systems which represent permissions differently, including Windows. Numeric modes should use the standard four-digit octal notation of `` (e.g. 0644). Each of the "owner," "group," and "other" digits should be a sum of the permissions for that class of users, where read = 4, write = 2, and execute/search = 1. When setting numeric permissions for directories, Puppet sets the search permission wherever the read permission is set. Symbolic modes should be represented as a string of comma-separated permission clauses, in the form ``: * "Who" should be u (user), g (group), o (other), and/or a (all) * "Op" should be = (set exact permissions), + (add select permissions), or - (remove select permissions) * "Perm" should be one or more of: * r (read) * w (write) * x (execute/search) * t (sticky) * s (setuid/setgid) * X (execute/search if directory or if any one user can execute) * u (user's current permissions) * g (group's current permissions) * o (other's current permissions) Thus, mode `0664` could be represented symbolically as either `a=r,ug+w` or `ug=rw,o=r`. However, symbolic modes are more expressive than numeric modes: a mode only affects the specified bits, so `mode => 'ug+w'` will set the user and group write bits, without affecting any other bits. See the manual page for GNU or BSD `chmod` for more details on numeric and symbolic modes. On Windows, permissions are translated as follows: * Owner and group names are mapped to Windows SIDs * The "other" class of users maps to the "Everyone" SID * The read/write/execute permissions map to the `FILE_GENERIC_READ`, `FILE_GENERIC_WRITE`, and `FILE_GENERIC_EXECUTE` access rights; a file's owner always has the `FULL_CONTROL` right * "Other" users can't have any permissions a file's group lacks, and its group can't have any permissions its owner lacks; that is, 0644 is an acceptable mode, but 0464 is not. EOT validate do |value| unless value.nil? or valid_symbolic_mode?(value) raise Puppet::Error, "The file mode specification is invalid: #{value.inspect}" end end munge do |value| return nil if value.nil? unless valid_symbolic_mode?(value) raise Puppet::Error, "The file mode specification is invalid: #{value.inspect}" end normalize_symbolic_mode(value) end def desired_mode_from_current(desired, current) current = current.to_i(8) if current.is_a? String - is_a_directory = @resource.stat and @resource.stat.directory? + is_a_directory = @resource.stat && @resource.stat.directory? symbolic_mode_to_int(desired, current, is_a_directory) end # If we're a directory, we need to be executable for all cases # that are readable. This should probably be selectable, but eh. def dirmask(value) if FileTest.directory?(resource[:path]) and value =~ /^\d+$/ then value = value.to_i(8) value |= 0100 if value & 0400 != 0 value |= 010 if value & 040 != 0 value |= 01 if value & 04 != 0 value = value.to_s(8) end value end # If we're not following links and we're a link, then we just turn # off mode management entirely. def insync?(currentvalue) if stat = @resource.stat and stat.ftype == "link" and @resource[:links] != :follow self.debug "Not managing symlink mode" return true else return super(currentvalue) end end def property_matches?(current, desired) return false unless current current_bits = normalize_symbolic_mode(current) desired_bits = desired_mode_from_current(desired, current).to_s(8) current_bits == desired_bits end # Ideally, dirmask'ing could be done at munge time, but we don't know if 'ensure' # will eventually be a directory or something else. And unfortunately, that logic # depends on the ensure, source, and target properties. So rather than duplicate # that logic, and get it wrong, we do dirmask during retrieve, after 'ensure' has # been synced. def retrieve if @resource.stat @should &&= @should.collect { |s| self.dirmask(s) } end super end # Finally, when we sync the mode out we need to transform it; since we # don't have access to the calculated "desired" value here, or the # "current" value, only the "should" value we need to retrieve again. def sync current = @resource.stat ? @resource.stat.mode : 0644 set(desired_mode_from_current(@should[0], current).to_s(8)) end def change_to_s(old_value, desired) return super if desired =~ /^\d+$/ old_bits = normalize_symbolic_mode(old_value) new_bits = normalize_symbolic_mode(desired_mode_from_current(desired, old_bits)) super(old_bits, new_bits) + " (#{desired})" end def should_to_s(should_value) should_value.rjust(4, "0") end def is_to_s(currentvalue) if currentvalue == :absent # This can occur during audits---if a file is transitioning from # present to absent the mode will have a value of `:absent`. super else currentvalue.rjust(4, "0") end end end end diff --git a/spec/unit/type/file/mode_spec.rb b/spec/unit/type/file/mode_spec.rb index 9936ebdbc..cb971b3a9 100755 --- a/spec/unit/type/file/mode_spec.rb +++ b/spec/unit/type/file/mode_spec.rb @@ -1,195 +1,220 @@ #! /usr/bin/env ruby require 'spec_helper' describe Puppet::Type.type(:file).attrclass(:mode) do include PuppetSpec::Files let(:path) { tmpfile('mode_spec') } let(:resource) { Puppet::Type.type(:file).new :path => path, :mode => 0644 } let(:mode) { resource.property(:mode) } describe "#validate" do it "should accept values specified as integers" do expect { mode.value = 0755 }.not_to raise_error end it "should accept values specified as octal numbers in strings" do expect { mode.value = '0755' }.not_to raise_error end it "should accept valid symbolic strings" do expect { mode.value = 'g+w,u-x' }.not_to raise_error end it "should not accept strings other than octal numbers" do expect do mode.value = 'readable please!' end.to raise_error(Puppet::Error, /The file mode specification is invalid/) end end describe "#munge" do # This is sort of a redundant test, but its spec is important. it "should return the value as a string" do mode.munge('0644').should be_a(String) end it "should accept strings as arguments" do mode.munge('0644').should == '644' end it "should accept symbolic strings as arguments and return them intact" do mode.munge('u=rw,go=r').should == 'u=rw,go=r' end it "should accept integers are arguments" do mode.munge(0644).should == '644' end end describe "#dirmask" do before :each do Dir.mkdir(path) end it "should add execute bits corresponding to read bits for directories" do mode.dirmask('0644').should == '755' end it "should not add an execute bit when there is no read bit" do mode.dirmask('0600').should == '700' end it "should not add execute bits for files that aren't directories" do resource[:path] = tmpfile('other_file') mode.dirmask('0644').should == '0644' end end describe "#insync?" do it "should return true if the mode is correct" do FileUtils.touch(path) mode.must be_insync('644') end it "should return false if the mode is incorrect" do FileUtils.touch(path) mode.must_not be_insync('755') end it "should return true if the file is a link and we are managing links", :if => Puppet.features.manages_symlinks? do Puppet::FileSystem.symlink('anything', path) mode.must be_insync('644') end describe "with a symbolic mode" do let(:resource_sym) { Puppet::Type.type(:file).new :path => path, :mode => 'u+w,g-w' } let(:mode_sym) { resource_sym.property(:mode) } it "should return true if the mode matches, regardless of other bits" do FileUtils.touch(path) mode_sym.must be_insync('644') end it "should return false if the mode requires 0's where there are 1's" do FileUtils.touch(path) mode_sym.must_not be_insync('624') end it "should return false if the mode requires 1's where there are 0's" do FileUtils.touch(path) mode_sym.must_not be_insync('044') end end end describe "#retrieve" do it "should return absent if the resource doesn't exist" do resource[:path] = File.expand_path("/does/not/exist") mode.retrieve.should == :absent end it "should retrieve the directory mode from the provider" do Dir.mkdir(path) mode.expects(:dirmask).with('644').returns '755' resource.provider.expects(:mode).returns '755' mode.retrieve.should == '755' end it "should retrieve the file mode from the provider" do FileUtils.touch(path) mode.expects(:dirmask).with('644').returns '644' resource.provider.expects(:mode).returns '644' mode.retrieve.should == '644' end end describe '#should_to_s' do describe 'with a 3-digit mode' do it 'returns a 4-digit mode with a leading zero' do mode.should_to_s('755').should == '0755' end end describe 'with a 4-digit mode' do it 'returns the 4-digit mode when the first digit is a zero' do mode.should_to_s('0755').should == '0755' end it 'returns the 4-digit mode when the first digit is not a zero' do mode.should_to_s('1755').should == '1755' end end end describe '#is_to_s' do describe 'with a 3-digit mode' do it 'returns a 4-digit mode with a leading zero' do mode.is_to_s('755').should == '0755' end end describe 'with a 4-digit mode' do it 'returns the 4-digit mode when the first digit is a zero' do mode.is_to_s('0755').should == '0755' end it 'returns the 4-digit mode when the first digit is not a zero' do mode.is_to_s('1755').should == '1755' end end describe 'when passed :absent' do it 'returns :absent' do mode.is_to_s(:absent).should == :absent end end end describe "#sync with a symbolic mode" do let(:resource_sym) { Puppet::Type.type(:file).new :path => path, :mode => 'u+w,g-w' } let(:mode_sym) { resource_sym.property(:mode) } before { FileUtils.touch(path) } it "changes only the requested bits" do # lower nibble must be set to 4 for the sake of passing on Windows Puppet::FileSystem.chmod(0464, path) mode_sym.sync stat = Puppet::FileSystem.stat(path) (stat.mode & 0777).to_s(8).should == "644" end end + + describe '#sync with a symbolic mode of +X for a file' do + let(:resource_sym) { Puppet::Type.type(:file).new :path => path, :mode => 'g+wX' } + let(:mode_sym) { resource_sym.property(:mode) } + + before { FileUtils.touch(path) } + + it 'does not change executable bit if no executable bit is set' do + Puppet::FileSystem.chmod(0644, path) + + mode_sym.sync + + stat = Puppet::FileSystem.stat(path) + (stat.mode & 0777).to_s(8).should == '664' + end + + it 'does change executable bit if an executable bit is set' do + Puppet::FileSystem.chmod(0744, path) + + mode_sym.sync + + stat = Puppet::FileSystem.stat(path) + (stat.mode & 0777).to_s(8).should == '774' + end + end end