bcedf72191d16cd3c0063f8b79669b283be4ded1
[packages/precise/mcollective.git] / lib / mcollective / security / base.rb
1 module MCollective
2   module Security
3     # This is a base class the other security modules should inherit from
4     # it handles statistics and validation of messages that should in most
5     # cases apply to all security models.
6     #
7     # To create your own security plugin you should provide a plugin that inherits
8     # from this and provides the following methods:
9     #
10     # decodemsg      - Decodes a message that was received from the middleware
11     # encodereply    - Encodes a reply message to a previous request message
12     # encoderequest  - Encodes a new request message
13     # validrequest?  - Validates a request received from the middleware
14     #
15     # Optionally if you are identifying users by some other means like certificate name
16     # you can provide your own callerid method that can provide the rest of the system
17     # with an id, and you would see this id being usable in SimpleRPC authorization methods
18     #
19     # The @initiated_by variable will be set to either :client or :node depending on
20     # who is using this plugin.  This is to help security providers that operate in an
21     # asymetric mode like public/private key based systems.
22     #
23     # Specifics of each of these are a bit fluid and the interfaces for this is not
24     # set in stone yet, specifically the encode methods will be provided with a helper
25     # that takes care of encoding the core requirements.  The best place to see how security
26     # works is by looking at the provided MCollective::Security::PSK plugin.
27     class Base
28       attr_reader :stats
29       attr_accessor :initiated_by
30
31       # Register plugins that inherits base
32       def self.inherited(klass)
33         PluginManager << {:type => "security_plugin", :class => klass.to_s}
34       end
35
36       # Initializes configuration and logging as well as prepare a zero'd hash of stats
37       # various security methods and filter validators should increment stats, see MCollective::Security::Psk for a sample
38       def initialize
39         @config = Config.instance
40         @log = Log
41         @stats = PluginManager["global_stats"]
42       end
43
44       # Takes a Hash with a filter in it and validates it against host information.
45       #
46       # At present this supports filter matches against the following criteria:
47       #
48       # - puppet_class|cf_class - Presence of a configuration management class in
49       #                           the file configured with classesfile
50       # - agent - Presence of a MCollective agent with a supplied name
51       # - fact - The value of a fact avout this system
52       # - identity - the configured identity of the system
53       #
54       # TODO: Support REGEX and/or multiple filter keys to be AND'd
55       def validate_filter?(filter)
56         failed = 0
57         passed = 0
58
59         passed = 1 if Util.empty_filter?(filter)
60
61         filter.keys.each do |key|
62           case key
63           when /puppet_class|cf_class/
64             filter[key].each do |f|
65               Log.debug("Checking for class #{f}")
66               if Util.has_cf_class?(f) then
67                 Log.debug("Passing based on configuration management class #{f}")
68                 passed += 1
69               else
70                 Log.debug("Failing based on configuration management class #{f}")
71                 failed += 1
72               end
73             end
74
75           when "compound"
76             filter[key].each do |compound|
77               result = false
78               truth_values = []
79
80               begin
81                 compound.each do |expression|
82                   case expression.keys.first
83                     when "statement"
84                       truth_values << Matcher.eval_compound_statement(expression).to_s
85                     when "fstatement"
86                       truth_values << Matcher.eval_compound_fstatement(expression.values.first)
87                     when "and"
88                       truth_values << "&&"
89                     when "or"
90                       truth_values << "||"
91                     when "("
92                       truth_values << "("
93                     when ")"
94                       truth_values << ")"
95                     when "not"
96                       truth_values << "!"
97                   end
98                 end
99
100                 result = eval(truth_values.join(" "))
101               rescue DDLValidationError
102                 result = false
103               end
104
105               if result
106                 Log.debug("Passing based on class and fact composition")
107                 passed +=1
108               else
109                 Log.debug("Failing based on class and fact composition")
110                 failed +=1
111               end
112             end
113
114           when "agent"
115             filter[key].each do |f|
116               if Util.has_agent?(f) || f == "mcollective"
117                 Log.debug("Passing based on agent #{f}")
118                 passed += 1
119               else
120                 Log.debug("Failing based on agent #{f}")
121                 failed += 1
122               end
123             end
124
125           when "fact"
126             filter[key].each do |f|
127               if Util.has_fact?(f[:fact], f[:value], f[:operator])
128                 Log.debug("Passing based on fact #{f[:fact]} #{f[:operator]} #{f[:value]}")
129                 passed += 1
130               else
131                 Log.debug("Failing based on fact #{f[:fact]} #{f[:operator]} #{f[:value]}")
132                 failed += 1
133               end
134             end
135
136           when "identity"
137             unless filter[key].empty?
138               # Identity filters should not be 'and' but 'or' as each node can only have one identity
139               matched = filter[key].select{|f| Util.has_identity?(f)}.size
140
141               if matched == 1
142                 Log.debug("Passing based on identity")
143                 passed += 1
144               else
145                 Log.debug("Failed based on identity")
146                 failed += 1
147               end
148             end
149           end
150         end
151
152         if failed == 0 && passed > 0
153           Log.debug("Message passed the filter checks")
154
155           @stats.passed
156
157           return true
158         else
159           Log.debug("Message failed the filter checks")
160
161           @stats.filtered
162
163           return false
164         end
165       end
166
167       def create_reply(reqid, agent, body)
168         Log.debug("Encoded a message for request #{reqid}")
169
170         {:senderid => @config.identity,
171          :requestid => reqid,
172          :senderagent => agent,
173          :msgtime => Time.now.utc.to_i,
174          :body => body}
175       end
176
177       def create_request(reqid, filter, msg, initiated_by, target_agent, target_collective, ttl=60)
178         Log.debug("Encoding a request for agent '#{target_agent}' in collective #{target_collective} with request id #{reqid}")
179
180         {:body => msg,
181          :senderid => @config.identity,
182          :requestid => reqid,
183          :filter => filter,
184          :collective => target_collective,
185          :agent => target_agent,
186          :callerid => callerid,
187          :ttl => ttl,
188          :msgtime => Time.now.utc.to_i}
189       end
190
191       # Give a MC::Message instance and a message id this will figure out if you the incoming
192       # message id matches the one the Message object is expecting and raise if its not
193       #
194       # Mostly used by security plugins to figure out if they should do the hard work of decrypting
195       # etc messages that would only later on be ignored
196       def should_process_msg?(msg, msgid)
197         if msg.expected_msgid
198           unless msg.expected_msgid == msgid
199             msgtext = "Got a message with id %s but was expecting %s, ignoring message" % [msgid, msg.expected_msgid]
200             Log.debug msgtext
201             raise MsgDoesNotMatchRequestID, msgtext
202           end
203         end
204
205         true
206       end
207
208       # Validates a callerid.  We do not want to allow things like \ and / in
209       # callerids since other plugins make assumptions that these are safe strings.
210       #
211       # callerids are generally in the form uid=123 or cert=foo etc so we do that
212       # here but security plugins could override this for some complex uses
213       def valid_callerid?(id)
214         !!id.match(/^[\w]+=[\w\.\-]+$/)
215       end
216
217       # Returns a unique id for the caller, by default we just use the unix
218       # user id, security plugins can provide their own means of doing ids.
219       def callerid
220         "uid=#{Process.uid}"
221       end
222
223       # Security providers should provide this, see MCollective::Security::Psk
224       def validrequest?(req)
225         Log.error("validrequest? is not implemented in #{self.class}")
226       end
227
228       # Security providers should provide this, see MCollective::Security::Psk
229       def encoderequest(sender, msg, filter={})
230         Log.error("encoderequest is not implemented in #{self.class}")
231       end
232
233       # Security providers should provide this, see MCollective::Security::Psk
234       def encodereply(sender, msg, requestcallerid=nil)
235         Log.error("encodereply is not implemented in #{self.class}")
236       end
237
238       # Security providers should provide this, see MCollective::Security::Psk
239       def decodemsg(msg)
240         Log.error("decodemsg is not implemented in #{self.class}")
241       end
242     end
243   end
244 end