19210 - Publish the actionpolicy simple rpc authorization plugin

Refactored, added tests and updated docs
This commit is contained in:
R.I.Pienaar
2013-03-19 16:39:33 +00:00
parent 77fe7664d7
commit 84ff5d959d
25 changed files with 903 additions and 2 deletions

View File

@@ -1,4 +1,91 @@
mcollective-actionpolicy-auth Action Policy Authorization Plugin
============================= =============================
MCollective Authorization plugin allowing fine grained ACLs This is a plugin that provides fine grained action level authorization for agents.
Installation
=============================
* Follow the [basic plugin install guide](http://projects.puppetlabs.com/projects/mcollective-plugins/wiki/InstalingPlugins) by placing actionpolicy.rb
and actionpolicy.ddl in the util directory.
Note that it is not currently possible to use the 'mco plugin package' command to package this plugin.
Configuration
=============================
There are three configuration options for the actionpolicy plugin
* allow_unconfigured - allow requests to agents that do not have policy files configured
* enable_default - enables a default policy file
* default_name - the name of the default policy file
General authentication configuration options can also be set in the config file.
# Enables system wide rpc authorization
rpcauthorization = 1
# Sets the authorization provider to use the actionpolicy plugin
rpcauthprovider = action_policy
Enabling a default policy
plugin.actionpolicy.enable_default = 1
plugin.actionpolicy.default_name = default
This allows you to create a policy file called default.policy which will be used unless a specific policy file exists.
Usage
=============================
Policies are defined in files like <configdir>/policies/<agent>.policy
Example: Puppet agent policy file
policy default deny
allow uid=500 * * *
allow uid=600 * customer=acme acme::devserver
allow uid=600 enable disable status customer=acme *
allow uid=700 restart (puppet().enabled=false and environment=production) or environment=development
The above policy can be described as:
* allow unix user id 500 to do all actions on all servers.
* allow unix user id 600 to do all actions on machines with the fact customer=acme and the config class acme::devserver
* allow unix user id 600 to do enable, disable and status on all other machines with fact customer=acme
* allow unix user id 700 to restart services at any time in development but in production only when Puppet has been disabled
* Everything else gets denied
The format of the userid will depend on your security plugin, other plugins might have a certificate name as caller it.
Like with actions you can space separate facts and config classes too which means all facts of classes listed has to be present on the system.
The last line in the example uses the compound statement language to do matching on facts and classes and allows any data plugin to be used.
This requires at least MCollective 2.2.x. When using data plugins in action policies you should avoid using slow ones as this will impact
the response times of agents and impact the client waiting time etc.
Using it in a specific Agent
=============================
You can now activate it in your agents:
module MCollective::Agent
class Service<RPC::Agent
authorized_by :action_policy
# ...
end
end
System wide configuration
=============================
You can apply this policy to all agents but ones that specify specific policies as above will use that:
# authorization
rpcauthorization = 1
rpcauthprovider = action_policy
plugin.actionpolicy.allow_unconfigured = 1
This enables system wide authorization, tells it to use the action_policy plugin and tells it to allow agents without a policy to be used.
If you had set allow_unconfigured to 0 all requests to agents without policy files will be denied. This is configured in your server.cfg file.

20
Rakefile Normal file
View File

@@ -0,0 +1,20 @@
specdir = File.join([File.dirname(__FILE__), "spec"])
require 'rake'
begin
require 'rspec/core/rake_task'
rescue LoadError
end
if defined?(RSpec::Core::RakeTask)
desc "Run plugin tests"
RSpec::Core::RakeTask.new(:test) do |t|
require "#{specdir}/spec_helper.rb"
t.pattern = 'spec/**/*_spec.rb'
tmp_load_path = $LOAD_PATH.map { |f| f.shellescape }.join(" -I ")
t.rspec_opts = tmp_load_path + " " + File.read("#{specdir}/spec.opts").chomp
end
end
task :default => :test

2
puppet.policy Normal file
View File

@@ -0,0 +1,2 @@
policy default deny
allow uid=500 status enable disable country=uk apache

View File

@@ -0,0 +1,510 @@
#!/bin/env rspec
require 'spec_helper'
require File.join(File.dirname(__FILE__), '../../', 'util', 'actionpolicy.rb')
module MCollective
module Util
describe ActionPolicy do
let(:request) do
request = mock
request.stubs(:agent).returns('rspec_agent')
request.stubs(:caller).returns('rspec_caller')
request.stubs(:action).returns('rspec_action')
request
end
let(:config) do
config = mock
config.stubs(:configdir).returns('/rspecdir')
config.stubs(:pluginconf).returns({})
config
end
let(:actionpolicy) { ActionPolicy.new(request) }
before do
Config.stubs(:instance).returns(config)
@fixtures_dir = File.join(File.dirname(__FILE__), 'fixtures')
end
describe '#authorize' do
it 'should create a new ActionPolicy object and call #authorize_request' do
actionpolicy.expects(:authorize_request)
ActionPolicy.expects(:new).returns(actionpolicy)
ActionPolicy.authorize(request)
end
end
describe '#initialize' do
it 'should set the default values' do
actionpolicy.config.should == config
actionpolicy.agent.should == 'rspec_agent'
actionpolicy.caller.should == 'rspec_caller'
actionpolicy.action.should == 'rspec_action'
actionpolicy.allow_unconfigured.should == false
actionpolicy.configdir.should == '/rspecdir'
end
it 'should set allow_unconfigured if set in config file' do
config.stubs(:pluginconf).returns({'actionpolicy.allow_unconfigured' => '1'})
result = ActionPolicy.new(request)
result.allow_unconfigured.should == true
end
end
describe '#authorize_request' do
before do
Log.stubs(:debug)
end
it 'should deny the request if policy file does not exist and allow_unconfigured is false' do
ActionPolicy.any_instance.expects(:lookup_policy_file).returns(nil)
expect{
actionpolicy.authorize_request
}.to raise_error RPCAborted
end
it 'should return true if policy file does not exist but allow_unconfigured is true' do
ActionPolicy.any_instance.expects(:lookup_policy_file).returns(nil)
config.stubs(:pluginconf).returns({'actionpolicy.allow_unconfigured' => 'y'})
actionpolicy.authorize_request.should be_true
end
it 'should parse the policy file if it exists' do
ActionPolicy.any_instance.expects(:lookup_policy_file).returns('/rspecdir/policyfile')
ActionPolicy.any_instance.expects(:parse_policy_file).with('/rspecdir/policyfile')
actionpolicy.authorize_request
end
end
describe '#parse_policy_file' do
before do
Log.stubs(:debug)
end
it 'should deny the request if allow_unconfigured is false and no lines match' do
File.expects(:read).with('policyfile').returns('')
expect{
actionpolicy.parse_policy_file('policyfile')
}.to raise_error RPCAborted
end
it 'should skip comment lines' do
File.expects(:read).with('policyfile').returns('#')
expect{
actionpolicy.parse_policy_file('policyfile')
}.to raise_error RPCAborted
end
# Fixtures
it 'should parse the default alllow policy' do
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'default_allow')).should be_true
end
it 'should parse the default deny policy' do
expect{
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'default_deny'))
}.to raise_error RPCAborted
end
# Example fixtures
it 'should parse example1 correctly' do
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example1')).should be_true
end
it 'should parse example2 correctly' do
request.stubs(:caller).returns('uid=500')
actionpolicy = ActionPolicy.new(request)
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example2')).should be_true
request.stubs(:caller).returns('uid=501')
actionpolicy = ActionPolicy.new(request)
expect{
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example2'))
}.to raise_error RPCAborted
end
it 'should parse example3 correctly' do
request.stubs(:action).returns('rspec')
actionpolicy = ActionPolicy.new(request)
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example3')).should be_true
request.stubs(:action).returns('notrspec')
actionpolicy = ActionPolicy.new(request)
expect{
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example3'))
}.to raise_error RPCAborted
end
it 'should parse example4 correctly' do
Util.stubs(:get_fact).with('foo').returns('bar')
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example4')).should be_true
Util.stubs(:get_fact).with('foo').returns('notbar')
expect{
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example4'))
}.to raise_error RPCAborted
end
it 'should parse example5 correctly' do
Util.stubs(:has_cf_class?).with('rspec').returns(true)
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example5')).should be_true
Util.stubs(:has_cf_class?).with('rspec').returns(false)
expect{
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example5'))
}.to raise_error RPCAborted
end
it 'should parse example6 correctly' do
request.stubs(:caller).returns('uid=500')
request.stubs(:action).returns('rspec')
actionpolicy = ActionPolicy.new(request)
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example6')).should be_true
request.stubs(:caller).returns('uid=501')
request.stubs(:action).returns('notrspec')
actionpolicy = ActionPolicy.new(request)
expect{
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example6'))
}.to raise_error RPCAborted
end
it 'should parse example7 correctly' do
request.stubs(:caller).returns('uid=500')
Util.stubs(:get_fact).with('foo').returns('bar')
actionpolicy = ActionPolicy.new(request)
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example7')).should be_true
request.stubs(:caller).returns('uid=501')
Util.stubs(:get_fact).with('foo').returns('notbar')
actionpolicy = ActionPolicy.new(request)
expect{
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example7'))
}.to raise_error RPCAborted
end
it 'should parse example8 correctly' do
request.stubs(:caller).returns('uid=500')
Util.stubs(:has_cf_class?).with('rspec').returns(true)
actionpolicy = ActionPolicy.new(request)
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example8')).should be_true
Util.stubs(:has_cf_class?).with('rspec').returns(false)
actionpolicy = ActionPolicy.new(request)
expect{
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example8'))
}.to raise_error RPCAborted
end
it 'should parse example9 correctly' do
request.stubs(:caller).returns('uid=500')
request.stubs(:action).returns('rspec')
Util.stubs(:get_fact).with('foo').returns('bar')
actionpolicy = ActionPolicy.new(request)
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example9')).should be_true
request.stubs(:caller).returns('uid=501')
request.stubs(:action).returns('notrspec')
Util.stubs(:get_fact).with('foo').returns('notbar')
actionpolicy = ActionPolicy.new(request)
expect{
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example9'))
}.to raise_error RPCAborted
end
it 'should parse example10 correctly' do
request.stubs(:caller).returns('uid=500')
request.stubs(:action).returns('rspec')
Util.stubs(:has_cf_class?).with('rspec').returns(true)
actionpolicy = ActionPolicy.new(request)
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example10')).should be_true
request.stubs(:caller).returns('uid=501')
request.stubs(:action).returns('notrspec')
Util.stubs(:has_cf_class?).with('rspec').returns(false)
actionpolicy = ActionPolicy.new(request)
expect{
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example10'))
}.to raise_error RPCAborted
end
it 'should parse example11 correctly' do
request.stubs(:caller).returns('uid=500')
request.stubs(:action).returns('rspec')
Util.stubs(:has_cf_class?).with('rspec').returns(true)
Util.stubs(:get_fact).with('foo').returns('bar')
actionpolicy = ActionPolicy.new(request)
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example10')).should be_true
request.stubs(:caller).returns('uid=501')
request.stubs(:action).returns('notrspec')
Util.stubs(:has_cf_class?).with('rspec').returns(false)
Util.stubs(:get_fact).with('foo').returns('notbar')
actionpolicy = ActionPolicy.new(request)
expect{
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example10'))
}.to raise_error RPCAborted
end
it 'should parse example12 correctly' do
request.stubs(:caller).returns('uid=500')
request.stubs(:action).returns('rspec')
Util.stubs(:has_cf_class?).with('rspec').returns(true)
Util.stubs(:get_fact).with('foo').returns('bar')
Util.stubs(:get_fact).with('bar').returns('foo')
actionpolicy = ActionPolicy.new(request)
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example12')).should be_true
end
it 'should parse example13 correctly' do
request.stubs(:caller).returns('uid=500')
request.stubs(:action).returns('rspec')
Util.stubs(:has_cf_class?).with('one').returns(true)
Util.stubs(:has_cf_class?).with('two').returns(true)
Util.stubs(:has_cf_class?).with('three').returns(false)
Util.stubs(:get_fact).with('foo').returns('bar')
actionpolicy = ActionPolicy.new(request)
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example13')).should be_true
end
it 'should parse example14 correctly' do
request.stubs(:caller).returns('uid=500')
request.stubs(:action).returns('rspec')
Util.stubs(:has_cf_class?).with('one').returns(true)
Util.stubs(:has_cf_class?).with('two').returns(false)
Util.stubs(:get_fact).with('foo').returns('bar')
actionpolicy = ActionPolicy.new(request)
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example14')).should be_true
end
it 'should parse example15 correctly' do
# first field
request.stubs(:caller).returns('uid=500')
actionpolicy = ActionPolicy.new(request)
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example15')).should be_true
# second field
request.stubs(:caller).returns('uid=600')
Util.stubs(:get_fact).with('customer').returns('acme')
Util.stubs(:has_cf_class?).with('acme::devserver').returns(true)
actionpolicy = ActionPolicy.new(request)
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example15')).should be_true
# third field
request.stubs(:caller).returns('uid=600')
request.stubs(:action).returns('status')
Util.stubs(:get_fact).with('customer').returns('acme')
actionpolicy = ActionPolicy.new(request)
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example15')).should be_true
# forth field
request.stubs(:caller).returns('uid=600')
request.stubs(:action).returns('status')
Util.stubs(:get_fact).with('customer').returns('acme')
actionpolicy = ActionPolicy.new(request)
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example15')).should be_true
# fith field
request.stubs(:caller).returns('uid=700')
request.stubs(:action).returns('restart')
Util.stubs(:get_fact).with('environment').returns('development')
Matcher.stubs(:eval_compound_fstatement).with('value' => 'enabled', 'name' => 'puppet', 'operator' => '==', 'params' => nil, 'r_compare' => 'false').returns(true)
actionpolicy = ActionPolicy.new(request)
actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example15')).should be_true
end
end
describe '#check_policy' do
it 'should return false if the policy line does not include the caller' do
actionpolicy.check_policy('caller', nil, nil, nil).should be_false
end
it 'should return false if the policy line does not include the action' do
actionpolicy.check_policy(nil, 'action', nil, nil).should be_false
end
it 'should parse both facts and classes if callers and actions match' do
actionpolicy.expects(:parse_facts).with('*').returns(true)
actionpolicy.expects(:parse_classes).with('*').returns(true)
actionpolicy.check_policy('rspec_caller', 'rspec_action', '*', '*').should be_true
end
it 'should parse a compound statement if callers and actions match but classes are excluded' do
actionpolicy.expects(:parse_compound).with('*').returns(true)
actionpolicy.check_policy('rspec_caller', 'rspec_action', '*', nil).should be_true
end
end
describe '#parse_facts' do
it 'should return true if facts is a wildcard' do
actionpolicy.parse_facts('*').should be_true
end
it 'should parse compound fact statements' do
actionpolicy.stubs(:is_compound?).returns(true)
actionpolicy.expects(:parse_compound).with('foo=bar and bar=foo').returns(true)
actionpolicy.parse_facts('foo=bar and bar=foo').should be_true
end
it 'should parse all facts' do
actionpolicy.stubs(:is_compound?).returns(false)
actionpolicy.expects(:lookup_fact).twice.returns(true)
actionpolicy.parse_facts('foo=bar bar=foo').should be_true
end
end
describe '#parse_classes' do
it 'should return true if classes is a wildcard' do
actionpolicy.parse_classes('*').should be_true
end
it 'should parse compound class statements' do
actionpolicy.stubs(:is_compound?).returns(true)
actionpolicy.expects(:parse_compound).with('foo=bar and bar=foo').returns(true)
actionpolicy.parse_facts('foo=bar and bar=foo').should be_true
end
it 'should parse all classes' do
actionpolicy.stubs(:is_compound?).returns(false)
actionpolicy.expects(:lookup_fact).times(3).returns(true)
actionpolicy.parse_facts('foo bar baz').should be_true
end
end
describe '#lookup_fact' do
it 'should return false if a class is found in the fact field' do
Log.expects(:warn).with('Class found where fact was expected')
actionpolicy.lookup_fact('rspec').should be_false
end
it 'should lookup a fact value and return its true value' do
Util.expects(:get_fact).with('foo').returns('bar')
actionpolicy.lookup_fact('foo=bar').should be_true
end
end
describe '#lookup_class' do
it 'should return false if a fact is found in the class field' do
Log.expects(:warn).with('Fact found where class was expected')
actionpolicy.lookup_class('foo=bar').should be_false
end
it 'should lookup a fact value and return its true value' do
Util.expects(:has_cf_class?).with('rspec').returns(true)
actionpolicy.lookup_class('rspec').should be_true
end
end
describe '#lookup' do
it 'should call #lookup_fact if a fact was passed' do
actionpolicy.expects(:lookup_fact).with('foo=bar').returns(true)
actionpolicy.lookup('foo=bar').should be_true
end
it 'should call #lookup_class if a class was passed' do
actionpolicy.expects(:lookup_class).with('/rspec/').returns(true)
actionpolicy.lookup('/rspec/').should be_true
end
end
describe '#lookup_policy_file' do
before do
Log.stubs(:debug)
end
it 'should return the path of the policyfile is present' do
File.expects(:exist?).with('/rspecdir/policies/rspec_agent.policy').returns(true)
actionpolicy.lookup_policy_file.should == '/rspecdir/policies/rspec_agent.policy'
end
it 'should return the default file path if one is specified' do
config.stubs(:pluginconf).returns({'actionpolicy.enable_default' => '1'})
File.expects(:exist?).with('/rspecdir/policies/rspec_agent.policy').returns(false)
File.expects(:exist?).with('/rspecdir/policies/default.policy').returns(true)
actionpolicy.lookup_policy_file.should == '/rspecdir/policies/default.policy'
end
it 'should return a custom default file path if one is specified' do
config.stubs(:pluginconf).returns({'actionpolicy.enable_default' => '1',
'actionpolicy.default_name' => 'rspec'})
File.expects(:exist?).with('/rspecdir/policies/rspec_agent.policy').returns(false)
File.expects(:exist?).with('/rspecdir/policies/rspec.policy').returns(true)
actionpolicy.lookup_policy_file.should == '/rspecdir/policies/rspec.policy'
end
it 'should return nil if no policy file exists' do
File.expects(:exist?).with('/rspecdir/policies/rspec_agent.policy').returns(false)
actionpolicy.lookup_policy_file.should == nil
end
end
describe '#eval_statement' do
it 'should return the logical string if param is not an statement or fstatement' do
actionpolicy.eval_statement({'and' => 'and'}).should == 'and'
end
it 'should lookup the value of a statement if param is a statement' do
actionpolicy.expects(:lookup).with('foo=bar').returns(true)
actionpolicy.eval_statement({'statement' => 'foo=bar'}).should be_true
end
it 'should lookup the value of a data function if param is a fstatement' do
Matcher.expects(:eval_compound_fstatement).with("rspec('data').value=result").returns(true)
actionpolicy.eval_statement({'fstatement' => "rspec('data').value=result"}).should be_true
end
it 'should log a failure message and return false if the fstatement cannot be parsed' do
Matcher.expects(:eval_compound_fstatement).with("rspec('data').value=result").raises('error')
Log.expects(:warn).with('Could not call Data function in policy file: error')
actionpolicy.eval_statement({'fstatement' => "rspec('data').value=result"}).should be_false
end
end
describe '#is_compound?' do
it 'should return false if a compound statement was not identified' do
actionpolicy.is_compound?('not').should be_true
actionpolicy.is_compound?('!rspec').should be_true
actionpolicy.is_compound?('and').should be_true
actionpolicy.is_compound?('or').should be_true
actionpolicy.is_compound?("data('field').value=othervalue").should be_true
end
it 'should return true if a compound statement was identified' do
actionpolicy.is_compound?('f1=v1 f1=v2').should be_false
actionpolicy.is_compound?('class1 class2 /class*/').should be_false
end
end
describe '#deny' do
it 'should log the failure and raise an RPCAborted error' do
Log.expects(:debug).with('fail')
expect{
actionpolicy.deny('fail')
}.to raise_error RPCAborted
end
end
end
end
end

View File

@@ -0,0 +1 @@
policy default allow

View File

@@ -0,0 +1 @@
policy default deny

View File

@@ -0,0 +1,2 @@
policy default deny
allow * * * *

View File

@@ -0,0 +1,2 @@
policy default deny
allow uid=500 rspec * rspec

View File

@@ -0,0 +1,2 @@
policy default deny
allow uid=500 rspec foo=bar rspec

View File

@@ -0,0 +1,2 @@
policy default deny
allow uid=500 rspec foo=bar and bar=foo rspec

View File

@@ -0,0 +1,2 @@
policy default deny
allow uid=500 rspec foo=bar one and two or three

View File

@@ -0,0 +1,2 @@
policy default deny
allow uid=500 rspec foo=bar and one or (two and one)

View File

@@ -0,0 +1,5 @@
policy default deny
allow uid=500 * * *
allow uid=600 * customer=acme acme::devserver
allow uid=600 enable disable status customer=acme *
allow uid=700 restart (puppet().enabled=false and environment=production) or environment=development

View File

@@ -0,0 +1,2 @@
policy default deny
allow uid=500 * * *

View File

@@ -0,0 +1,2 @@
policy default deny
allow * rspec * *

View File

@@ -0,0 +1,2 @@
policy default deny
allow * * foo=bar *

View File

@@ -0,0 +1,2 @@
policy default deny
allow * * * rspec

View File

@@ -0,0 +1,2 @@
policy default deny
allow uid=500 rspec * *

View File

@@ -0,0 +1,2 @@
policy default deny
allow uid=500 * foo=bar *

View File

@@ -0,0 +1,2 @@
policy default deny
allow uid=500 * * rspec

View File

@@ -0,0 +1,2 @@
policy default deny
allow uid=500 rspec foo=bar *

1
spec/spec.opts Normal file
View File

@@ -0,0 +1 @@
--colour --backtrace

18
spec/spec_helper.rb Normal file
View File

@@ -0,0 +1,18 @@
$: << File.join([File.dirname(__FILE__), "lib"])
require 'rubygems'
require 'rspec'
require 'mcollective'
require 'mcollective/test'
require 'rspec/mocks'
require 'mocha'
require 'tempfile'
RSpec.configure do |config|
config.mock_with :mocha
config.include(MCollective::Test::Matchers)
config.before :each do
MCollective::PluginManager.clear
end
end

9
util/actionpolicy.ddl Normal file
View 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
View 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