--- /dev/null
+require 'set'
+require 'pathname'
+
+Puppet::Type.newtype(:posix_acl) do
+ desc <<-EOT
+ Ensures that a set of ACL permissions are applied to a given file
+ or directory.
+
+ Example:
+
+ posix_acl { '/var/www/html':
+ action => exact,
+ permission => [
+ 'user::rwx',
+ 'group::r-x',
+ 'mask::rwx',
+ 'other::r--',
+ 'default:user::rwx',
+ 'default:user:www-data:r-x',
+ 'default:group::r-x',
+ 'default:mask::rwx',
+ 'default:other::r--',
+ ],
+ provider => posixacl,
+ recursive => true,
+ }
+
+ In this example, Puppet will ensure that the user and group
+ permissions are set recursively on /var/www/html as well as add
+ default permissions that will apply to new directories and files
+ created under /var/www/html
+
+ Setting an ACL can change a file's mode bits, so if the file is
+ managed by a File resource, that resource needs to set the mode
+ bits according to what the calculated mode bits will be, for
+ example, the File resource for the ACL above should be:
+
+ file { '/var/www/html':
+ mode => 754,
+ }
+ EOT
+
+ newparam(:action) do
+ desc 'What do we do with this list of ACLs? Options are set, unset, exact, and purge'
+ newvalues(:set, :unset, :exact, :purge)
+ defaultto :set
+ end
+
+ newparam(:path) do
+ desc 'The file or directory to which the ACL applies.'
+ isnamevar
+ validate do |value|
+ path = Pathname.new(value)
+ unless path.absolute?
+ raise ArgumentError, "Path must be absolute: #{path}"
+ end
+ end
+ end
+
+ newparam(:recursemode) do
+ desc "Should Puppet apply the ACL recursively with the -R option or
+ apply it to individual files?
+
+ lazy means -R option
+ deep means apply to every file"
+
+ newvalues(:lazy, :deep)
+ defaultto :lazy
+ end
+
+ # Credits to @itdoesntwork
+ # http://stackoverflow.com/questions/26878341/how-do-i-tell-if-one-path-is-an-ancestor-of-another
+ def self.descendant?(a, b)
+ a_list = File.expand_path(a).split('/')
+ b_list = File.expand_path(b).split('/')
+
+ b_list[0..a_list.size - 1] == a_list && b_list != a_list
+ end
+
+ # Snippet based on upstream Puppet (ASL 2.0)
+ [:posix_acl, :file].each do |autorequire_type|
+ autorequire(autorequire_type) do
+ req = []
+ path = Pathname.new(self[:path])
+ # rubocop:disable Style/MultilineBlockChain
+ if autorequire_type != :posix_acl
+ if self[:recursive] == :true
+ catalog.resources.select do |r|
+ r.is_a?(Puppet::Type.type(autorequire_type)) && self.class.descendant?(self[:path], r[:path])
+ end.each do |found|
+ req << found[:path]
+ end
+ end
+ req << self[:path]
+ end
+ unless path.root?
+ # Start at our parent, to avoid autorequiring ourself
+ parents = path.parent.enum_for(:ascend)
+ # should this be = or == ? I don't know
+ if found = parents.find { |p| catalog.resource(autorequire_type, p.to_s) } # rubocop:disable Lint/AssignmentInCondition
+ req << found.to_s
+ end
+ end
+ req
+ end
+ # rubocop:enable Style/MultilineBlockChain
+ end
+ # End of Snippet
+
+ autorequire(:package) do
+ ['acl']
+ end
+
+ newproperty(:permission, array_matching: :all) do
+ desc 'ACL permission(s).'
+
+ def is_to_s(value) # rubocop:disable Style/PredicateName
+ if value == :absent || value.include?(:absent)
+ super
+ else
+ value.sort.inspect
+ end
+ end
+
+ def should_to_s(value)
+ if value == :absent || value.include?(:absent)
+ super
+ else
+ value.sort.inspect
+ end
+ end
+
+ def retrieve
+ provider.permission
+ end
+
+ # Remove permission bits from an ACL line, eg:
+ # 'user:root:rwx' becomes 'user:root:'
+ def strip_perms(pl)
+ Puppet.debug 'permission.strip_perms'
+ value = []
+ pl.each do |perm|
+ unless perm =~ %r{^(((u(ser)?)|(g(roup)?)|(m(ask)?)|(o(ther)?)):):}
+ perm = perm.split(':', -1)[0..-2].join(':')
+ value << perm
+ end
+ end
+ value.sort
+ end
+
+ # in unset_insync and set_insync the test_should has been added as a work around
+ # to prevent puppet-posix_acl from interpreting recursive permission notation (e.g. rwX)
+ # from causing a false mismatch. A better solution needs to be implemented to
+ # recursively check permissions, not rely upon getfacl
+ def unset_insync(cur_perm)
+ # Puppet.debug "permission.unset_insync"
+ test_should = []
+ @should.each { |x| test_should << x.downcase }
+ cp = strip_perms(cur_perm)
+ sp = strip_perms(test_should)
+ (sp - cp).sort == sp
+ end
+
+ def set_insync(cur_perm) # rubocop:disable Style/AccessorMethodName
+ should = @should.uniq.sort
+ (cur_perm.sort == should) || (provider.check_set && (should - cur_perm).empty?)
+ end
+
+ def purge_insync(cur_perm)
+ # Puppet.debug "permission.purge_insync"
+ cur_perm.each do |perm|
+ # If anything other than the mode bits are set, we're not in sync
+ return false unless perm =~ %r{^(((u(ser)?)|(g(roup)?)|(o(ther)?)):):}
+ end
+ true
+ end
+
+ def insync?(is)
+ Puppet.debug "permission.insync? is: #{is.inspect} @should: #{@should.inspect}"
+ return purge_insync(is) if provider.check_purge
+ return unset_insync(is) if provider.check_unset
+ set_insync(is)
+ end
+
+ # Munge into normalised form
+ munge do |acl|
+ r = ''
+ a = acl.split ':', -1 # -1 keeps trailing empty fields.
+ raise ArgumentError, "Too few fields. At least 3 required, got #{a.length}." if a.length < 3
+ raise ArgumentError, "Too many fields. At most 4 allowed, got #{a.length}." if a.length > 4
+ if a.length == 4
+ d = a.shift
+ raise ArgumentError, %(First field of 4 must be "d" or "default", got "#{d}".) unless %w[d default].include?(d)
+ r << 'default:'
+ end
+ t = a.shift # Copy the type.
+ r << case t
+ when 'u', 'user'
+ 'user:'
+ when 'g', 'group'
+ 'group:'
+ when 'o', 'other'
+ 'other:'
+ when 'm', 'mask'
+ 'mask:'
+ else
+ raise ArgumentError, %(Unknown type "#{t}", expected "user", "group", "other" or "mask".)
+ end
+ r << "#{a.shift}:" # Copy the "who".
+ p = a.shift
+ if p =~ %r{[0-7]}
+ p = p.oct
+ r << (p | 4 ? 'r' : '-')
+ r << (p | 2 ? 'w' : '-')
+ r << (p | 1 ? 'x' : '-')
+ else
+ # Not the most efficient but checks for multiple and invalid chars.
+ s = p.tr '-', ''
+ r << (s.sub!('r', '') ? 'r' : '-')
+ r << (s.sub!('w', '') ? 'w' : '-')
+ r << (s.sub!('x', '') ? 'x' : '-')
+ raise ArgumentError, %(Invalid permission set "#{p}".) unless s.empty?
+ end
+ r
+ end
+ end
+
+ newparam(:recursive) do
+ desc 'Apply ACLs recursively.'
+ newvalues(:true, :false)
+ defaultto :false
+ end
+
+ def self.pick_default_perms(acl)
+ acl.reject { |a| a.split(':', -1).length == 4 }
+ end
+
+ def newchild(path)
+ options = @original_parameters.merge(name: path).reject { |_param, value| value.nil? }
+ unless File.directory?(options[:name])
+ options[:permission] = self.class.pick_default_perms(options[:permission]) if options.include?(:permission)
+ end
+ [:recursive, :recursemode, :path].each do |param|
+ options.delete(param) if options.include?(param)
+ end
+ self.class.new(options)
+ end
+
+ def generate
+ return [] unless self[:recursive] == :true && self[:recursemode] == :deep
+ results = []
+ paths = Set.new
+ if File.directory?(self[:path])
+ Dir.chdir(self[:path]) do
+ Dir['**/*'].each do |path|
+ paths << ::File.join(self[:path], path)
+ end
+ end
+ end
+ # At the time we generate extra resources, all the files might now be present yet.
+ # In prediction to that we also create ACL resources for child file resources that
+ # might not have been applied yet.
+ catalog.resources.select do |r|
+ r.is_a?(Puppet::Type.type(:file)) && self.class.descendant?(self[:path], r[:path])
+ end.each do |found| # rubocop:disable Style/MultilineBlockChain
+ paths << found[:path]
+ end
+ paths.each do |path|
+ results << newchild(path)
+ end
+ results
+ end
+
+ validate do
+ unless self[:permission]
+ raise(Puppet::Error, 'permission is a required property.')
+ end
+ end
+end