Add the posix_acl module
[mirror/dsa-puppet.git] / 3rdparty / modules / posix_acl / lib / puppet / type / posix_acl.rb
1 require 'set'
2 require 'pathname'
3
4 Puppet::Type.newtype(:posix_acl) do
5   desc <<-EOT
6      Ensures that a set of ACL permissions are applied to a given file
7      or directory.
8
9       Example:
10
11           posix_acl { '/var/www/html':
12             action      => exact,
13             permission  => [
14               'user::rwx',
15               'group::r-x',
16               'mask::rwx',
17               'other::r--',
18               'default:user::rwx',
19               'default:user:www-data:r-x',
20               'default:group::r-x',
21               'default:mask::rwx',
22               'default:other::r--',
23             ],
24             provider    => posixacl,
25             recursive   => true,
26           }
27
28       In this example, Puppet will ensure that the user and group
29       permissions are set recursively on /var/www/html as well as add
30       default permissions that will apply to new directories and files
31       created under /var/www/html
32
33       Setting an ACL can change a file's mode bits, so if the file is
34       managed by a File resource, that resource needs to set the mode
35       bits according to what the calculated mode bits will be, for
36       example, the File resource for the ACL above should be:
37
38           file { '/var/www/html':
39                  mode => 754,
40                }
41     EOT
42
43   newparam(:action) do
44     desc 'What do we do with this list of ACLs? Options are set, unset, exact, and purge'
45     newvalues(:set, :unset, :exact, :purge)
46     defaultto :set
47   end
48
49   newparam(:path) do
50     desc 'The file or directory to which the ACL applies.'
51     isnamevar
52     validate do |value|
53       path = Pathname.new(value)
54       unless path.absolute?
55         raise ArgumentError, "Path must be absolute: #{path}"
56       end
57     end
58   end
59
60   newparam(:recursemode) do
61     desc "Should Puppet apply the ACL recursively with the -R option or
62       apply it to individual files?
63
64       lazy means -R option
65       deep means apply to every file"
66
67     newvalues(:lazy, :deep)
68     defaultto :lazy
69   end
70
71   # Credits to @itdoesntwork
72   # http://stackoverflow.com/questions/26878341/how-do-i-tell-if-one-path-is-an-ancestor-of-another
73   def self.descendant?(a, b)
74     a_list = File.expand_path(a).split('/')
75     b_list = File.expand_path(b).split('/')
76
77     b_list[0..a_list.size - 1] == a_list && b_list != a_list
78   end
79
80   # Snippet based on upstream Puppet (ASL 2.0)
81   [:posix_acl, :file].each do |autorequire_type|
82     autorequire(autorequire_type) do
83       req = []
84       path = Pathname.new(self[:path])
85       # rubocop:disable Style/MultilineBlockChain
86       if autorequire_type != :posix_acl
87         if self[:recursive] == :true
88           catalog.resources.select do |r|
89             r.is_a?(Puppet::Type.type(autorequire_type)) && self.class.descendant?(self[:path], r[:path])
90           end.each do |found|
91             req << found[:path]
92           end
93         end
94         req << self[:path]
95       end
96       unless path.root?
97         # Start at our parent, to avoid autorequiring ourself
98         parents = path.parent.enum_for(:ascend)
99         # should this be = or == ? I don't know
100         if found = parents.find { |p| catalog.resource(autorequire_type, p.to_s) } # rubocop:disable Lint/AssignmentInCondition
101           req << found.to_s
102         end
103       end
104       req
105     end
106     # rubocop:enable Style/MultilineBlockChain
107   end
108   # End of Snippet
109
110   autorequire(:package) do
111     ['acl']
112   end
113
114   newproperty(:permission, array_matching: :all) do
115     desc 'ACL permission(s).'
116
117     def is_to_s(value) # rubocop:disable Style/PredicateName
118       if value == :absent || value.include?(:absent)
119         super
120       else
121         value.sort.inspect
122       end
123     end
124
125     def should_to_s(value)
126       if value == :absent || value.include?(:absent)
127         super
128       else
129         value.sort.inspect
130       end
131     end
132
133     def retrieve
134       provider.permission
135     end
136
137     # Remove permission bits from an ACL line, eg:
138     # 'user:root:rwx' becomes 'user:root:'
139     def strip_perms(pl)
140       Puppet.debug 'permission.strip_perms'
141       value = []
142       pl.each do |perm|
143         unless perm =~ %r{^(((u(ser)?)|(g(roup)?)|(m(ask)?)|(o(ther)?)):):}
144           perm = perm.split(':', -1)[0..-2].join(':')
145           value << perm
146         end
147       end
148       value.sort
149     end
150
151     # in unset_insync and set_insync the test_should has been added as a work around
152     #  to prevent puppet-posix_acl from interpreting recursive permission notation (e.g. rwX)
153     #  from causing a false mismatch.  A better solution needs to be implemented to
154     #  recursively check permissions, not rely upon getfacl
155     def unset_insync(cur_perm)
156       # Puppet.debug "permission.unset_insync"
157       test_should = []
158       @should.each { |x| test_should << x.downcase }
159       cp = strip_perms(cur_perm)
160       sp = strip_perms(test_should)
161       (sp - cp).sort == sp
162     end
163
164     def set_insync(cur_perm) # rubocop:disable Style/AccessorMethodName
165       should = @should.uniq.sort
166       (cur_perm.sort == should) || (provider.check_set && (should - cur_perm).empty?)
167     end
168
169     def purge_insync(cur_perm)
170       # Puppet.debug "permission.purge_insync"
171       cur_perm.each do |perm|
172         # If anything other than the mode bits are set, we're not in sync
173         return false unless perm =~ %r{^(((u(ser)?)|(g(roup)?)|(o(ther)?)):):}
174       end
175       true
176     end
177
178     def insync?(is)
179       Puppet.debug "permission.insync? is: #{is.inspect} @should: #{@should.inspect}"
180       return purge_insync(is) if provider.check_purge
181       return unset_insync(is) if provider.check_unset
182       set_insync(is)
183     end
184
185     # Munge into normalised form
186     munge do |acl|
187       r = ''
188       a = acl.split ':', -1 # -1 keeps trailing empty fields.
189       raise ArgumentError, "Too few fields.  At least 3 required, got #{a.length}." if a.length < 3
190       raise ArgumentError, "Too many fields.  At most 4 allowed, got #{a.length}."  if a.length > 4
191       if a.length == 4
192         d = a.shift
193         raise ArgumentError, %(First field of 4 must be "d" or "default", got "#{d}".) unless %w[d default].include?(d)
194         r << 'default:'
195       end
196       t = a.shift # Copy the type.
197       r << case t
198            when 'u', 'user'
199              'user:'
200            when 'g', 'group'
201              'group:'
202            when 'o', 'other'
203              'other:'
204            when 'm', 'mask'
205              'mask:'
206            else
207              raise ArgumentError, %(Unknown type "#{t}", expected "user", "group", "other" or "mask".)
208            end
209       r << "#{a.shift}:" # Copy the "who".
210       p = a.shift
211       if p =~ %r{[0-7]}
212         p = p.oct
213         r << (p | 4 ? 'r' : '-')
214         r << (p | 2 ? 'w' : '-')
215         r << (p | 1 ? 'x' : '-')
216       else
217         # Not the most efficient but checks for multiple and invalid chars.
218         s = p.tr '-', ''
219         r << (s.sub!('r', '') ? 'r' : '-')
220         r << (s.sub!('w', '') ? 'w' : '-')
221         r << (s.sub!('x', '') ? 'x' : '-')
222         raise ArgumentError, %(Invalid permission set "#{p}".) unless s.empty?
223       end
224       r
225     end
226   end
227
228   newparam(:recursive) do
229     desc 'Apply ACLs recursively.'
230     newvalues(:true, :false)
231     defaultto :false
232   end
233
234   def self.pick_default_perms(acl)
235     acl.reject { |a| a.split(':', -1).length == 4 }
236   end
237
238   def newchild(path)
239     options = @original_parameters.merge(name: path).reject { |_param, value| value.nil? }
240     unless File.directory?(options[:name])
241       options[:permission] = self.class.pick_default_perms(options[:permission]) if options.include?(:permission)
242     end
243     [:recursive, :recursemode, :path].each do |param|
244       options.delete(param) if options.include?(param)
245     end
246     self.class.new(options)
247   end
248
249   def generate
250     return [] unless self[:recursive] == :true && self[:recursemode] == :deep
251     results = []
252     paths = Set.new
253     if File.directory?(self[:path])
254       Dir.chdir(self[:path]) do
255         Dir['**/*'].each do |path|
256           paths << ::File.join(self[:path], path)
257         end
258       end
259     end
260     # At the time we generate extra resources, all the files might now be present yet.
261     # In prediction to that we also create ACL resources for child file resources that
262     # might not have been applied yet.
263     catalog.resources.select do |r|
264       r.is_a?(Puppet::Type.type(:file)) && self.class.descendant?(self[:path], r[:path])
265     end.each do |found| # rubocop:disable Style/MultilineBlockChain
266       paths << found[:path]
267     end
268     paths.each do |path|
269       results << newchild(path)
270     end
271     results
272   end
273
274   validate do
275     unless self[:permission]
276       raise(Puppet::Error, 'permission is a required property.')
277     end
278   end
279 end