--- /dev/null
+module MCollective
+ module Security
+ # This is a base class the other security modules should inherit from
+ # it handles statistics and validation of messages that should in most
+ # cases apply to all security models.
+ #
+ # To create your own security plugin you should provide a plugin that inherits
+ # from this and provides the following methods:
+ #
+ # decodemsg - Decodes a message that was received from the middleware
+ # encodereply - Encodes a reply message to a previous request message
+ # encoderequest - Encodes a new request message
+ # validrequest? - Validates a request received from the middleware
+ #
+ # Optionally if you are identifying users by some other means like certificate name
+ # you can provide your own callerid method that can provide the rest of the system
+ # with an id, and you would see this id being usable in SimpleRPC authorization methods
+ #
+ # The @initiated_by variable will be set to either :client or :node depending on
+ # who is using this plugin. This is to help security providers that operate in an
+ # asymetric mode like public/private key based systems.
+ #
+ # Specifics of each of these are a bit fluid and the interfaces for this is not
+ # set in stone yet, specifically the encode methods will be provided with a helper
+ # that takes care of encoding the core requirements. The best place to see how security
+ # works is by looking at the provided MCollective::Security::PSK plugin.
+ class Base
+ attr_reader :stats
+ attr_accessor :initiated_by
+
+ # Register plugins that inherits base
+ def self.inherited(klass)
+ PluginManager << {:type => "security_plugin", :class => klass.to_s}
+ end
+
+ # Initializes configuration and logging as well as prepare a zero'd hash of stats
+ # various security methods and filter validators should increment stats, see MCollective::Security::Psk for a sample
+ def initialize
+ @config = Config.instance
+ @log = Log
+ @stats = PluginManager["global_stats"]
+ end
+
+ # Takes a Hash with a filter in it and validates it against host information.
+ #
+ # At present this supports filter matches against the following criteria:
+ #
+ # - puppet_class|cf_class - Presence of a configuration management class in
+ # the file configured with classesfile
+ # - agent - Presence of a MCollective agent with a supplied name
+ # - fact - The value of a fact avout this system
+ # - identity - the configured identity of the system
+ #
+ # TODO: Support REGEX and/or multiple filter keys to be AND'd
+ def validate_filter?(filter)
+ failed = 0
+ passed = 0
+
+ passed = 1 if Util.empty_filter?(filter)
+
+ filter.keys.each do |key|
+ case key
+ when /puppet_class|cf_class/
+ filter[key].each do |f|
+ Log.debug("Checking for class #{f}")
+ if Util.has_cf_class?(f) then
+ Log.debug("Passing based on configuration management class #{f}")
+ passed += 1
+ else
+ Log.debug("Failing based on configuration management class #{f}")
+ failed += 1
+ end
+ end
+
+ when "compound"
+ filter[key].each do |compound|
+ result = false
+ truth_values = []
+
+ begin
+ compound.each do |expression|
+ case expression.keys.first
+ when "statement"
+ truth_values << Matcher.eval_compound_statement(expression).to_s
+ when "fstatement"
+ truth_values << Matcher.eval_compound_fstatement(expression.values.first)
+ when "and"
+ truth_values << "&&"
+ when "or"
+ truth_values << "||"
+ when "("
+ truth_values << "("
+ when ")"
+ truth_values << ")"
+ when "not"
+ truth_values << "!"
+ end
+ end
+
+ result = eval(truth_values.join(" "))
+ rescue DDLValidationError
+ result = false
+ end
+
+ if result
+ Log.debug("Passing based on class and fact composition")
+ passed +=1
+ else
+ Log.debug("Failing based on class and fact composition")
+ failed +=1
+ end
+ end
+
+ when "agent"
+ filter[key].each do |f|
+ if Util.has_agent?(f) || f == "mcollective"
+ Log.debug("Passing based on agent #{f}")
+ passed += 1
+ else
+ Log.debug("Failing based on agent #{f}")
+ failed += 1
+ end
+ end
+
+ when "fact"
+ filter[key].each do |f|
+ if Util.has_fact?(f[:fact], f[:value], f[:operator])
+ Log.debug("Passing based on fact #{f[:fact]} #{f[:operator]} #{f[:value]}")
+ passed += 1
+ else
+ Log.debug("Failing based on fact #{f[:fact]} #{f[:operator]} #{f[:value]}")
+ failed += 1
+ end
+ end
+
+ when "identity"
+ unless filter[key].empty?
+ # Identity filters should not be 'and' but 'or' as each node can only have one identity
+ matched = filter[key].select{|f| Util.has_identity?(f)}.size
+
+ if matched == 1
+ Log.debug("Passing based on identity")
+ passed += 1
+ else
+ Log.debug("Failed based on identity")
+ failed += 1
+ end
+ end
+ end
+ end
+
+ if failed == 0 && passed > 0
+ Log.debug("Message passed the filter checks")
+
+ @stats.passed
+
+ return true
+ else
+ Log.debug("Message failed the filter checks")
+
+ @stats.filtered
+
+ return false
+ end
+ end
+
+ def create_reply(reqid, agent, body)
+ Log.debug("Encoded a message for request #{reqid}")
+
+ {:senderid => @config.identity,
+ :requestid => reqid,
+ :senderagent => agent,
+ :msgtime => Time.now.utc.to_i,
+ :body => body}
+ end
+
+ def create_request(reqid, filter, msg, initiated_by, target_agent, target_collective, ttl=60)
+ Log.debug("Encoding a request for agent '#{target_agent}' in collective #{target_collective} with request id #{reqid}")
+
+ {:body => msg,
+ :senderid => @config.identity,
+ :requestid => reqid,
+ :filter => filter,
+ :collective => target_collective,
+ :agent => target_agent,
+ :callerid => callerid,
+ :ttl => ttl,
+ :msgtime => Time.now.utc.to_i}
+ end
+
+ # Give a MC::Message instance and a message id this will figure out if you the incoming
+ # message id matches the one the Message object is expecting and raise if its not
+ #
+ # Mostly used by security plugins to figure out if they should do the hard work of decrypting
+ # etc messages that would only later on be ignored
+ def should_process_msg?(msg, msgid)
+ if msg.expected_msgid
+ unless msg.expected_msgid == msgid
+ msgtext = "Got a message with id %s but was expecting %s, ignoring message" % [msgid, msg.expected_msgid]
+ Log.debug msgtext
+ raise MsgDoesNotMatchRequestID, msgtext
+ end
+ end
+
+ true
+ end
+
+ # Validates a callerid. We do not want to allow things like \ and / in
+ # callerids since other plugins make assumptions that these are safe strings.
+ #
+ # callerids are generally in the form uid=123 or cert=foo etc so we do that
+ # here but security plugins could override this for some complex uses
+ def valid_callerid?(id)
+ !!id.match(/^[\w]+=[\w\.\-]+$/)
+ end
+
+ # Returns a unique id for the caller, by default we just use the unix
+ # user id, security plugins can provide their own means of doing ids.
+ def callerid
+ "uid=#{Process.uid}"
+ end
+
+ # Security providers should provide this, see MCollective::Security::Psk
+ def validrequest?(req)
+ Log.error("validrequest? is not implemented in #{self.class}")
+ end
+
+ # Security providers should provide this, see MCollective::Security::Psk
+ def encoderequest(sender, msg, filter={})
+ Log.error("encoderequest is not implemented in #{self.class}")
+ end
+
+ # Security providers should provide this, see MCollective::Security::Psk
+ def encodereply(sender, msg, requestcallerid=nil)
+ Log.error("encodereply is not implemented in #{self.class}")
+ end
+
+ # Security providers should provide this, see MCollective::Security::Psk
+ def decodemsg(msg)
+ Log.error("decodemsg is not implemented in #{self.class}")
+ end
+ end
+ end
+end