Files
mcollective-actionpolicy-auth/util/actionpolicy.rb
R.I.Pienaar 84ff5d959d 19210 - Publish the actionpolicy simple rpc authorization plugin
Refactored, added tests and updated docs
2013-03-19 16:39:33 +00:00

220 lines
6.5 KiB
Ruby

module MCollective
module Util
class ActionPolicy
attr_accessor :config, :allow_unconfigured, :configdir, :agent, :caller, :action
def self.authorize(request)
ActionPolicy.new(request).authorize_request
end
def initialize(request)
@config = Config.instance
@agent = request.agent
@caller = request.caller
@action = request.action
@allow_unconfigured = !!(config.pluginconf.fetch('actionpolicy.allow_unconfigured', 'n') =~ /^1|y/i)
@configdir = @config.configdir
end
def authorize_request
# Lookup the policy file. If none exists and @allow_unconfigured
# is false the request gets denied.
policy_file = lookup_policy_file
# No policy file exists and allow_unconfigured is false
if !policy_file && !@allow_unconfigured
deny('Could not load any valid policy files. Denying based on allow_unconfigured: %s' % @allow_unconfigured)
# No policy exists but allow_unconfigured is true
elsif !(policy_file) && @allow_unconfigured
Log.debug('Could not load any valid policy files. Allowing based on allow_unconfigured: %s' % @allow_unconfigured)
return true
end
# A policy file exists
parse_policy_file(policy_file)
end
def parse_policy_file(policy_file)
Log.debug('Parsing policyfile for %s: %s' % [@agent, policy_file])
allow = @allow_unconfigured
File.read(policy_file).each_line do |line|
next if line =~ /^(#.*|\s*)$/
if line =~ /^policy\s+default\s+(\w+)/
if $1 == 'allow'
allow = true
else
allow = false
end
elsif line =~ /^(allow|deny)\t+(.+?)\t+(.+?)\t+(.+?)(\t+(.+?))*$/
if check_policy($2, $3, $4, $6)
if $1 == 'allow'
return true
else
deny("Denying based on explicit 'deny' policy rule in policyfile: %s" % File.basename(policy_file))
end
end
else
Log.debug("Cannot parse policy line: %s" % line)
end
end
allow || deny("Denying based on default policy in %s" % File.basename(policy_file))
end
# Check if a request made by a caller matches the state defined in the policy
def check_policy(rpccaller, actions, facts, classes)
# If we have a wildcard caller or the caller matches our policy line
# then continue else skip this policy line\
if (rpccaller != '*') && (rpccaller != @caller)
return false
end
# If we have a wildcard actions list or the request action is in the list
# of actions in the policy line continue, else skip this policy line
if (actions != '*') && !(actions.split.include?(@action))
return false
end
unless classes
return parse_compound(facts)
else
return parse_facts(facts) && parse_classes(classes)
end
end
def parse_facts(facts)
return true if facts == '*'
if is_compound?(facts)
return parse_compound(facts)
else
facts.split.each do |fact|
return false unless lookup_fact(fact)
end
end
true
end
def parse_classes(classes)
return true if classes == '*'
if is_compound?(classes)
return parse_compound(classes)
else
classes.split.each do |klass|
return false unless lookup_class(klass)
end
end
true
end
def lookup_fact(fact)
if fact =~ /(.+)(<|>|=|<=|>=)(.+)/
lv = $1
sym = $2
rv = $3
sym = '==' if sym == '='
return eval("'#{Util.get_fact(lv)}'#{sym}'#{rv}'")
else
Log.warn("Class found where fact was expected")
return false
end
end
def lookup_class(klass)
if klass =~ /(.+)(<|>|=|<=|>=)(.+)/
Log.warn("Fact found where class was expected")
return false
else
return Util.has_cf_class?(klass)
end
end
def lookup(token)
if token =~ /(.+)(<|>|=|<=|>=)(.+)/
return lookup_fact(token)
else
return lookup_class(token)
end
end
# Here we lookup the full path of the policy file. If the policyfile
# does not exist, we check to see if a default file was set and
# determine its full path. If no default file exists, or default was
# not specified, we return false.
def lookup_policy_file
policy_file = File.join(@configdir, "policies", "#{@agent}.policy")
Log.debug("Looking for policy in #{policy_file}")
return policy_file if File.exist?(policy_file)
if @config.pluginconf.fetch('actionpolicy.enable_default', 'n') =~ /^1|y/i
defaultname = @config.pluginconf.fetch('actionpolicy.default_name', 'default')
default_file = File.join(@configdir, "policies", "#{defaultname}.policy")
Log.debug("Initial lookup failed: looking for policy in #{default_file}")
return default_file if File.exist?(default_file)
end
Log.debug('Could not find any policy files.')
nil
end
# Evalute a compound statement and return its truth value
def eval_statement(statement)
token_type = statement.keys.first
token_value = statement.values.first
return token_value if (token_type != 'statement' && token_type != 'fstatement')
if token_type == 'statement'
return lookup(token_value)
elsif token_type == 'fstatement'
begin
return Matcher.eval_compound_fstatement(token_value)
rescue => e
Log.warn("Could not call Data function in policy file: #{e}")
return false
end
end
end
def is_compound?(list)
list.split.each do |token|
if token =~ /^!|^not$|^or$|^and$|\(.+\)/
return true
end
end
false
end
def parse_compound(list)
stack = Matcher.create_compound_callstack(list)
begin
stack.map!{ |item| eval_statement(item) }
rescue => e
Log.debug(e.to_s)
return false
end
eval(stack.join(' '))
end
def deny(logline)
Log.debug(logline)
raise(RPCAborted, 'You are not authorized to call this agent or action.')
end
end
end
end