summaryrefslogtreecommitdiffstats
path: root/lib/puppet/provider/macauthorization/macauthorization.rb
blob: 759540cc011cb3bf30eba47154b02837745ed9bc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
require 'facter'
require 'facter/util/plist'
require 'puppet'
require 'tempfile'

Puppet::Type.type(:macauthorization).provide :macauthorization, :parent => Puppet::Provider do

    desc "Manage Mac OS X authorization database rules and rights.

    "

    commands :security => "/usr/bin/security"
    commands :sw_vers => "/usr/bin/sw_vers"
    
    confine :operatingsystem => :darwin
    
    # This should be confined based on macosx_productversion once
    # http://projects.reductivelabs.com/issues/show/1796 
    # is resolved.
    if FileTest.exists?("/usr/bin/sw_vers")
        product_version = sw_vers "-productVersion"
    
        confine :true => if /^10.5/.match(product_version) or /^10.6/.match(product_version)
            true
        end
    end
       
    defaultfor :operatingsystem => :darwin
    
    AuthDB = "/etc/authorization"
    
    @rights = {}
    @rules = {}
    @parsed_auth_db = {}
    @comment = ""  # Not implemented yet. Is there any real need to?
    
    # This map exists due to the use of hyphens and reserved words in
    # the authorization schema.
    PuppetToNativeAttributeMap = {  :allow_root => "allow-root",
                                    :authenticate_user => "authenticate-user",                                    
                                    :auth_class => "class",
                                    :k_of_n => "k-of-n",
                                    :session_owner => "session-owner", }
    
    class << self
        attr_accessor :parsed_auth_db
        attr_accessor :rights
        attr_accessor :rules
        attr_accessor :comments  # Not implemented yet.
    
        def prefetch(resources)
            self.populate_rules_rights
        end
    
        def instances
            if self.parsed_auth_db == {}
                self.prefetch(nil)
            end
            self.parsed_auth_db.collect do |k,v|
                new(:name => k)
            end
        end
    
        def populate_rules_rights
            auth_plist = Plist::parse_xml(AuthDB)
            if not auth_plist
                raise Puppet::Error.new("Cannot parse: #{AuthDB}")
            end
            self.rights = auth_plist["rights"].dup
            self.rules = auth_plist["rules"].dup
            self.parsed_auth_db = self.rights.dup
            self.parsed_auth_db.merge!(self.rules.dup)
        end
    
    end
    
    # standard required provider instance methods
    
    def initialize(resource)
        if self.class.parsed_auth_db == {}
            self.class.prefetch(resource)
        end
        super
    end
    
    
    def create
        # we just fill the @property_hash in here and let the flush method 
        # deal with it rather than repeating code.
        new_values = {}
        validprops = Puppet::Type.type(resource.class.name).validproperties
        validprops.each do |prop|
            next if prop == :ensure
            if value = resource.should(prop) and value != ""
                new_values[prop] = value
            end
        end
        @property_hash = new_values.dup
    end

    def destroy
        # We explicitly delete here rather than in the flush method.
        case resource[:auth_type]
        when :right
            destroy_right
        when :rule
            destroy_rule
        else
            raise Puppet::Error.new("Must specify auth_type when destroying.")
        end
    end
    
    def exists?
        if self.class.parsed_auth_db.has_key?(resource[:name])
            return true
        else
            return false
        end
    end
    
    
    def flush
        # deletion happens in the destroy methods
        if resource[:ensure] != :absent
            case resource[:auth_type]
            when :right
                flush_right
            when :rule
                flush_rule
            else
                raise Puppet::Error.new("flush requested for unknown type.")
            end
            @property_hash.clear
        end
    end
    
    
    # utility methods below
    
    def destroy_right
        security "authorizationdb", :remove, resource[:name]
    end
    
    def destroy_rule
        authdb = Plist::parse_xml(AuthDB)
        authdb_rules = authdb["rules"].dup
        if authdb_rules[resource[:name]]
            begin
                authdb["rules"].delete(resource[:name])
                Plist::Emit.save_plist(authdb, AuthDB)
            rescue Errno::EACCES => e
                raise Puppet::Error.new("Error saving #{AuthDB}: #{e}")
            end
        end
    end
    
    def flush_right
        # first we re-read the right just to make sure we're in sync for 
        # values that weren't specified in the manifest. As we're supplying 
        # the whole plist when specifying the right it seems safest to be 
        # paranoid given the low cost of quering the db once more.
        cmds = [] 
        cmds << :security << "authorizationdb" << "read" << resource[:name]
        output = execute(cmds, :combine => false)
        current_values = Plist::parse_xml(output)
        if current_values.nil?
            current_values = {}
        end
        specified_values = convert_plist_to_native_attributes(@property_hash)

        # take the current values, merge the specified values to obtain a 
        # complete description of the new values.
        new_values = current_values.merge(specified_values)
        set_right(resource[:name], new_values)
    end
    
    def flush_rule
        authdb = Plist::parse_xml(AuthDB)
        authdb_rules = authdb["rules"].dup
        current_values = {}
        if authdb_rules[resource[:name]]
            current_values = authdb_rules[resource[:name]]
        end
        specified_values = convert_plist_to_native_attributes(@property_hash)
        new_values = current_values.merge(specified_values)
        set_rule(resource[:name], new_values)
    end
    
    def set_right(name, values)
        # Both creates and modifies rights as it simply overwrites them.
        # The security binary only allows for writes using stdin, so we
        # dump the values to a tempfile.
        values = convert_plist_to_native_attributes(values)
        tmp = Tempfile.new('puppet_macauthorization')
        begin
            Plist::Emit.save_plist(values, tmp.path)
            cmds = [] 
            cmds << :security << "authorizationdb" << "write" << name
            output = execute(cmds, :combine => false,
                             :stdinfile => tmp.path.to_s)
        rescue Errno::EACCES => e
            raise Puppet::Error.new("Cannot save right to #{tmp.path}: #{e}")
        ensure
            tmp.close
            tmp.unlink
        end
    end
    
    def set_rule(name, values)
        # Both creates and modifies rules as it overwrites the entry in the 
        # rules dictionary.  Unfortunately the security binary doesn't 
        # support modifying rules at all so we have to twiddle the whole 
        # plist... :( See Apple Bug #6386000
        values = convert_plist_to_native_attributes(values)
        authdb = Plist::parse_xml(AuthDB)
        authdb["rules"][name] = values

        begin
            Plist::Emit.save_plist(authdb, AuthDB)
        rescue
            raise Puppet::Error.new("Error writing to: #{AuthDB}")
        end
    end
    
    def convert_plist_to_native_attributes(propertylist)
        # This mainly converts the keys from the puppet attributes to the
        # 'native' ones, but also enforces that the keys are all Strings 
        # rather than Symbols so that any merges of the resultant Hash are 
        # sane.
        newplist = {}
        propertylist.each_pair do |key, value|
            next if key == :ensure     # not part of the auth db schema.
            next if key == :auth_type  # not part of the auth db schema.
            new_key = key
            if PuppetToNativeAttributeMap.has_key?(key)
                new_key = PuppetToNativeAttributeMap[key].to_s
            elsif not key.is_a?(String)
                new_key = key.to_s
            end
            newplist[new_key] = value
        end
        newplist
    end
    
    def retrieve_value(resource_name, attribute)
        
        if not self.class.parsed_auth_db.has_key?(resource_name)
            raise Puppet::Error.new("Cannot find #{resource_name} in auth db")
        end
       
        if PuppetToNativeAttributeMap.has_key?(attribute)
            native_attribute = PuppetToNativeAttributeMap[attribute]
        else
            native_attribute = attribute.to_s
        end

        if self.class.parsed_auth_db[resource_name].has_key?(native_attribute)
            value = self.class.parsed_auth_db[resource_name][native_attribute]
            case value
            when true, "true", :true
                value = :true
            when false, "false", :false
                value = :false
            end

            @property_hash[attribute] = value
            return value
        else
            @property_hash.delete(attribute)
            return ""  # so ralsh doesn't display it.
        end
    end
    
    
    # property methods below
    #
    # We define them all dynamically apart from auth_type which is a special
    # case due to not being in the actual authorization db schema.
    
    properties = [  :allow_root, :authenticate_user, :auth_class, :comment,
                    :group, :k_of_n, :mechanisms, :rule, :session_owner,
                    :shared, :timeout, :tries ]

    properties.each do |field|
        define_method(field.to_s) do
            retrieve_value(resource[:name], field)
        end

        define_method(field.to_s + "=") do |value|
            @property_hash[field] = value
        end
    end
    
    def auth_type
        if resource.should(:auth_type) != nil
            return resource.should(:auth_type)
        elsif self.exists?
            # this is here just for ralsh, so it can work out what type it is.
            if self.class.rights.has_key?(resource[:name])
                return :right
            elsif self.class.rules.has_key?(resource[:name])
                return :rule
            else
                raise Puppet::Error.new("#{resource[:name]} is unknown type.")
            end
        else
            raise Puppet::Error.new("auth_type required for new resources.")
        end
    end
    
    def auth_type=(value)
        @property_hash[:auth_type] = value
    end
    
end