19210 - Publish the actionpolicy simple rpc authorization plugin
Refactored, added tests and updated docs
This commit is contained in:
9
util/actionpolicy.ddl
Normal file
9
util/actionpolicy.ddl
Normal file
@@ -0,0 +1,9 @@
|
||||
metadata :name => "actionpolicy",
|
||||
:description => "Action Policy simplerpc authorization plugin",
|
||||
:author => "P.Loubser <pieter.loubser@puppetlabs.com>",
|
||||
:license => "ASL 2.0",
|
||||
:version => "2.0.0",
|
||||
:url => "https://github.com/puppetlabs/mcollective-actionpolicy-authorization",
|
||||
:timeout => 1
|
||||
|
||||
requires :mcollective => "2.2.1"
|
||||
219
util/actionpolicy.rb
Normal file
219
util/actionpolicy.rb
Normal file
@@ -0,0 +1,219 @@
|
||||
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
|
||||
Reference in New Issue
Block a user